using System.Globalization; using System.Text; namespace ZB.MOM.WW.ScadaBridge.LoadHarness; /// /// Renders a as the markdown table that goes into the /// results document, so the published numbers and the JSON come from one source. /// public static class ResultsFormatter { /// Formats a run result for console output and the results doc. /// The run to format. /// A markdown-ish plain-text report. public static string Format(HarnessRunResult result) { var culture = CultureInfo.InvariantCulture; var builder = new StringBuilder(); builder.AppendLine("=== MEASURED ==="); builder.AppendLine(culture, $"host {result.Environment.MachineName} / {result.Environment.OsDescription}"); builder.AppendLine(culture, $"cpus / runtime / gc {result.Environment.ProcessorCount} / {result.Environment.RuntimeVersion} / serverGC={result.Environment.ServerGc}"); builder.AppendLine(culture, $"scale {result.Config.Sites} sites x {result.Config.InstancesPerSite} instances x {result.Config.TagsPerInstance} tags = {result.Config.TotalSubscriptions:N0} subscriptions"); builder.AppendLine(culture, $"total run {result.TotalSeconds:F1}s"); builder.AppendLine(); builder.AppendLine("-- deployment / ramp --"); builder.AppendLine(culture, $"site fixtures built {result.SiteRampSeconds:F1}s"); builder.AppendLine(culture, $"all instance actors {result.InstanceRampSeconds:F1}s"); builder.AppendLine(culture, $"slowest single site {result.SlowestSiteInstanceRampSeconds:F1}s ({result.Config.InstancesPerSite} instances)"); builder.AppendLine(); builder.AppendLine("-- offered load (steady-state window) --"); builder.AppendLine(culture, $"nominal {result.NominalUpdatesPerSecond:N0} updates/s"); builder.AppendLine(culture, $"achieved {result.AchievedUpdatesPerSecond:N0} updates/s ({result.AchievedUpdatesPerSecond / result.NominalUpdatesPerSecond:P1} of nominal)"); builder.AppendLine(culture, $"updates offered {result.SteadyStateEmittedTagUpdates:N0}"); builder.AppendLine(culture, $"driver slice overrun {result.DriverLagSeconds:F1}s cumulative"); builder.AppendLine(culture, $"skipped (no callback) {result.DriverSkippedNoCallback:N0}"); builder.AppendLine(); builder.AppendLine("-- tag update latency (DCL boundary -> stream subscriber) --"); AppendLatency(builder, culture, result.TagUpdateLatency); builder.AppendLine(); builder.AppendLine("-- live stream subscribers (steady-state window) --"); builder.AppendLine(culture, $"events delivered {result.StreamProbeReceived:N0}"); builder.AppendLine(culture, $"events dropped {result.StreamProbeDropped:N0}"); builder.AppendLine(); builder.AppendLine("-- health report delivery --"); AppendLatency(builder, culture, result.HealthReportLatency); builder.AppendLine(culture, $"reports ingested {result.HealthReportsDelivered:N0}"); builder.AppendLine(culture, $"sites tracked centrally {result.SitesTrackedByAggregator}"); builder.AppendLine(); builder.AppendLine("-- debug view snapshot (under load) --"); AppendLatency(builder, culture, result.DebugSnapshotLatency); builder.AppendLine(culture, $"completed / timed out {result.DebugSnapshotsCompleted:N0} / {result.DebugSnapshotTimeouts:N0}"); builder.AppendLine(); if (result.SteadyStateResources is { } steady) { builder.AppendLine("-- resources (steady-state window) --"); builder.AppendLine(culture, $"window {steady.DurationSeconds:F0}s over {steady.SampleCount} samples"); builder.AppendLine(culture, $"working set {steady.WorkingSetStartMb:F0} -> {steady.WorkingSetEndMb:F0} MB (peak {steady.WorkingSetPeakMb:F0} MB)"); builder.AppendLine(culture, $"working set slope {steady.WorkingSetSlopeMbPerMinute:F2} MB/min"); builder.AppendLine(culture, $"managed heap {steady.ManagedHeapStartMb:F0} -> {steady.ManagedHeapEndMb:F0} MB (peak {steady.ManagedHeapPeakMb:F0} MB)"); builder.AppendLine(culture, $"managed heap slope {steady.ManagedHeapSlopeMbPerMinute:F2} MB/min"); builder.AppendLine(culture, $"cpu mean / peak {steady.MeanCpuPercentOfOneCore:F0}% / {steady.PeakCpuPercentOfOneCore:F0}% of one core ({steady.MeanCpuPercentOfOneCore / result.Environment.ProcessorCount:F1}% of the box)"); builder.AppendLine(culture, $"threads / gen2 GCs {steady.MeanThreadCount:F0} / {steady.Gen2Collections}"); builder.AppendLine(); } if (result.StoreAndForwardDrain is { } drain) { builder.AppendLine("-- store-and-forward (register row 50) --"); builder.AppendLine(culture, $"messages {drain.MessageCount:N0}"); builder.AppendLine(culture, $"concurrent buffering {drain.EnqueuePerSecond:N0} msg/s ({drain.EnqueueSeconds:F1}s)"); builder.AppendLine(culture, $"retry wait before drain {drain.TimeToFirstDeliverySeconds:F1}s (DefaultRetryInterval)"); builder.AppendLine(culture, $"drain throughput {drain.DrainPerSecond:N0} msg/s (active drain {drain.DrainSeconds - drain.TimeToFirstDeliverySeconds:F2}s)"); builder.AppendLine(culture, $"residual depth {drain.ResidualDepth}"); builder.AppendLine(); } if (result.SlowSubscriber is { } slow) { builder.AppendLine("-- slow-subscriber isolation (register row 50) --"); builder.AppendLine(culture, $"published {slow.PublishedEvents:N0} at {slow.PublishPerSecond:N0}/s"); builder.AppendLine(culture, $"healthy min delivery {slow.HealthyMinDeliveryRatio:P2}"); builder.AppendLine(culture, $"stalled delivery {slow.SlowDeliveryRatio:P2}"); foreach (var outcome in slow.Outcomes) { builder.AppendLine(culture, $" {outcome.Name,-28} {(outcome.IsSlow ? "STALLED" : "healthy"),-8} " + $"recv {outcome.Received,8:N0} dropped {outcome.Dropped,8:N0} ratio {outcome.DeliveryRatio:P2}"); } builder.AppendLine(); } return builder.ToString(); } private static void AppendLatency(StringBuilder builder, CultureInfo culture, Metrics.LatencySnapshot snapshot) { builder.AppendLine(culture, $"samples {snapshot.Count:N0} mean {snapshot.MeanMs:F2}ms p50 {snapshot.P50Ms:F2}ms " + $"p95 {snapshot.P95Ms:F2}ms p99 {snapshot.P99Ms:F2}ms p99.9 {snapshot.P999Ms:F2}ms max {snapshot.MaxMs:F2}ms"); } }