namespace ZB.MOM.WW.ScadaBridge.LoadHarness;
///
/// Scale and duration knobs for a load-harness run. Defaults are the Phase-8 WP-4
/// target scale (10 sites x 500 instances x 75 tags = 375,000 subscriptions).
/// Every value is overridable from the command line so the same binary serves both
/// the full-scale protocol run and the scaled-down CI smoke.
///
public sealed record HarnessConfig
{
/// Number of simulated sites (WP-4 acceptance criterion [2.5-1]).
public int Sites { get; init; } = 10;
/// Instance Actors per site (WP-4 [2.5-2]).
public int InstancesPerSite { get; init; } = 500;
/// Data-sourced attributes ("live tags") per instance (WP-4 [2.5-3]).
public int TagsPerInstance { get; init; } = 75;
///
/// Nominal per-tag update period. The driver emits each tag once per period, so
/// the site-wide event rate is InstancesPerSite * TagsPerInstance / period.
/// The 10 s default puts the target-scale fleet at 37,500 tag updates/second.
///
public TimeSpan TagUpdatePeriod { get; init; } = TimeSpan.FromSeconds(10);
///
/// Length of the steady-state measurement window, measured AFTER the ramp
/// completes and after the post-ramp settle. Memory-growth and CPU figures are
/// computed over exactly this window.
///
public TimeSpan SustainDuration { get; init; } = TimeSpan.FromMinutes(20);
///
/// Quiet period between the last instance starting and the start of the
/// measurement window — lets startup allocations settle so the memory-growth
/// slope reflects steady state, not the ramp.
///
public TimeSpan SettleDuration { get; init; } = TimeSpan.FromMinutes(2);
/// Resource-sampling cadence (working set, GC heap, CPU, thread count).
public TimeSpan SampleInterval { get; init; } = TimeSpan.FromSeconds(10);
///
/// Health report cadence. Defaults to the production
/// HealthMonitoringOptions.ReportInterval (30 s) so the timing measured is
/// the one that ships; only the CI smoke shortens it, because a 20-second smoke
/// window would otherwise never see a single tick.
///
public TimeSpan HealthReportInterval { get; init; } = TimeSpan.FromSeconds(30);
/// How often to take a debug view snapshot of a random live instance.
public TimeSpan DebugProbeInterval { get; init; } = TimeSpan.FromSeconds(5);
///
/// Pause between the last Instance Actor starting and the tag driver starting, to
/// let every instance complete its DCL subscribe round-trip. The adapter callback
/// is captured at subscribe time, so an emit before that lands is silently
/// discarded — this window is what keeps SkippedNoCallback at zero.
///
public TimeSpan SubscribeSettleDuration { get; init; } = TimeSpan.FromSeconds(30);
///
/// Instances per site that carry a live stream subscriber (a real
/// StreamRelayActor + bounded DropOldest channel, i.e. the production
/// Debug View / central shape). Every subscriber's stream graph sees the FULL
/// site event flow and filters it, so this is the fan-out multiplier.
///
public int StreamProbesPerSite { get; init; } = 10;
/// Store-and-forward messages enqueued for the drain-rate measurement (register row 50).
public int StoreAndForwardDrainMessages { get; init; } = 20_000;
/// Events published at the slow-subscriber isolation probe (register row 50).
public int SlowSubscriberEvents { get; init; } = 200_000;
/// Directory for the site SQLite files. A temp directory is used when null.
public string? DataDirectory { get; init; }
/// Path the JSON metrics document is written to.
public string ResultsPath { get; init; } = "loadharness-results.json";
/// Total live tag subscriptions across the fleet.
public int TotalSubscriptions => Sites * InstancesPerSite * TagsPerInstance;
/// Nominal fleet-wide tag updates per second implied by the scale and update period.
public double NominalUpdatesPerSecond => TotalSubscriptions / TagUpdatePeriod.TotalSeconds;
///
/// Parses --key value / --key=value arguments over the defaults.
/// Unknown keys throw so a typo in a 20-minute run's command line fails fast
/// rather than silently measuring the wrong scale.
///
/// Raw command-line arguments.
/// The parsed configuration.
public static HarnessConfig Parse(string[] args)
{
var config = new HarnessConfig();
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
if (!arg.StartsWith("--", StringComparison.Ordinal))
throw new ArgumentException($"Unexpected argument '{arg}' (expected --key value).");
string key;
string value;
var eq = arg.IndexOf('=', StringComparison.Ordinal);
if (eq >= 0)
{
key = arg[2..eq];
value = arg[(eq + 1)..];
}
else
{
key = arg[2..];
if (i + 1 >= args.Length)
throw new ArgumentException($"Option '--{key}' requires a value.");
value = args[++i];
}
config = key switch
{
"sites" => config with { Sites = int.Parse(value) },
"instances-per-site" => config with { InstancesPerSite = int.Parse(value) },
"tags-per-instance" => config with { TagsPerInstance = int.Parse(value) },
"tag-update-period-seconds" => config with { TagUpdatePeriod = TimeSpan.FromSeconds(double.Parse(value)) },
"sustain-minutes" => config with { SustainDuration = TimeSpan.FromMinutes(double.Parse(value)) },
"settle-minutes" => config with { SettleDuration = TimeSpan.FromMinutes(double.Parse(value)) },
"subscribe-settle-seconds" => config with { SubscribeSettleDuration = TimeSpan.FromSeconds(double.Parse(value)) },
"sample-seconds" => config with { SampleInterval = TimeSpan.FromSeconds(double.Parse(value)) },
"health-interval-seconds" => config with { HealthReportInterval = TimeSpan.FromSeconds(double.Parse(value)) },
"debug-probe-interval-seconds" => config with { DebugProbeInterval = TimeSpan.FromSeconds(double.Parse(value)) },
"stream-probes-per-site" => config with { StreamProbesPerSite = int.Parse(value) },
"sf-drain-messages" => config with { StoreAndForwardDrainMessages = int.Parse(value) },
"slow-subscriber-events" => config with { SlowSubscriberEvents = int.Parse(value) },
"data-dir" => config with { DataDirectory = value },
"results" => config with { ResultsPath = value },
_ => throw new ArgumentException($"Unknown option '--{key}'."),
};
}
return config;
}
}