diff --git "a/docs/03_PLAN/PLAN_03_\343\203\227\343\203\254\343\203\223\343\203\245\343\203\274\343\203\241\343\203\242\343\203\252\343\203\252\343\203\274\343\202\257\344\277\256\346\255\243\350\250\210\347\224\273.md" "b/docs/03_PLAN/PLAN_03_\343\203\227\343\203\254\343\203\223\343\203\245\343\203\274\343\203\241\343\203\242\343\203\252\343\203\252\343\203\274\343\202\257\344\277\256\346\255\243\350\250\210\347\224\273.md" new file mode 100644 index 0000000..155b4a3 --- /dev/null +++ "b/docs/03_PLAN/PLAN_03_\343\203\227\343\203\254\343\203\223\343\203\245\343\203\274\343\203\241\343\203\242\343\203\252\343\203\252\343\203\274\343\202\257\344\277\256\346\255\243\350\250\210\347\224\273.md" @@ -0,0 +1,85 @@ +# プレビューメモリリーク修正計画 (Preview Memory Leak Fix Plan) + +## 前提 (Prerequisites) + +- 本計画は,`PLAN_02_バグ修正計画.md`(連続撮影クラッシュ=撮影経路の修正)とは**独立した別系統のバグ**を扱う.PLAN_02 は「撮影時」の不具合,本計画は「プレビュー表示中(撮影していなくても)長時間起動で落ちる」不具合を対象とする. +- 修正は1件ずつ独立して行い,都度ビルド・動作確認する. +- コミットメッセージは `[fix]` タグを使用する. +- 該当箇所はリファクタ後の現構造(`Cameras/`・`UI/`)で記載する.プレビュー供給は `Cameras/IScam.cs`・`Cameras/Lucam.cs`,表示は `UI/Form1.cs`・`UI/PreviewMonitor.cs` に分かれている. + +## 症状 (Symptom) + +- ver1.6 で,アプリを**長時間起動していると落ちる**ことがあった. +- 連続撮影の有無に関わらず発生する(撮影せずプレビュー表示のまま放置していても起こりうる). +- ver1.6(タグ `v1.6`)と現行コードの該当経路を比較した結果,**プレビュー表示経路は v1.6 からほぼ同一で未修正**であり,現行でも同じクラッシュが起こりうる. + +## 原因 (Cause) + +プレビューはカメラのフレームレート(30〜60 fps 相当)で連続的にフレームを供給し続ける.`IScam` では `Cameras/IScam.cs` の `Retrieve`(`FrameQueueSink` コールバック)が毎フレーム `_form.ShowImage(imgt.ToBitmap())` を呼ぶ.この経路で `Bitmap` が確実に解放されず,時間とともに蓄積する. + +`Bitmap` は内部に GDI ハンドル(HBITMAP)とネイティブ画素バッファ(IScam の 1840×2440×3 で約 13 MB/枚)を持つ.一方マネージドのラッパは小さいため GC がメモリ圧を感じにくく,回収が遅れる.結果として **GDI ハンドルの枯渇(既定上限はプロセスあたり 1 万)** か **ワーキングセット肥大による OutOfMemory/GDI+ のエラー**で,時間とともにクラッシュする.確率的に「たまに/いずれ」落ちる症状と整合する. + +### 原因1: `ShowImage` のフルコピー Bitmap が毎フレームリークする(決定的・最優先) + +- `UI/Form1.cs` の `ShowImage(Bitmap bmp)` は冒頭で `var bmp2 = new Bitmap(bmp);` と**フルコピー Bitmap を毎フレーム生成**する. +- `bmp2` は `_previewMonitor.IsShown` が true のとき(副プレビューモニタ表示時)だけ `PreviewMonitor.UpdateImage` に渡される.**通常運用(副モニタ非表示)では `bmp2` はどこにも使われず,かつ Dispose されない**ため,1 フレームごとに 1 枚まるごとリークする. +- 撮影せずプレビュー表示しているだけでも蓄積する=「長時間アイドルで落ちる」の主因. + +**該当箇所** + +- `UI/Form1.cs` の `ShowImage`(`bmp2 = new Bitmap(bmp)` と,`IsShown` 分岐). + +### 原因2: 表示バッファの取りこぼしフレームが Dispose されない + +- `ShowImage` は `_dispBuf = bmp;` で表示バッファ参照を差し替え,`Invalidate()` で再描画を要求する.実際の Dispose は `Form1_Paint` の `picDisplay.Image?.Dispose();` で行われる. +- `Invalidate()` による `WM_PAINT` は合体(コアレス)されるため,`Form1_Paint` が走る前に次フレームが届くと `_dispBuf` が複数回上書きされる.`IScam` 経路は二重バッファを持たないため,**Paint に到達しなかった `ToBitmap()` 結果は Dispose を通らずに捨てられ,リークする**. + +**該当箇所** + +- `UI/Form1.cs` の `ShowImage`(`_dispBuf` 差し替え)/`Form1_Paint`(`picDisplay.Image?.Dispose()`). +- `Cameras/IScam.cs` の `Retrieve`(毎フレーム `ToBitmap()` を `ShowImage` へ渡す.二重バッファなし). +- ※ `Cameras/Lucam.cs` の `PreviewCallback` は `_bmps[2]` の二重バッファで `ToBitmap()` 結果を 2 枚に抑えて Dispose しており,この点は安全.ただし原因1(`ShowImage` 内の `bmp2`)は Lucam 経路でも同様にリークする. + +### 原因3: `PreviewMonitor.UpdateImage` の上書き前バッファが Dispose されない + +- `UI/PreviewMonitor.cs` の `UpdateImage` も単一バッファを上書きする構造で,差し替え前の旧バッファを Dispose していない(副プレビューモニタ表示時のみ顕在化). + +**該当箇所** + +- `UI/PreviewMonitor.cs` の `UpdateImage`(表示バッファ上書き). + +## 修正方針 (Fix Policy) + +### 優先度1: `ShowImage` のフルコピー Bitmap を必要時のみ生成する + +- `bmp2 = new Bitmap(bmp)` を**無条件生成から,`_previewMonitor != null && _previewMonitor.IsShown` が true のときだけ生成**に変更する.未表示時のフルコピーリークを止める(効果が最も大きく,変更も小さい). + +### 優先度2: プレビュー表示バッファを確実に Dispose する + +- 取りこぼしフレームを含め,プレビュー用 `Bitmap` を確実に解放する.方針候補: + - `Lucam` と同様の**二重バッファ+確実な Dispose** に表示経路を統一する,または + - `_dispBuf` を差し替える前に,まだ `picDisplay.Image` に反映されていない旧 `_dispBuf` を Dispose する(Paint を通らなかったフレームも必ず破棄する). +- `picDisplay.Image` への代入は「直前の Image を Dispose してから差し替え」を保証する. + +### 優先度3: `PreviewMonitor.UpdateImage` の上書き前 Dispose + +- `UpdateImage` で表示バッファを上書きする前に,旧バッファを Dispose する. + +## 検証 (Verification) + +- `MEMMONITOR` ビルド(`UI/MemoryMonitorForm`)で,**撮影せずプレビュー表示のまま放置**し,メモリ(PrivateMemory/WorkingSet)が時間とともに増え続けないことを確認する.修正前後で対比する. +- 既存のメモリ計測ベンチ(`PLAN_02` の「効果検証」・[TEST_01](../06_TEST/TEST_01_メモリ修正効果計測.md))は**撮影フロー対象**であり,本件(プレビュー放置)はカバーしていない.プレビュー放置シナリオの観測は別途行う. +- GDI ハンドルのリークが疑われるため,観測ウィンドウに **GDI オブジェクト数**(`GetGuiResources` 等)の表示を追加して,メモリだけでなくハンドル数も時間で増えないことを確認することを推奨する(任意). + +## 修正順序まとめ (Summary) + +| 順位 | 原因 | 該当ファイル | クラッシュとの関連 | +| --- | --- | --- | --- | +| 1 | `ShowImage` のフルコピー Bitmap が毎フレームリーク | `UI/Form1.cs` | 直接原因(アイドル時の主因・決定的リーク) | +| 2 | 表示バッファの取りこぼしフレーム未 Dispose | `UI/Form1.cs` / `Cameras/IScam.cs` | 直接原因(二重バッファなしで取りこぼし) | +| 3 | `PreviewMonitor.UpdateImage` の上書き前未 Dispose | `UI/PreviewMonitor.cs` | 副モニタ表示時のリーク | + +## 関連 (Related) + +- [PLAN_02 バグ修正計画](PLAN_02_バグ修正計画.md) — 連続撮影クラッシュ(撮影経路).本計画とは別系統. +- [TEST_01 メモリ修正効果計測](../06_TEST/TEST_01_メモリ修正効果計測.md) — 撮影経路のメモリ計測結果.