# 帳票設計書 5-メモリスナップショットレポート

## 概要

本ドキュメントは、Lua Language Serverのメモリ解析機能によって生成される「メモリスナップショットレポート」の帳票設計を定義する。この帳票は、Luaランタイムのメモリ使用状況を解析し、オブジェクト参照関係をレポート形式で出力する。

### 本帳票の処理概要

本帳票は、`script/doctor.lua`モジュールによって提供されるメモリ診断機能であり、Luaランタイムの内部状態をスナップショットとして取得し、オブジェクト間の参照関係を追跡可能な形式で出力する。

**業務上の目的・背景**：Lua Language Serverの開発・保守において、メモリリークの検出、メモリ使用量の最適化、オブジェクト参照の追跡が必要である。長時間稼働時のメモリ増加問題のデバッグや、パフォーマンスチューニングに活用される。

**帳票の利用シーン**：メモリリーク調査時、パフォーマンス問題の原因特定時、オブジェクト参照関係の確認時、またメモリ使用量の経時変化を比較分析する際に利用される。

**主要な出力内容**：
1. オブジェクト情報（ポインタ、参照カウント、名前、子オブジェクト数）
2. 参照パス（ルートからオブジェクトまでの参照チェーン）
3. 参照関係の種別（field, key, upvalue, metatable, local, stack等）
4. 2つのスナップショット間の差分比較

**帳票の出力タイミング**：プログラム内から`doctor.report()`または`doctor.catch()`を呼び出した際に、Luaテーブル形式で返却される。

**帳票の利用者**：Language Server開発者、パフォーマンスエンジニア、デバッガー

## 帳票種別

診断レポート（メモリ解析）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| API | doctor.report() | Luaコード内から呼び出し | メモリスナップショット全体レポート |
| API | doctor.catch(...) | Luaコード内から呼び出し | 特定オブジェクトの参照パス検索 |
| API | doctor.compare(old, new) | Luaコード内から呼び出し | 2つのレポートの差分比較 |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | Luaテーブル（プログラム内データ構造） |
| 用紙サイズ | N/A |
| 向き | N/A |
| ファイル名 | N/A（メモリ上のデータ） |
| 出力方法 | 関数戻り値 |
| 文字コード | UTF-8 |

### データ構造

| 項目 | 内容 |
|-----|------|
| report形式 | `{point, count, name, childs}[]` |
| catch形式 | `string[][]`（参照パスの配列） |
| snapshot形式 | ツリー構造（内部使用） |

## 帳票レイアウト

### レイアウト概要

report()の戻り値は、各オブジェクトの情報を含む配列として構成される。

```lua
{
  {
    point = "0x12345678",        -- オブジェクトのポインタ
    count = 3,                   -- 参照カウント
    name = "table:0x12345678",   -- オブジェクト名
    childs = 5                   -- 子オブジェクト数
  },
  ...
}
```

catch()の戻り値は、参照パスの配列として構成される。

```lua
{
  {"(root)registry", "(field)number:2|table:0x...", "(metatable)", ...},
  ...
}
```

### ヘッダー部

N/A（配列形式のためヘッダーなし）

### 明細部（report形式）

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | point | オブジェクトポインタ | getPoint(obj) | 16進数文字列 |
| 2 | count | 参照カウント | 走査中にカウント | 整数 |
| 3 | name | オブジェクト名 | formatName(obj) | 型:値 形式 |
| 4 | childs | 子オブジェクト数 | #t.info | 整数 |

### 明細部（catch形式）

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | パス要素 | 参照パスの各ステップ | search(t) | (種別)名前 形式 |

### 参照種別一覧

| 種別 | 説明 | 例 |
|------|------|-----|
| root | ルートノード | (root)registry |
| field | テーブルフィールド | (field)key\|value |
| key | テーブルキー | (key)keyname |
| upvalue | 関数のアップバリュー | (upvalue)varname |
| metatable | メタテーブル | (metatable) |
| local | ローカル変数 | (local)varname |
| stack | スタックフレーム | (stack)1@function:... |
| uservalue | ユーザーデータ値 | (uservalue)number:1 |
| weakvalue-key | 弱値テーブルのキー | (weakvalue-key)... |
| weakkey-field | 弱キーテーブルのフィールド | (weakkey-field)... |
| thread | スレッド | (thread)main |
| _G | グローバルテーブル | (_G)_G |

### フッター部

N/A（配列形式のためフッターなし）

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| Luaランタイム | 解析対象のLua環境 | Yes |
| 除外オブジェクト | doctor.exclude()で指定 | No |
| メインスレッド無視 | _ignoreMainThread設定 | No（デフォルト: true） |

### ソート順

| 優先度 | 項目 | 昇順/降順 |
|-------|------|---------|
| 1 | なし | 走査順 |

### 改ページ条件

N/A（データ構造のため改ページなし）

## データベース参照仕様

### 参照テーブル一覧

本帳票はLuaランタイムのデバッグAPIを使用する。

| データソース | 用途 | 取得方法 |
|-------------|------|---------|
| debug.getregistry() | レジストリテーブル | Lua Debug API |
| debug.getmetatable() | メタテーブル | Lua Debug API |
| debug.getupvalue() | アップバリュー | Lua Debug API |
| debug.getuservalue() | ユーザーデータ値 | Lua Debug API |
| debug.getlocal() | ローカル変数 | Lua Debug API |
| debug.getinfo() | スタック情報 | Lua Debug API |

### テーブル別参照項目詳細

#### Luaオブジェクト

| 参照項目 | 帳票項目との対応 | 取得条件 | 備考 |
|---------|----------------|---------|------|
| type(obj) | name前半 | なし | オブジェクト型 |
| tostring(obj) | point | なし | ポインタ取得 |
| pairs(tbl) | field/key | table時 | フィールド走査 |
| getupvalue() | upvalue | function時 | アップバリュー走査 |

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| ポインタ | `string.format('%p', obj)` または `tostring(obj):match(': (.+)')` | N/A | Lua 5.1互換 |
| 参照カウント | 走査中に同一ポインタの出現回数をカウント | N/A | スナップショット内 |
| 名前 | `type(obj):値` 形式で生成 | N/A | 100文字超は短縮 |

## 処理フロー

### 出力フロー（report）

```mermaid
flowchart TD
    A[doctor.report()呼び出し] --> B[snapshot()でスナップショット取得]
    B --> C[レジストリから走査開始]
    C --> D{オブジェクトあり?}
    D -->|Yes| E{テーブル?}
    E -->|Yes| F[findTable()で走査]
    E -->|No| G{関数?}
    G -->|Yes| H[findFunction()で走査]
    G -->|No| I{ユーザーデータ?}
    I -->|Yes| J[findUserData()で走査]
    I -->|No| K{スレッド?}
    K -->|Yes| L[findThread()で走査]
    K -->|No| D
    F --> M[子要素を再帰走査]
    H --> M
    J --> M
    L --> M
    M --> D
    D -->|No| N[cache集計]
    N --> O[list生成]
    O --> P[戻り値として返却]
```

### 出力フロー（catch）

```mermaid
flowchart TD
    A[doctor.catch(obj1, obj2, ...)呼び出し] --> B[snapshot()でスナップショット取得]
    B --> C[検索対象をtargetsに登録]
    C --> D[ルートからsearch開始]
    D --> E{オブジェクト発見?}
    E -->|Yes| F[参照パスを記録]
    F --> G[resultに追加]
    G --> H[子要素を再帰検索]
    E -->|No| H
    H --> I{全オブジェクト走査完了?}
    I -->|No| D
    I -->|Yes| J[result返却]
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| 特になし | - | - | doctor.luaはpcall等で内部エラーを抑制 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | メモリ内オブジェクト数依存（数千〜数十万） |
| 目標出力時間 | スナップショット取得に数秒〜数十秒（メモリ量依存） |
| 同時出力数上限 | 1（シングルスレッド実行） |

## セキュリティ考慮事項

- スナップショットにはメモリ内の全オブジェクト情報が含まれるため、機密データの漏洩に注意
- 文字列オブジェクトは最大100文字まで出力されるため、長い機密文字列は部分的に露出する可能性

## 備考

- `doctor.enableCache(true)`でキャッシュ有効化、連続検索時のパフォーマンス向上
- `doctor.flushCache()`でキャッシュクリア
- `doctor.exclude(...)`で特定オブジェクトを走査対象から除外可能
- `doctor.ignoreMainThread(flag)`でメインスレッドの走査を制御
- `doctor.compare(old, new)`で2つのレポートの差分を比較可能
- メインスレッドはデフォルトで無視される（弱参照として扱われる）

---

## コードリーディングガイド

本帳票を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

出力されるレポートのデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | doctor.lua | `script/doctor.lua` | report型定義（530行目のLuaDoc注釈） |
| 1-2 | doctor.lua | `script/doctor.lua` | formatName関数（85-149行目）で名前形式を理解 |

**読解のコツ**: `---@alias report {point: string, count: integer, name: string, childs: integer}`がレポート型定義。formatName()でオブジェクト種別ごとの名前生成を確認。

#### Step 2: エントリーポイントを理解する

主要なAPI関数を特定。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | doctor.lua | `script/doctor.lua` | m.report関数（535-573行目） |
| 2-2 | doctor.lua | `script/doctor.lua` | m.catch関数（478-528行目） |
| 2-3 | doctor.lua | `script/doctor.lua` | m.snapshot関数（171-471行目） |

**主要処理フロー**:
1. **535行目**: `m.report()`関数開始
2. **536行目**: `m.snapshot()`でスナップショット取得
3. **540-566行目**: scan()でオブジェクト情報を収集
4. **567-572行目**: cacheからlistを生成して返却

#### Step 3: スナップショット取得処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | doctor.lua | `script/doctor.lua` | snapshot関数（171-471行目） |
| 3-2 | doctor.lua | `script/doctor.lua` | findTable関数（198-268行目） |
| 3-3 | doctor.lua | `script/doctor.lua` | findFunction関数（270-287行目） |
| 3-4 | doctor.lua | `script/doctor.lua` | findUserData関数（289-318行目） |
| 3-5 | doctor.lua | `script/doctor.lua` | findThread関数（320-360行目） |

**主要処理フロー**:
- **198-268行目**: `findTable()`でテーブルの全フィールド走査
- **270-287行目**: `findFunction()`でアップバリュー走査
- **289-318行目**: `findUserData()`でユーザー値・メタテーブル走査
- **320-360行目**: `findThread()`でスタック・ローカル変数走査

#### Step 4: オブジェクト検索処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | doctor.lua | `script/doctor.lua` | catch関数（478-528行目） |
| 4-2 | doctor.lua | `script/doctor.lua` | search関数（499-523行目） |

**主要処理フロー**:
- **479-485行目**: 検索対象をtargetsに登録
- **499-523行目**: search()で再帰的に参照パスを検索
- **500行目**: パス構築（`(種別)名前`形式）
- **502-512行目**: 対象オブジェクト発見時にパスを記録

#### Step 5: ユーティリティ関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | doctor.lua | `script/doctor.lua` | getPoint関数（30-48行目） |
| 5-2 | doctor.lua | `script/doctor.lua` | formatName関数（85-149行目） |
| 5-3 | doctor.lua | `script/doctor.lua` | formatObject関数（50-56行目） |

**主要処理フロー**:
- **30-48行目**: `getPoint()`でオブジェクトポインタ取得（Lua 5.1互換）
- **50-56行目**: `formatObject()`で`型:ポインタ(追加情報)`形式生成
- **85-149行目**: `formatName()`で型ごとに適切な名前生成

### プログラム呼び出し階層図

```
doctor.report()
    │
    └─ m.snapshot()
           │
           ├─ find(registry) ──────────────────┐
           │                                    │
           │      ┌────────────────────────────┼────────────────────────────┐
           │      │                            │                            │
           │      ▼                            ▼                            ▼
           ├─ findTable(t)               findFunction(f)             findUserData(u)
           │      │                            │                            │
           │      ├─ pairs(t)でフィールド走査   ├─ getupvalue()で         ├─ getuservalue()で
           │      ├─ getmetatable(t)で         │   アップバリュー走査      │   ユーザー値走査
           │      │   メタテーブル走査          │                          ├─ getmetatable(u)で
           │      └─ find(子) [再帰]            └─ find(子) [再帰]          │   メタテーブル走査
           │                                                               └─ find(子) [再帰]
           │
           ├─ findThread(trd)
           │      │
           │      ├─ getinfo(trd)でスタック走査
           │      ├─ getlocal()でローカル変数走査
           │      └─ find(子) [再帰]
           │
           └─ scan(snapshot)
                  │
                  ├─ cache[point]でオブジェクト情報集計
                  │
                  └─ list生成・返却

doctor.catch(obj1, obj2, ...)
    │
    ├─ targets登録
    │
    ├─ m.snapshot()
    │
    └─ search(snapshot)
           │
           ├─ path構築
           │
           ├─ targets照合
           │      └─ 発見時: result[#result+1] = path
           │
           └─ 子要素を再帰検索
```

### データフロー図

```
[入力]                    [処理]                         [出力]

Luaランタイム ──────▶ debug.getregistry()
                              │
                              ▼
                        スナップショット構築
                              │
                   ┌──────────┼──────────┐
                   │          │          │
                   ▼          ▼          ▼
              findTable   findFunc   findUserData
                   │          │          │
                   └──────────┼──────────┘
                              │
                              ▼
                        オブジェクト情報集計
                              │
              ┌───────────────┼───────────────┐
              │               │               │
              ▼               ▼               ▼
         report()        catch()         compare()
              │               │               │
              ▼               ▼               ▼
    ─────────────────▶ ─────────────────▶ ─────────────────▶
    {point,count,...}  {{path1},{path2}}  {old,new}[]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| doctor.lua | `script/doctor.lua` | ソース | メモリ解析の全機能を提供 |
