Newer
Older
TIASshot / docs / 03_PLAN / PLAN_03_プレビューメモリリーク修正計画.md

プレビューメモリリーク修正計画 (Preview Memory Leak Fix Plan)

前提 (Prerequisites)

  • 本計画は,PLAN_02_バグ修正計画.md(連続撮影クラッシュ=撮影経路の修正)とは独立した別系統のバグを扱う.PLAN_02 は「撮影時」の不具合,本計画は「プレビュー表示中(撮影していなくても)長時間起動で落ちる」不具合を対象とする.
  • 修正は1件ずつ独立して行い,都度ビルド・動作確認する.
  • コミットメッセージは [fix] タグを使用する.
  • 該当箇所はリファクタ後の現構造(Cameras/UI/)で記載する.プレビュー供給は Cameras/IScam.csCameras/Lucam.cs,表示は UI/Form1.csUI/PreviewMonitor.cs に分かれている.

症状 (Symptom)

  • ver1.6 で,アプリを長時間起動していると落ちることがあった.
  • 連続撮影の有無に関わらず発生する(撮影せずプレビュー表示のまま放置していても起こりうる).
  • ver1.6(タグ v1.6)と現行コードの該当経路を比較した結果,プレビュー表示経路は v1.6 からほぼ同一で未修正であり,現行でも同じクラッシュが起こりうる.

原因 (Cause)

プレビューはカメラのフレームレート(30〜60 fps 相当)で連続的にフレームを供給し続ける.IScam では Cameras/IScam.csRetrieveFrameQueueSink コールバック)が毎フレーム _form.ShowImage(imgt.ToBitmap()) を呼ぶ.この経路で Bitmap が確実に解放されず,時間とともに蓄積する.

Bitmap は内部に GDI ハンドル(HBITMAP)とネイティブ画素バッファ(IScam の 1840×2440×3 で約 13 MB/枚)を持つ.一方マネージドのラッパは小さいため GC がメモリ圧を感じにくく,回収が遅れる.結果として GDI ハンドルの枯渇(既定上限はプロセスあたり 1 万)ワーキングセット肥大による OutOfMemory/GDI+ のエラーで,時間とともにクラッシュする.確率的に「たまに/いずれ」落ちる症状と整合する.

原因1: ShowImage のフルコピー Bitmap が毎フレームリークする(決定的・最優先)

  • UI/Form1.csShowImage(Bitmap bmp) は冒頭で var bmp2 = new Bitmap(bmp);フルコピー Bitmap を毎フレーム生成する.
  • bmp2_previewMonitor.IsShown が true のとき(副プレビューモニタ表示時)だけ PreviewMonitor.UpdateImage に渡される.通常運用(副モニタ非表示)では bmp2 はどこにも使われず,かつ Dispose されないため,1 フレームごとに 1 枚まるごとリークする.
  • 撮影せずプレビュー表示しているだけでも蓄積する=「長時間アイドルで落ちる」の主因.

該当箇所

  • UI/Form1.csShowImagebmp2 = new Bitmap(bmp) と,IsShown 分岐).

原因2: 表示バッファの取りこぼしフレームが Dispose されない

  • ShowImage_dispBuf = bmp; で表示バッファ参照を差し替え,Invalidate() で再描画を要求する.実際の Dispose は Form1_PaintpicDisplay.Image?.Dispose(); で行われる.
  • Invalidate() による WM_PAINT は合体(コアレス)されるため,Form1_Paint が走る前に次フレームが届くと _dispBuf が複数回上書きされる.IScam 経路は二重バッファを持たないため,Paint に到達しなかった ToBitmap() 結果は Dispose を通らずに捨てられ,リークする

該当箇所

  • UI/Form1.csShowImage_dispBuf 差し替え)/Form1_PaintpicDisplay.Image?.Dispose()).
  • Cameras/IScam.csRetrieve(毎フレーム ToBitmap()ShowImage へ渡す.二重バッファなし).
  • Cameras/Lucam.csPreviewCallback_bmps[2] の二重バッファで ToBitmap() 結果を 2 枚に抑えて Dispose しており,この点は安全.ただし原因1(ShowImage 内の bmp2)は Lucam 経路でも同様にリークする.

原因3: PreviewMonitor.UpdateImage の上書き前バッファが Dispose されない

  • UI/PreviewMonitor.csUpdateImage も単一バッファを上書きする構造で,差し替え前の旧バッファを Dispose していない(副プレビューモニタ表示時のみ顕在化).

該当箇所

  • UI/PreviewMonitor.csUpdateImage(表示バッファ上書き).

修正方針 (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)は撮影フロー対象であり,本件(プレビュー放置)はカバーしていない.プレビュー放置シナリオの観測は別途行う.
  • GDI ハンドルのリークが疑われるため,観測ウィンドウに GDI オブジェクト数GetGuiResources 等)の表示を追加して,メモリだけでなくハンドル数も時間で増えないことを確認することを推奨する(任意).

修正順序まとめ (Summary)

順位原因該当ファイルクラッシュとの関連
1ShowImage のフルコピー Bitmap が毎フレームリークUI/Form1.cs直接原因(アイドル時の主因・決定的リーク)
2表示バッファの取りこぼしフレーム未 DisposeUI/Form1.cs / Cameras/IScam.cs直接原因(二重バッファなしで取りこぼし)
3PreviewMonitor.UpdateImage の上書き前未 DisposeUI/PreviewMonitor.cs副モニタ表示時のリーク