Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATSumCommandPerfTests.cs
2026-04-25 21:43:32 -04:00

94 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// PR 2.1 perf gate — verifies the bulk Sum-read path is materially faster than the
/// equivalent serial per-tag <see cref="TwinCAT.Ads.AdsClient.ReadValueAsync(string, Type, System.Threading.CancellationToken)"/>
/// loop on a real XAR runtime. The driver's <see cref="TwinCATDriver.ReadAsync"/>
/// 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.
/// </summary>
/// <remarks>
/// Requires the perf fixture: <c>GVL_Perf.aTags : ARRAY[1..1000] OF DINT</c> per
/// <c>TwinCatProject/README.md §Performance scenarios</c>. Skipped unless
/// <c>TWINCAT_PERF=1</c> is set.
/// </remarks>
[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})");
}
}