using System.Diagnostics; namespace EnvelopeRenderer.Cli.Progress; /// /// Writes PROGRESS lines to the given (stdout in production). Only the /// high-frequency `render` event is throttled — to at least once per second, per the "Emit /// machine-readable progress during render" story — because `startup`, `complete`, and `failure` /// each happen exactly once per run and must never be dropped. The elapsed-time source is /// injected (rather than reading directly) so tests can drive throttling /// deterministically without real `Thread.Sleep` calls; wires up a /// real stopwatch for production use. /// public sealed class ConsoleProgressReporter : IProgressReporter { private readonly TextWriter _output; private readonly Func _elapsedMillisecondsProvider; private readonly long _minIntervalMs; private long? _lastEmittedAtMs; public ConsoleProgressReporter( TextWriter output, Func elapsedMillisecondsProvider, long minIntervalMs = 1000) { _output = output; _elapsedMillisecondsProvider = elapsedMillisecondsProvider; _minIntervalMs = minIntervalMs; } /// Production factory: throttles against a real, freshly-started stopwatch. public static ConsoleProgressReporter CreateDefault(TextWriter output) { var stopwatch = Stopwatch.StartNew(); return new ConsoleProgressReporter(output, () => stopwatch.ElapsedMilliseconds); } public void Startup() => Write("startup", 0); public void ReportRenderProgress(int completed) { var elapsed = _elapsedMillisecondsProvider(); // Always emit the first call immediately (so a slow run shows *something* right away), // then suppress further calls until at least _minIntervalMs has passed since the last // one actually written — this is the "at least once per second, not once per row" rule. if (_lastEmittedAtMs is not null && elapsed - _lastEmittedAtMs.Value < _minIntervalMs) { return; } _lastEmittedAtMs = elapsed; Write("render", completed); } public void Complete(int completed) => Write("complete", completed); public void Failure(int completed, string reason) => Write("failure", completed, reason); private void Write(string kind, int completed, string? reason = null) { _output.WriteLine(ProgressEventFormatter.Format(kind, _elapsedMillisecondsProvider(), completed, reason)); } }