using System;
using NUnit.Framework;
using OpenCvSharp;
namespace TIASshot.Tests {
/// <summary>
/// PLAN_02 優先度2(ConvertImage の中間 Mat 未 Dispose)修正の効果検証ベンチマーク.
///
/// 修正前(リーク版)と修正後(using 版)の ConvertImage を同条件で繰り返し呼び出し,
/// プロセスのメモリ使用量(PrivateMemorySize64 / WorkingSet64)を比較する.
///
/// 注意:
/// - OpenCvSharp の Mat はネイティブメモリを持つ.未 Dispose の Mat は GC+ファイナライザが
/// 走るまで解放されない.連続撮影の本番フローでは GC 前に確保が積み上がり OOM クラッシュする.
/// 本ベンチはループ中に GC を呼ばないことでこの機序を再現する.
/// - 通常のテスト実行では走らない([Explicit] + Category("Benchmark")).
/// 実行は vstest の TestCaseFilter で明示指定する(README 末尾の実行コマンド参照).
///
/// 環境変数でスケール調整可:
/// BENCH_WIDTH 画像幅 (既定 2440)
/// BENCH_HEIGHT 画像高 (既定 1840)
/// BENCH_CHANNEL 拡張チャネル数 (既定 17)
/// BENCH_FRAMES 繰り返し回数 (既定 40)
/// BENCH_CEILING_GB 打ち切り上限GB (既定 10) ← リーク版がここに達したら「限界」として停止
///
/// 計測ユーティリティ(Sample / ReclaimNative / GB / PrintRow / EnvInt / EnvDouble)は
/// MemBenchSupport に集約.
/// </summary>
[TestFixture]
[Explicit("メモリ計測ベンチマーク.フルスケールで大量のメモリを確保するため明示実行のみ.")]
[Category("Benchmark")]
public class ConvertImageMemoryBenchmark {
[Test]
public void Compare_LeakyVsFixed_MemoryUsage() {
int width = MemBenchSupport.EnvInt("BENCH_WIDTH", 2440);
int height = MemBenchSupport.EnvInt("BENCH_HEIGHT", 1840);
int channel = MemBenchSupport.EnvInt("BENCH_CHANNEL", 17);
int frames = MemBenchSupport.EnvInt("BENCH_FRAMES", 40);
long ceiling = (long)(MemBenchSupport.EnvDouble("BENCH_CEILING_GB", 10.0) * 1024 * 1024 * 1024);
var corrector = new ColorCorrector(null, null, new int[] { channel });
// 変換行列(channel x 3).値は任意(メモリ挙動に影響しない).
using (var conv = new Mat(channel, 3, MatType.CV_64FC1, new Scalar(0.01))) {
TestContext.WriteLine("=== ConvertImage メモリ計測ベンチマーク(PLAN_02 優先度2) ===");
TestContext.WriteLine($"画像: {width}x{height} CV_8UC3 / チャネル: {channel} / 繰り返し: {frames} 回");
TestContext.WriteLine($"打ち切り上限: {MemBenchSupport.GB(ceiling):F1} GB (PrivateMemory)");
TestContext.WriteLine("");
// --- 修正後(using 版)を先にクリーンな状態で計測 ---
var fixedResult = RunVariant(
"修正後(using 版)",
(src) => corrector.ConvertImage(src, conv),
width, height, frames, ceiling);
// リーク版の前にネイティブメモリを回収して baseline を揃える
MemBenchSupport.ReclaimNative();
// --- 修正前(リーク版)を計測 ---
var leakyResult = RunVariant(
"修正前(リーク版)",
(src) => MemBenchSupport.ConvertImageLeaky(corrector, src, conv),
width, height, frames, ceiling);
// 放置したネイティブメモリを後始末(後続テストへの影響を防ぐ)
MemBenchSupport.ReclaimNative();
// --- 比較サマリ ---
TestContext.WriteLine("");
TestContext.WriteLine("================ 比較サマリ ================");
MemBenchSupport.PrintRow("項目", "修正前(リーク版)", "修正後(using 版)");
MemBenchSupport.PrintRow("完了/打ち切り",
leakyResult.Aborted ? $"打ち切り(iter {leakyResult.Iterations})" : $"完走({leakyResult.Iterations})",
fixedResult.Aborted ? $"打ち切り(iter {fixedResult.Iterations})" : $"完走({fixedResult.Iterations})");
MemBenchSupport.PrintRow("Private baseline",
$"{MemBenchSupport.GB(leakyResult.BasePriv):F2} GB",
$"{MemBenchSupport.GB(fixedResult.BasePriv):F2} GB");
MemBenchSupport.PrintRow("Private peak",
$"{MemBenchSupport.GB(leakyResult.PeakPriv):F2} GB",
$"{MemBenchSupport.GB(fixedResult.PeakPriv):F2} GB");
MemBenchSupport.PrintRow("Private 増分(peak-base)",
$"{MemBenchSupport.GB(leakyResult.PeakPriv - leakyResult.BasePriv):F2} GB",
$"{MemBenchSupport.GB(fixedResult.PeakPriv - fixedResult.BasePriv):F2} GB");
MemBenchSupport.PrintRow("WorkingSet peak",
$"{MemBenchSupport.GB(leakyResult.PeakWs):F2} GB",
$"{MemBenchSupport.GB(fixedResult.PeakWs):F2} GB");
MemBenchSupport.PrintRow("1回あたり平均増分",
$"{(leakyResult.Iterations > 0 ? MemBenchSupport.GB(leakyResult.PeakPriv - leakyResult.BasePriv) / leakyResult.Iterations * 1024 : 0):F0} MB",
$"{(fixedResult.Iterations > 0 ? MemBenchSupport.GB(fixedResult.PeakPriv - fixedResult.BasePriv) / fixedResult.Iterations * 1024 : 0):F0} MB");
TestContext.WriteLine("============================================");
var leakyGrowth = leakyResult.PeakPriv - leakyResult.BasePriv;
var fixedGrowth = fixedResult.PeakPriv - fixedResult.BasePriv;
TestContext.WriteLine("");
TestContext.WriteLine(
$"判定: 修正後の増分({MemBenchSupport.GB(fixedGrowth):F2} GB) は 修正前の増分({MemBenchSupport.GB(leakyGrowth):F2} GB) の "
+ $"{(leakyGrowth > 0 ? (double)fixedGrowth / leakyGrowth * 100 : 0):F1}% に抑制された.");
// この閾値判定はあくまで「修正が効いていること」の目安(緩め).
Assert.That(fixedGrowth, Is.LessThan(leakyGrowth),
"修正後の方がメモリ増分が小さいこと(修正が効いている)");
}
}
class VariantResult {
public long BasePriv, PeakPriv, FinalPriv, PeakWs;
public int Iterations;
public bool Aborted;
}
/// <summary>
/// 1 つの ConvertImage バリアントを frames 回繰り返し,メモリ推移を計測する.
/// src は in-place 書き換え(CV_64FC3)副作用があるため毎回新規生成して using で破棄する.
/// 戻り値(convImg8)は呼び出し側相当で破棄する(中間 Mat のリークだけを切り出すため).
/// </summary>
VariantResult RunVariant(string label, Func<Mat, Mat> convert, int width, int height, int frames, long ceiling) {
MemBenchSupport.ReclaimNative();
var (basePriv, baseWs) = MemBenchSupport.Sample();
var r = new VariantResult { BasePriv = basePriv, PeakPriv = basePriv, PeakWs = baseWs };
TestContext.WriteLine($"--- {label} ---");
TestContext.WriteLine($"baseline: Private {MemBenchSupport.GB(basePriv):F2} GB / WS {MemBenchSupport.GB(baseWs):F2} GB");
int i = 0;
for (; i < frames; i++) {
using (var src = new Mat(height, width, MatType.CV_8UC3, new Scalar(50, 100, 150)))
using (var dst = convert(src)) {
// dst は using で破棄(呼び出し側 SaveImages の using 相当)
}
var (priv, ws) = MemBenchSupport.Sample();
if (priv > r.PeakPriv) r.PeakPriv = priv;
if (ws > r.PeakWs) r.PeakWs = ws;
if ((i + 1) % 5 == 0 || i == 0) {
TestContext.WriteLine($" iter {i + 1,3}: Private {MemBenchSupport.GB(priv):F2} GB / WS {MemBenchSupport.GB(ws):F2} GB");
}
if (priv >= ceiling) {
TestContext.WriteLine($" ★ 上限 {MemBenchSupport.GB(ceiling):F1} GB に到達.iter {i + 1} で打ち切り(リーク蓄積の限界).");
r.Aborted = true;
i++;
break;
}
}
var (finalPriv, _) = MemBenchSupport.Sample();
r.FinalPriv = finalPriv;
r.Iterations = i;
TestContext.WriteLine($"結果: iter {i} / peak Private {MemBenchSupport.GB(r.PeakPriv):F2} GB / peak WS {MemBenchSupport.GB(r.PeakWs):F2} GB / 増分 {MemBenchSupport.GB(r.PeakPriv - r.BasePriv):F2} GB");
TestContext.WriteLine("");
return r;
}
}
}