20f6b0b969
Standalone console harness under tests/ZB.MOM.WW.ScadaBridge.LoadHarness plus a scaled-down Category=Performance smoke [Fact] in PerformanceTests. Deliberately an Exe rather than an xunit suite: the Performance trait enables a filter but does not exclude by default, so a 20-minute test would run on every 'dotnet test' of the slnx. What is real: per-site ActorSystem + LocalDb SQLite file, the real DCL (DataConnectionManagerActor/DataConnectionActor over a SimulatedDataConnection registered through the documented DataConnectionFactory.RegisterAdapter seam), real InstanceActors fed real TagValueUpdates, the real SiteStreamManager, real StreamRelayActor + production-capacity bounded DropOldest channel, real StoreAndForwardService/Storage, real SiteHealthCollector + CentralHealthAggregator. Only the socket hops are stood in for. Measures: end-to-end tag update latency (the emit instant rides TagValueUpdate.Timestamp verbatim to the subscriber), instance ramp, memory growth/CPU over a steady-state window, health report and debug view latency under load, S&F concurrent buffering + drain throughput, and slow-subscriber isolation.
111 lines
6.9 KiB
C#
111 lines
6.9 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.LoadHarness;
|
|
|
|
/// <summary>
|
|
/// Renders a <see cref="HarnessRunResult"/> as the markdown table that goes into the
|
|
/// results document, so the published numbers and the JSON come from one source.
|
|
/// </summary>
|
|
public static class ResultsFormatter
|
|
{
|
|
/// <summary>Formats a run result for console output and the results doc.</summary>
|
|
/// <param name="result">The run to format.</param>
|
|
/// <returns>A markdown-ish plain-text report.</returns>
|
|
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");
|
|
}
|
|
}
|