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