using System.Diagnostics; using Akka.Actor; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; using ZB.MOM.WW.ScadaBridge.SiteRuntime; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming; namespace ZB.MOM.WW.ScadaBridge.PerformanceTests.Streaming; /// /// Throughput measurement for the site-wide attribute/alarm stream /// (: Source.ActorRefBroadcastHub /// with per-subscriber buffering, DropHead overflow). Design envelope: this /// stream carries every attribute value change on a site; it must comfortably /// sustain tens of thousands of events/sec. Thresholds are deliberately /// conservative (CI-machine safe) — this test exists to catch order-of-magnitude /// regressions, not to benchmark. /// /// /// /// This is the first of the two highest-value real measurements arch-review 08 §2.3 /// called for. It follows the existing HotPathLatencyTests style: /// -based, [Trait("Category", "Performance")], /// conservative thresholds so CI stays green on slow machines. /// /// /// It spins up a real (non-clustered) rather than using /// Akka.TestKit — the PerformanceTests project references SiteRuntime (which brings /// in Akka + Akka.Streams transitively) but not Akka.TestKit.Xunit2, so no csproj /// change is required. /// /// public class SiteStreamThroughputTests { private sealed class CountingActor : ReceiveActor { public static long Received; public CountingActor() { // The manager Tells the raw ISiteStreamEvent record (AttributeValueChanged) // straight to the subscriber — no wrapper — so we match on the record. Receive(_ => Interlocked.Increment(ref Received)); ReceiveAny(_ => { }); } } [Trait("Category", "Performance")] [Fact] public async Task SiteStream_SustainsAtLeast10kEventsPerSecond_ToOneSubscriber() { const int eventCount = 100_000; CountingActor.Received = 0; var system = ActorSystem.Create("SiteStreamThroughputTests"); try { // Buffer sized to the whole burst so DropHead should not fire. var manager = new SiteStreamManager( new SiteRuntimeOptions { StreamBufferSize = eventCount }, NullLogger.Instance); manager.Initialize(system); var counter = system.ActorOf(Props.Create()); manager.Subscribe("PerfInstance", counter); var sw = Stopwatch.StartNew(); for (var i = 0; i < eventCount; i++) { manager.PublishAttributeValueChanged(new AttributeValueChanged( "PerfInstance", "Attr", "Attr", i, "Good", DateTimeOffset.UtcNow)); } // Drain: wait until the counter goes quiet (no growth across a 200ms window). long last = -1; var deadline = DateTime.UtcNow.AddSeconds(30); while (DateTime.UtcNow < deadline) { var now = Interlocked.Read(ref CountingActor.Received); if (now == last && now > 0) break; last = now; await Task.Delay(200); } sw.Stop(); var received = Interlocked.Read(ref CountingActor.Received); var eventsPerSecond = received / sw.Elapsed.TotalSeconds; // DropHead means under extreme pressure some events may drop; with the // buffer sized to the burst, deliver ratio should be ~1.0. Assert.True(received >= eventCount * 0.9, $"Expected >=90% delivery, got {received}/{eventCount}"); Assert.True(eventsPerSecond >= 10_000, $"Expected >=10k events/s sustained, got {eventsPerSecond:F0}/s over {sw.Elapsed.TotalSeconds:F1}s " + $"(delivered {received}/{eventCount})"); } finally { await system.Terminate(); } } }