Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/SaturationObserver.cs
T

134 lines
7.7 KiB
C#

// Diagnostic-only observer. The target remains the unchanged canonical lifecycle test process.
using System.Diagnostics.Tracing;
using System.Text.Json;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tracing;
const string protocol = "sdk2-saturation-v1";
const string markerProvider = "OtOpcUa-Sdk2-Saturation";
var control = Path.GetFullPath(args[0]);
var runId = args[1];
if (!Guid.TryParseExact(runId, "D", out _)) throw new InvalidDataException("Malformed run identity");
using var lifetime = new CancellationTokenSource(TimeSpan.FromSeconds(1980));
var ct = lifetime.Token;
try
{
var request = await WaitJson("attach-request.json", ct);
using (request)
{
Identity(request.RootElement);
Require(request.RootElement.GetProperty("firstCycle").GetInt32() == 321
&& request.RootElement.GetProperty("completedRestarts").GetInt32() == 80, "Invalid attach boundary");
var targetPid = request.RootElement.GetProperty("targetPid").GetInt32();
Require(targetPid > 0 && targetPid != Environment.ProcessId, "Invalid target process");
var providers = new[]
{
new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, 0x18),
new EventPipeProvider(markerProvider, EventLevel.Informational)
};
var client = new DiagnosticsClient(targetPid);
var session = await Task.Run(() => client.StartEventPipeSession(providers, requestRundown: false, circularBufferMB: 16), ct)
.WaitAsync(TimeSpan.FromSeconds(10), ct);
var trace = Path.Combine(control, "readiness.nettrace");
await using (var output = new FileStream(trace, FileMode.CreateNew, FileAccess.Write, FileShare.Read))
{
var copy = session.EventStream.CopyToAsync(output, ct);
await Publish("active.json", new { protocol, runId, targetPid, observerPid = Environment.ProcessId, active = true });
using var stop = await WaitJson("detach-request.json", ct);
Identity(stop.RootElement);
Require(stop.RootElement.GetProperty("targetPid").GetInt32() == targetPid
&& stop.RootElement.GetProperty("lastCycle").GetInt32() == 384
&& stop.RootElement.GetProperty("completedRestarts").GetInt32() == 96, "Invalid detach boundary");
await Task.Run(session.Stop, ct).WaitAsync(TimeSpan.FromSeconds(10), ct);
await copy.WaitAsync(TimeSpan.FromSeconds(10), ct);
await output.FlushAsync(ct);
}
await Task.Run(session.Dispose, ct).WaitAsync(TimeSpan.FromSeconds(5), ct);
var summary = await Task.Run(() => Parse(trace, runId), ct).WaitAsync(TimeSpan.FromSeconds(10), ct);
// Published only after Stop, stream drain, disposal and complete parsing. Never just a file-exists ACK.
await Publish("drained.json", new
{
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.runtimeControlJit,
configuration = new { runtimeProvider = "Microsoft-Windows-DotNETRuntime", level = "Verbose", keywords = "0x18",
markerProvider, circularBufferMiB = 16, requestRundown = false },
acceptance = false
});
}
}
catch (Exception error)
{
await Publish("observer-failed.json", new { protocol, runId, exceptionType = error.GetType().FullName });
// An IPC/Stop worker can still be blocked. The parent also bounds and reaps this separate process.
Environment.Exit(1);
}
void Identity(JsonElement value) => Require(value.GetProperty("protocol").GetString() == protocol
&& value.GetProperty("runId").GetString() == runId, "Mismatched observer identity");
static void Require(bool condition, string message) { if (!condition) throw new InvalidDataException(message); }
async Task<JsonDocument> WaitJson(string name, CancellationToken token)
{
var path = Path.Combine(control, name);
while (!File.Exists(path)) await Task.Delay(25, token);
return JsonDocument.Parse(await File.ReadAllTextAsync(path, token));
}
async Task Publish(string name, object value)
{
var temporary = Path.Combine(control, name + ".tmp");
await using (var stream = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write))
await JsonSerializer.SerializeAsync(stream, value);
File.Move(temporary, Path.Combine(control, name));
}
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 =>
{
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 =>
{
if (data.FullyQualifiedAssemblyName.StartsWith("Opc.Ua.", StringComparison.Ordinal))
events.Add(new(data.TimeStampRelativeMSec, "sdk-assembly", 0));
};
source.Dynamic.All += data =>
{
if (data.ProviderName == "OtOpcUa-Sdk2-Saturation" && data.EventName == "Boundary")
markers.Add(new(data.TimeStampRelativeMSec, (string)data.PayloadByName("runId"),
(string)data.PayloadByName("name"), (int)data.PayloadByName("cycle"), (int)data.PayloadByName("restarts")));
};
source.Process();
(string Name, int Cycle, int Restarts)[] expected =
[ ("window1-start", 321, 80), ("window1-end", 352, 88),
("window2-start", 353, 88), ("window2-end", 384, 96) ];
var valid = markers.Count == 4 && markers.Select((marker, index) =>
marker.Run == expectedRun && (marker.Name, marker.Cycle, marker.Restarts) == expected[index]).All(x => x)
&& markers.Zip(markers.Skip(1)).All(pair => pair.First.Ms < pair.Second.Ms);
var windows = Enumerable.Range(0, 2).Select(index =>
{
var selected = valid ? events.Where(x => x.Ms >= markers[index * 2].Ms && x.Ms <= markers[index * 2 + 1].Ms).ToArray() : [];
return new Window($"window{index + 1}", 321 + index * 32, 352 + index * 32, 80 + index * 8, 88 + index * 8,
selected.Count(x => x.Kind == "factory-jit"), selected.Count(x => x.Kind == "factory-load"),
selected.Count(x => x.Kind == "sdk-assembly"));
}).ToArray();
// 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), 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 runtimeControlJit);