|
- using System.Diagnostics;
-
- namespace EnvelopeRenderer.Cli.Progress;
-
- /// <summary>
- /// Writes PROGRESS lines to the given <see cref="TextWriter"/> (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 <see cref="Stopwatch"/> directly) so tests can drive throttling
- /// deterministically without real `Thread.Sleep` calls; <see cref="CreateDefault"/> wires up a
- /// real stopwatch for production use.
- /// </summary>
- public sealed class ConsoleProgressReporter : IProgressReporter
- {
- private readonly TextWriter _output;
- private readonly Func<long> _elapsedMillisecondsProvider;
- private readonly long _minIntervalMs;
- private long? _lastEmittedAtMs;
-
- public ConsoleProgressReporter(
- TextWriter output, Func<long> elapsedMillisecondsProvider, long minIntervalMs = 1000)
- {
- _output = output;
- _elapsedMillisecondsProvider = elapsedMillisecondsProvider;
- _minIntervalMs = minIntervalMs;
- }
-
- /// <summary>Production factory: throttles against a real, freshly-started stopwatch.</summary>
- 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));
- }
- }
|