using System.Diagnostics; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests; /// /// PR 2.1 perf gate — verifies the bulk Sum-read path is materially faster than the /// equivalent serial per-tag /// loop on a real XAR runtime. The driver's /// already routes through the bulk path; the baseline calls through a hand-rolled /// loop so we're comparing apples to apples on the same wire. /// /// /// Requires the perf fixture: GVL_Perf.aTags : ARRAY[1..1000] OF DINT per /// TwinCatProject/README.md §Performance scenarios. Skipped unless /// TWINCAT_PERF=1 is set. /// [Collection("TwinCATXar")] [Trait("Category", "Performance")] [Trait("Simulator", "TwinCAT-XAR")] public sealed class TwinCATSumCommandPerfTests(TwinCATXarFixture sim) { private const int TagCount = 1000; [TwinCATPerfFact] public async Task Driver_sum_read_1000_tags_beats_loop_baseline_by_5x() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); var deviceAddress = $"ads://{sim.TargetNetId}:{sim.AmsPort}"; var tags = new TwinCATTagDefinition[TagCount]; var refs = new string[TagCount]; for (var i = 0; i < TagCount; i++) { // GVL_Perf.aTags is 1-based per IEC 61131-3 ARRAY declaration. var name = $"Perf{i + 1}"; refs[i] = name; tags[i] = new TwinCATTagDefinition( Name: name, DeviceHostAddress: deviceAddress, SymbolPath: $"GVL_Perf.aTags[{i + 1}]", DataType: TwinCATDataType.DInt); } var options = new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions(deviceAddress, "XAR-VM")], Tags = tags, UseNativeNotifications = false, Timeout = TimeSpan.FromSeconds(15), Probe = new TwinCATProbeOptions { Enabled = false }, }; // ---- Bulk (PR 2.1) measurement ---- await using var bulkDrv = new TwinCATDriver(options, "tc3-perf-bulk"); await bulkDrv.InitializeAsync("{}", TestContext.Current.CancellationToken); // Warm-up: prime symbol-handle caches in the XAR runtime so the timed run // measures sum-read steady state, not first-touch handle resolution. await bulkDrv.ReadAsync(refs, TestContext.Current.CancellationToken); var bulkSw = Stopwatch.StartNew(); var bulkResults = await bulkDrv.ReadAsync(refs, TestContext.Current.CancellationToken); bulkSw.Stop(); // ---- Loop baseline measurement ---- // Use a fresh driver instance so the cache state matches the bulk run on first // call. Single-tag-per-call across the full set is what PR 2.1 replaces. await using var loopDrv = new TwinCATDriver(options, "tc3-perf-loop"); await loopDrv.InitializeAsync("{}", TestContext.Current.CancellationToken); // Warm-up against the same refs. for (var i = 0; i < TagCount; i++) _ = await loopDrv.ReadAsync([refs[i]], TestContext.Current.CancellationToken); var loopSw = Stopwatch.StartNew(); for (var i = 0; i < TagCount; i++) _ = await loopDrv.ReadAsync([refs[i]], TestContext.Current.CancellationToken); loopSw.Stop(); // Sanity: bulk path must produce TagCount snapshots all Good. bulkResults.Count.ShouldBe(TagCount); bulkResults.ShouldAllBe(s => s.StatusCode == TwinCATStatusMapper.Good); // Conservative ratio — on the dev box bulk is ~10-20× the loop, target is 5×. // Lower bound exists so the test is robust to noisy CI / VM scheduling. var ratio = (double)loopSw.ElapsedMilliseconds / Math.Max(1, bulkSw.ElapsedMilliseconds); ratio.ShouldBeGreaterThan(5.0, $"Bulk Sum-read should be >5x faster than per-tag loop; got bulk={bulkSw.ElapsedMilliseconds}ms, loop={loopSw.ElapsedMilliseconds}ms (ratio={ratio:F2})"); } }