#if MEMMONITOR
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
namespace TIASshot {
/// <summary>
/// メモリ観測ウィンドウ(MEMMONITOR 限定)。
/// TIASshot 本体プロセスのメモリ使用量をライブ折れ線グラフで表示する。
/// 研究室デモ用:実使用時にメモリが増え続けないことを画面録画で示す。
/// </summary>
public partial class MemoryMonitorForm : Form {
// ---- GDI/USER ハンドル数取得(GDI ハンドル枯渇の疑いを確認するため)------
[DllImport("user32.dll")]
private static extern uint GetGuiResources(IntPtr hProcess, uint uiFlags);
private const uint GR_GDIOBJECTS = 0;
private const uint GR_USEROBJECTS = 1;
// ---- サンプリング記録 -------------------------------------------------
private struct MemSample {
public double ElapsedSec;
public double PrivateMB;
public double WorkingSetMB;
// プレビュー計測(コピー負荷 vs UI 描画負荷の切り分け用。PreviewProfiler.Snapshot() の区間値)
public double CallbackFps; // ShowImage 実効フレームレート(コールバックスレッド律速の指標)
public double MainPaintFps; // MainRefreshTimer_Tick の実スワップレート
public double SubUpdateFps; // PreviewMonitor.UpdateImage 呼び出しレート
public double SubPaintFps; // PreviewMonitor_Paint の実スワップレート(副モニタ描画側の指標)
public double SubDropFps; // 副モニタ側の取りこぼしレート
public double SubPaintMs; // picPreview の実描画(base.OnPaint)平均所要時間
public double MainCopyMs; // 本モニタ用コピー(dispCopy)の平均所要時間
public double SubCopyMs; // 副モニタ用コピーの平均所要時間
public uint GdiObjects; // プロセスの GDI オブジェクト数(GetGuiResources GR_GDIOBJECTS)
public uint UserObjects; // プロセスの USER オブジェクト数(GetGuiResources GR_USEROBJECTS)
public int HandleCount; // プロセスのハンドル数(Process.HandleCount)
public int ImgW; // 直近に表示したフレームの幅
public int ImgH; // 直近に表示したフレームの高さ
public bool SubShown; // 区間内に副モニタが表示されていたか
public string Event; // "SUB_ON"/"SUB_OFF"/"CALIB_START"/"CALIB_END"/"SHOT_START"/"SHOT_END" を ";" 連結(無ければ "")
}
private readonly List<MemSample> _samples = new List<MemSample>();
private readonly System.Windows.Forms.Timer _timer;
private readonly Stopwatch _stopwatch = new Stopwatch();
private bool _isRunning;
private double _lastTickElapsedSec = 0.0;
// CSV 出力先
private readonly string _outputDir;
// ---- コンストラクタ ---------------------------------------------------
public MemoryMonitorForm() {
InitializeComponent();
// F9 で副モニタ計測トグル(Form1.ToggleSubMonitorProfiling)を,
// このウィンドウにフォーカスがある場合にも呼び出せるようにする。
// このフォームは ShowMemoryMonitor() で Show(this) されるため Owner は Form1。
this.KeyPreview = true;
this.KeyDown += MemoryMonitorForm_KeyDown;
this.Text = "メモリ観測ウィンドウ [MEMMONITOR]";
_outputDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"MemMonitor_Results");
Directory.CreateDirectory(_outputDir);
_timer = new System.Windows.Forms.Timer();
_timer.Interval = 500;
_timer.Tick += Timer_Tick;
this.FormClosing += (s, e) => {
_timer.Stop();
};
// ウィンドウを開いたら自動でサンプリング開始
this.Shown += (s, e) => StartSampling();
}
// ---- キー操作 --------------------------------------------------------
private void MemoryMonitorForm_KeyDown(object sender, KeyEventArgs e) {
if (e.KeyCode == Keys.F9) {
(this.Owner as Form1)?.ToggleSubMonitorProfiling();
e.Handled = true;
}
}
// ---- サンプリング制御 ------------------------------------------------
private void StartSampling() {
if (_isRunning) return;
_isRunning = true;
_stopwatch.Start();
_timer.Start();
btnStartStop.Text = "停止";
}
private void StopSampling() {
if (!_isRunning) return;
_isRunning = false;
_stopwatch.Stop();
_timer.Stop();
btnStartStop.Text = "開始";
}
// ---- タイマー Tick(サンプリング本体)--------------------------------
private void Timer_Tick(object sender, EventArgs e) {
// Process.Refresh() で最新値を取得(GC 強制なし)
using (var proc = Process.GetCurrentProcess()) {
proc.Refresh();
double elapsedSec = _stopwatch.Elapsed.TotalSeconds;
double privateMB = proc.PrivateMemorySize64 / (1024.0 * 1024.0);
double wsMB = proc.WorkingSet64 / (1024.0 * 1024.0);
// GDI/USER ハンドル数・プロセスハンドル数(GDI ハンドル枯渇による描画劣化の疑いを確認する)
uint gdiObjects = GetGuiResources(proc.Handle, GR_GDIOBJECTS);
uint userObjects = GetGuiResources(proc.Handle, GR_USEROBJECTS);
int handleCount = proc.HandleCount;
// プレビュー計測区間の実経過秒(Timer.Interval のブレを吸収するため実測差分を使う)
double intervalSec = elapsedSec - _lastTickElapsedSec;
if (intervalSec <= 0) intervalSec = _timer.Interval / 1000.0;
_lastTickElapsedSec = elapsedSec;
// PreviewProfiler.Snapshot() は呼び出しごとにカウンタをゼロリセットして区間値を返す。
// ここで各段の実効 FPS とコピー時間・描画時間平均を算出し,
// - 副モニタ ON で callback_fps が大きく落ちる → コピー/コールバックスレッド律速
// - callback_fps は高いのに sub_paint_fps が sub_update_fps より大幅に低い → 副モニタの UI 描画律速
// - CALIB_END/SHOT_END 直後に sub_paint_ms が跳ね上がる → 描画コスト増(副モニタ描画律速)
// 跳ね上がらず sub_paint_fps だけ落ちる → メッセージ枯渇/別要因
// - gdi_objects/user_objects/handle_count が撮影のたびに増え続け sub_paint_ms 増加と相関
// → GDI ハンドル枯渇による描画劣化の疑い
// - img_w/img_h が校正前後で変化 → プレビュー画像サイズ肥大による拡大負荷増の疑い
// を切り分けるための材料とする。
var snap = PreviewProfiler.Snapshot();
double callbackFps = snap.CallbackFrames / intervalSec;
double mainPaintFps = snap.MainPaints / intervalSec;
double subUpdateFps = snap.SubUpdates / intervalSec;
double subPaintFps = snap.SubPaints / intervalSec;
double subDropFps = snap.SubDrops / intervalSec;
// 保留イベント(F9 トグル・校正・撮影)を消費する。
// イベントは 500ms 区間内の直近サンプルに現れる(分析上この粒度で十分)。
string eventLabel = PreviewProfiler.ConsumeEvents();
// 記録
_samples.Add(new MemSample {
ElapsedSec = elapsedSec,
PrivateMB = privateMB,
WorkingSetMB = wsMB,
CallbackFps = callbackFps,
MainPaintFps = mainPaintFps,
SubUpdateFps = subUpdateFps,
SubPaintFps = subPaintFps,
SubDropFps = subDropFps,
SubPaintMs = snap.SubPaintMs,
MainCopyMs = snap.MainCopyMs,
SubCopyMs = snap.SubCopyMs,
GdiObjects = gdiObjects,
UserObjects = userObjects,
HandleCount = handleCount,
ImgW = snap.ImgW,
ImgH = snap.ImgH,
SubShown = snap.SubShown,
Event = eventLabel,
});
// グラフ追加
double privateGB = privateMB / 1024.0;
double wsGB = wsMB / 1024.0;
// 左 Y 軸: メモリ主指標 Private(1 本)。右 Y 軸(副軸): 本/副モニタの実描画 FPS。
// WorkingSet はグラフに描かず CSV にのみ残す(主指標 1 本を録画で強調するため)。
chart.Series["Private"].Points.AddXY(elapsedSec, privateGB);
chart.Series["MainFps"].Points.AddXY(elapsedSec, mainPaintFps);
chart.Series["SubFps"].Points.AddXY(elapsedSec, subPaintFps);
// Y 軸オートスケール追従(左右両軸)
chart.ChartAreas["ChartArea1"].RecalculateAxesScale();
// ラベル更新
int n = (int)Math.Round(elapsedSec);
lblCurrent.Text =
$"現在: Private {privateGB:F2} GB / WS {wsGB:F2} GB(経過 {n}s)";
string eventSuffix = string.IsNullOrEmpty(eventLabel) ? "" : $" / event {eventLabel}";
lblPreview.Text =
$"CB {callbackFps:F1} / mainPaint {mainPaintFps:F1} / subUpd {subUpdateFps:F1} subPaint {subPaintFps:F1} subDrop {subDropFps:F1} / " +
$"copy main {snap.MainCopyMs:F1}ms sub {snap.SubCopyMs:F1}ms / subPaint {snap.SubPaintMs:F1}ms / " +
$"gdi {gdiObjects} user {userObjects} handle {handleCount} / img {snap.ImgW}x{snap.ImgH} / " +
$"sub {(snap.SubShown ? "ON" : "OFF")}{eventSuffix} (F9: 副モニタ計測トグル)";
}
}
// ---- ボタンイベント --------------------------------------------------
private void btnStartStop_Click(object sender, EventArgs e) {
if (_isRunning) {
StopSampling();
} else {
StartSampling();
}
}
private void btnReset_Click(object sender, EventArgs e) {
StopSampling();
_samples.Clear();
_stopwatch.Reset();
_lastTickElapsedSec = 0.0;
PreviewProfiler.Snapshot(); // 未読の累積値を捨てて次回計測をゼロから開始する
PreviewProfiler.ConsumeEvents(); // 未読のイベントも破棄する
chart.Series["Private"].Points.Clear();
chart.Series["MainFps"].Points.Clear();
chart.Series["SubFps"].Points.Clear();
lblCurrent.Text = "現在: --- (リセット)";
lblPreview.Text = "プレビュー計測: ---";
}
private void btnSaveCsv_Click(object sender, EventArgs e) {
if (_samples.Count == 0) {
MessageBox.Show("記録データがありません。", "CSV 保存",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string path = NextCsvPath();
var sb = new StringBuilder();
sb.AppendLine(
"elapsed_sec,private_MB,ws_MB,callback_fps,main_paint_fps,sub_update_fps,sub_paint_fps,sub_drop_fps," +
"sub_paint_ms,main_copy_ms,sub_copy_ms,gdi_objects,user_objects,handle_count,img_w,img_h,sub_shown,event");
foreach (var s in _samples) {
sb.AppendLine(
$"{s.ElapsedSec:F3},{s.PrivateMB:F2},{s.WorkingSetMB:F2}," +
$"{s.CallbackFps:F2},{s.MainPaintFps:F2},{s.SubUpdateFps:F2},{s.SubPaintFps:F2},{s.SubDropFps:F2}," +
$"{s.SubPaintMs:F3},{s.MainCopyMs:F3},{s.SubCopyMs:F3}," +
$"{s.GdiObjects},{s.UserObjects},{s.HandleCount},{s.ImgW},{s.ImgH}," +
$"{(s.SubShown ? 1 : 0)},{s.Event}");
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
MessageBox.Show($"保存しました:\n{path}", "CSV 保存",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void btnOpenFolder_Click(object sender, EventArgs e) {
Directory.CreateDirectory(_outputDir);
Process.Start("explorer.exe", _outputDir);
}
// ---- ユーティリティ --------------------------------------------------
private string NextCsvPath() {
const string prefix = "mem_monitor_";
int max = 0;
if (Directory.Exists(_outputDir)) {
foreach (var f in Directory.GetFiles(_outputDir, prefix + "*.csv")) {
string name = Path.GetFileNameWithoutExtension(f);
string numPart = name.Substring(prefix.Length);
if (int.TryParse(numPart, out int n) && n > max) max = n;
}
}
return Path.Combine(_outputDir, $"{prefix}{max + 1:D4}.csv");
}
}
}
#endif