test(opcua): prove saturation observer runtime event delivery
This commit is contained in:
@@ -29,7 +29,10 @@ A fresh task-owned control directory and UUID bind every request/receipt to this
|
||||
run and target PID. Files are published atomically; existing, malformed, foreign,
|
||||
or incomplete receipts fail closed. The observer is a separate diagnostic process
|
||||
using public `DiagnosticsClient` / EventPipe APIs. Product dependencies are
|
||||
unchanged.
|
||||
unchanged. A separate short positive control first invokes a named non-SDK method
|
||||
with NoInlining/NoOptimization after attaching and requires its JIT-start count in
|
||||
the parsed receipt. This proves CLR provider delivery/classification independently
|
||||
of custom marker delivery. That probe is never invoked by the canonical experiment.
|
||||
|
||||
One continuous trace covers both fixed windows, including their boundary gap:
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ try
|
||||
{
|
||||
protocol, runId, targetPid, observerPid = Environment.ProcessId, active = false, drained = true,
|
||||
summary.markersValid, summary.eventsLost, summary.sdkFactoryJit, summary.sdkFactoryLoads,
|
||||
summary.sdkAssemblyLoads, summary.windows, summary.otherJit, summary.loadedNativeBytes,
|
||||
summary.sdkAssemblyLoads, summary.windows, summary.otherJit, summary.loadedNativeBytes, summary.runtimeControlJit,
|
||||
configuration = new { runtimeProvider = "Microsoft-Windows-DotNETRuntime", level = "Verbose", keywords = "0x18",
|
||||
markerProvider, circularBufferMiB = 16, requestRundown = false },
|
||||
acceptance = false
|
||||
@@ -82,11 +82,17 @@ async Task Publish(string name, object value)
|
||||
}
|
||||
static TraceResult Parse(string path, string expectedRun)
|
||||
{
|
||||
var runtimeControlJit = 0;
|
||||
var markers = new List<Marker>();
|
||||
var events = new List<Observed>();
|
||||
using var source = new EventPipeEventSource(path);
|
||||
source.Clr.MethodJittingStarted += data => events.Add(new(data.TimeStampRelativeMSec,
|
||||
data.MethodNamespace == "Opc.Ua.OpcUaExtensions" ? "factory-jit" : "other-jit", 0));
|
||||
source.Clr.MethodJittingStarted += data =>
|
||||
{
|
||||
if (data.MethodNamespace == "ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.Sdk2LifecycleSoakTests"
|
||||
&& data.MethodName == "SaturationProtocolRuntimeProbe") runtimeControlJit++;
|
||||
events.Add(new(data.TimeStampRelativeMSec,
|
||||
data.MethodNamespace == "Opc.Ua.OpcUaExtensions" ? "factory-jit" : "other-jit", 0));
|
||||
};
|
||||
source.Clr.MethodLoadVerbose += data => events.Add(new(data.TimeStampRelativeMSec,
|
||||
data.MethodNamespace == "Opc.Ua.OpcUaExtensions" ? "factory-load" : "other-load", data.MethodSize));
|
||||
source.Clr.LoaderAssemblyLoad += data =>
|
||||
@@ -117,11 +123,11 @@ static TraceResult Parse(string path, string expectedRun)
|
||||
// Totals include the whole attached interval, including marker gaps, startup and stopping the collector.
|
||||
return new TraceResult(valid, source.EventsLost, events.Count(x => x.Kind == "factory-jit"),
|
||||
events.Count(x => x.Kind == "factory-load"), events.Count(x => x.Kind == "sdk-assembly"), windows,
|
||||
events.Count(x => x.Kind == "other-jit"), events.Sum(x => x.Bytes));
|
||||
events.Count(x => x.Kind == "other-jit"), events.Sum(x => x.Bytes), runtimeControlJit);
|
||||
}
|
||||
record Marker(double Ms, string Run, string Name, int Cycle, int Restarts);
|
||||
record Observed(double Ms, string Kind, long Bytes);
|
||||
record Window(string name, int firstCycle, int lastCycle, int startRestarts, int endRestarts,
|
||||
int sdkFactoryJit, int sdkFactoryLoads, int sdkAssemblyLoads);
|
||||
record TraceResult(bool markersValid, int eventsLost, int sdkFactoryJit, int sdkFactoryLoads,
|
||||
int sdkAssemblyLoads, Window[] windows, int otherJit, long loadedNativeBytes);
|
||||
int sdkAssemblyLoads, Window[] windows, int otherJit, long loadedNativeBytes, int runtimeControlJit);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare a fresh immutable candidate bundle. This command never launches the experiment."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
@@ -42,15 +43,18 @@ def main():
|
||||
shutil.copy2(source / name, tools / name)
|
||||
manifest = {str(path.relative_to(root)): sha(path) for directory in ["app", "observer", "tools"]
|
||||
for path in sorted((root / directory).rglob("*")) if path.is_file()}
|
||||
runtime = json.loads((root / "app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.runtimeconfig.json").read_text())
|
||||
included = runtime["runtimeOptions"]["includedFrameworks"]
|
||||
if not included or any(value["version"] != "10.0.14" for value in included):
|
||||
raise RuntimeError("Published app is not the prospectively fixed self-contained runtime")
|
||||
for config in ["app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.runtimeconfig.json", "observer/observer.runtimeconfig.json"]:
|
||||
runtime = json.loads((root / config).read_text())
|
||||
included = runtime["runtimeOptions"]["includedFrameworks"]
|
||||
if not included or any(value["version"] != "10.0.14" for value in included):
|
||||
raise RuntimeError("Published participant is not the prospectively fixed self-contained runtime")
|
||||
dependencies = json.loads((root / "app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.deps.json").read_text())["libraries"]
|
||||
receipt = {"protocol": "sdk2-saturation-v1", "sourceRevision": revision, "image": options.image,
|
||||
"rid": "linux-arm64", "runtimeVersion": "10.0.14", "selfContained": True,
|
||||
"canonicalCycles": 384, "completedRestarts": 96, "settlingCycles": 1, "maximumPhaseSeconds": 1800,
|
||||
"windows": [[321, 352], [353, 384]], "observerBufferMiB": 16, "runtimeKeywords": "0x18", "rundown": False,
|
||||
"acceptance": False, "publishCommand": command,
|
||||
"acceptance": False, "preparedUtc": datetime.now(timezone.utc).isoformat(), "publishCommand": command,
|
||||
"sdkLibraries": [name for name in dependencies if name.startswith("OPCFoundation.")],
|
||||
"testSourceSha256": sha(project.parent / "Sdk2LifecycleSoakTests.cs"),
|
||||
"observerSourceSha256": sha(source / "SaturationObserver.cs"), "files": manifest}
|
||||
receipt_path = root / "receipt.json"
|
||||
|
||||
@@ -859,6 +859,7 @@ public sealed class Sdk2LifecycleSoakTests
|
||||
var stdout = observer.StandardOutput.ReadToEndAsync(deadline.Token);
|
||||
var stderr = observer.StandardError.ReadToEndAsync(deadline.Token);
|
||||
await control.AttachAsync(deadline.Token);
|
||||
if (!foreignMarker) SaturationProtocolRuntimeProbe(1).ShouldBe(2);
|
||||
if (foreignMarker) SaturationMarkers.Log.Boundary(Guid.NewGuid().ToString("D"), "window1-start", 321, 80);
|
||||
control.Boundary("window1-start", 321, 80);
|
||||
await Task.Delay(20, deadline.Token);
|
||||
@@ -868,6 +869,12 @@ public sealed class Sdk2LifecycleSoakTests
|
||||
control.Boundary("window2-end", 384, 96);
|
||||
await control.DetachAndDrainAsync(deadline.Token);
|
||||
control.Readiness.ShouldBe(!foreignMarker);
|
||||
if (!foreignMarker)
|
||||
{
|
||||
using var parsed = JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(directory, "drained.json"), deadline.Token));
|
||||
parsed.RootElement.GetProperty("runtimeControlJit").GetInt32().ShouldBeGreaterThan(0,
|
||||
"the known non-SDK probe must prove runtime-provider delivery and method classification");
|
||||
}
|
||||
SaturationMarkers.Log.IsEnabled().ShouldBeFalse("Stop and stream drain precede return to the target");
|
||||
await observer.WaitForExitAsync(deadline.Token);
|
||||
observer.ExitCode.ShouldBe(0, await stderr);
|
||||
@@ -880,6 +887,10 @@ public sealed class Sdk2LifecycleSoakTests
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining
|
||||
| System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
|
||||
private static int SaturationProtocolRuntimeProbe(int value) => value + 1;
|
||||
|
||||
[EventSource(Name = "OtOpcUa-Sdk2-Saturation")]
|
||||
private sealed class SaturationMarkers : EventSource
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user