@@ -0,0 +1,93 @@
|
||||
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})");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user