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; } }