Files
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

204 lines
7.9 KiB
C#

using System.Diagnostics;
namespace ZB.MOM.WW.ScadaBridge.LoadHarness.Metrics;
/// <summary>
/// One periodic observation of process resource usage.
/// </summary>
/// <param name="ElapsedSeconds">Seconds since sampling started.</param>
/// <param name="WorkingSetMb">Process working set (RSS).</param>
/// <param name="ManagedHeapMb"><see cref="GC.GetTotalMemory(bool)"/> without forcing a collection.</param>
/// <param name="CpuPercent">Mean CPU utilization since the previous sample, as a percentage of ONE core.</param>
/// <param name="ThreadCount">OS threads in the process.</param>
/// <param name="Gen2Collections">Cumulative generation 2 collections.</param>
public sealed record ResourceSample(
double ElapsedSeconds,
double WorkingSetMb,
double ManagedHeapMb,
double CpuPercent,
int ThreadCount,
int Gen2Collections);
/// <summary>
/// Samples working set, managed heap, CPU and thread count on a fixed cadence for the
/// life of a run. CPU is differential (processor time delta / wall delta) so a sample
/// reflects the interval it covers rather than the whole process lifetime.
/// </summary>
public sealed class ResourceSampler : IAsyncDisposable
{
private readonly List<ResourceSample> _samples = new();
private readonly object _lock = new();
private readonly CancellationTokenSource _cts = new();
private readonly Task _loop;
private readonly Stopwatch _wall = Stopwatch.StartNew();
private TimeSpan _lastCpu;
private double _lastElapsedSeconds;
private ResourceSampler(TimeSpan interval)
{
_lastCpu = Process.GetCurrentProcess().TotalProcessorTime;
_loop = Task.Run(() => SampleLoopAsync(interval, _cts.Token));
}
/// <summary>Starts sampling at the given cadence.</summary>
/// <param name="interval">Sampling interval.</param>
/// <returns>The running sampler.</returns>
public static ResourceSampler Start(TimeSpan interval) => new(interval);
/// <summary>All samples collected so far, oldest first.</summary>
/// <returns>A snapshot copy of the sample list.</returns>
public IReadOnlyList<ResourceSample> Snapshot()
{
lock (_lock) return _samples.ToList();
}
private async Task SampleLoopAsync(TimeSpan interval, CancellationToken cancellationToken)
{
using var timer = new PeriodicTimer(interval);
try
{
while (await timer.WaitForNextTickAsync(cancellationToken))
Capture();
}
catch (OperationCanceledException)
{
// Normal teardown.
}
}
private void Capture()
{
using var process = Process.GetCurrentProcess();
process.Refresh();
var elapsedSeconds = _wall.Elapsed.TotalSeconds;
var cpu = process.TotalProcessorTime;
var wallDelta = elapsedSeconds - _lastElapsedSeconds;
var cpuPercent = wallDelta > 0
? (cpu - _lastCpu).TotalSeconds / wallDelta * 100.0
: 0.0;
_lastCpu = cpu;
_lastElapsedSeconds = elapsedSeconds;
var sample = new ResourceSample(
elapsedSeconds,
process.WorkingSet64 / 1024.0 / 1024.0,
GC.GetTotalMemory(forceFullCollection: false) / 1024.0 / 1024.0,
cpuPercent,
process.Threads.Count,
GC.CollectionCount(2));
lock (_lock) _samples.Add(sample);
}
/// <summary>
/// Summarizes the samples falling inside a window, expressed as seconds since
/// sampling started. Memory growth is reported both as an absolute delta and as a
/// least-squares slope, because a run that sawtooths around a stable mean and a
/// run that climbs monotonically can share the same endpoint delta.
/// </summary>
/// <param name="fromSeconds">Window start (inclusive), seconds since start.</param>
/// <param name="toSeconds">Window end (inclusive), seconds since start.</param>
/// <returns>The window summary, or null when fewer than two samples fall inside it.</returns>
public ResourceWindowSummary? Summarize(double fromSeconds, double toSeconds)
{
var window = Snapshot()
.Where(s => s.ElapsedSeconds >= fromSeconds && s.ElapsedSeconds <= toSeconds)
.ToList();
if (window.Count < 2)
return null;
var first = window[0];
var last = window[^1];
return new ResourceWindowSummary(
SampleCount: window.Count,
DurationSeconds: last.ElapsedSeconds - first.ElapsedSeconds,
WorkingSetStartMb: first.WorkingSetMb,
WorkingSetEndMb: last.WorkingSetMb,
WorkingSetPeakMb: window.Max(s => s.WorkingSetMb),
WorkingSetSlopeMbPerMinute: Slope(window, s => s.WorkingSetMb) * 60.0,
ManagedHeapStartMb: first.ManagedHeapMb,
ManagedHeapEndMb: last.ManagedHeapMb,
ManagedHeapPeakMb: window.Max(s => s.ManagedHeapMb),
ManagedHeapSlopeMbPerMinute: Slope(window, s => s.ManagedHeapMb) * 60.0,
MeanCpuPercentOfOneCore: window.Average(s => s.CpuPercent),
PeakCpuPercentOfOneCore: window.Max(s => s.CpuPercent),
MeanThreadCount: window.Average(s => s.ThreadCount),
Gen2Collections: last.Gen2Collections - first.Gen2Collections);
}
private static double Slope(IReadOnlyList<ResourceSample> samples, Func<ResourceSample, double> selector)
{
var n = samples.Count;
var meanX = samples.Average(s => s.ElapsedSeconds);
var meanY = samples.Average(selector);
double numerator = 0, denominator = 0;
for (var i = 0; i < n; i++)
{
var dx = samples[i].ElapsedSeconds - meanX;
numerator += dx * (selector(samples[i]) - meanY);
denominator += dx * dx;
}
return denominator == 0 ? 0 : numerator / denominator;
}
private int _disposed;
/// <summary>Stops the sampling loop and releases the sampler's resources.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
public async ValueTask DisposeAsync()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
await _cts.CancelAsync();
try
{
await _loop;
}
catch (OperationCanceledException)
{
// Expected.
}
_cts.Dispose();
}
}
/// <summary>Aggregate resource behaviour over a measurement window.</summary>
/// <param name="SampleCount">Samples in the window.</param>
/// <param name="DurationSeconds">Window length.</param>
/// <param name="WorkingSetStartMb">Working set at window start.</param>
/// <param name="WorkingSetEndMb">Working set at window end.</param>
/// <param name="WorkingSetPeakMb">Peak working set in the window.</param>
/// <param name="WorkingSetSlopeMbPerMinute">Least-squares working-set growth rate.</param>
/// <param name="ManagedHeapStartMb">Managed heap at window start.</param>
/// <param name="ManagedHeapEndMb">Managed heap at window end.</param>
/// <param name="ManagedHeapPeakMb">Peak managed heap in the window.</param>
/// <param name="ManagedHeapSlopeMbPerMinute">Least-squares managed-heap growth rate.</param>
/// <param name="MeanCpuPercentOfOneCore">Mean CPU as a percentage of one core (1400% = 14 cores saturated).</param>
/// <param name="PeakCpuPercentOfOneCore">Peak single-sample CPU as a percentage of one core.</param>
/// <param name="MeanThreadCount">Mean OS thread count.</param>
/// <param name="Gen2Collections">Generation 2 collections during the window.</param>
public sealed record ResourceWindowSummary(
int SampleCount,
double DurationSeconds,
double WorkingSetStartMb,
double WorkingSetEndMb,
double WorkingSetPeakMb,
double WorkingSetSlopeMbPerMinute,
double ManagedHeapStartMb,
double ManagedHeapEndMb,
double ManagedHeapPeakMb,
double ManagedHeapSlopeMbPerMinute,
double MeanCpuPercentOfOneCore,
double PeakCpuPercentOfOneCore,
double MeanThreadCount,
int Gen2Collections);