perf(scripting): #789 profile the Windows misses; add the batched paced row; nothing kept in the runner
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
Profiled the remaining lmxopcua#789 misses on windev, a VMware VM, using per-thread CPU and context-switch accounting plus ETW CPU sampling. - One blocking hand-off costs ~26 us of latency and ~18 us of CPU there, about 10x macOS. - An idle evaluation makes four hand-offs in a row: host writer -> worker reader -> evaluation thread -> host reader -> dispatcher. - 48% of host CPU at 10k/s is in the kernel: the per-RunAsync NtWriteFile, thread-pool wakes, reads and waits. Spin-before-block was measured at every hand-off and is not kept. It meets 150 us idle p50 (118 us) only by keeping about two host cores and four worker cores busy for one stream. At 10k/s it reads +250 points. Harness: new "paced 10,000/s, batched" row. It uses the same rate and keys, with each 1 ms tick submitted as one RunBatch call, which is the production seams' shape. It runs after the original four phases, so their rows stay comparable. On windev it halves host CPU, to +36..+45 points against +71..+84 unbatched. That is still above 25. Design doc: - 12.8: the record. - 14 #15: the owner's decision on the Windows budgets. Recommended: measure on bare-metal Windows, then state the two budgets per platform, with the batched row as the gate. The alternative is a shared-memory transport. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -154,7 +154,15 @@ public sealed class ScriptRunnerPerfTests
|
||||
/// the keys, skipping a key still in flight), and latency is measured per call. This is the "p99 at N evals/s" of
|
||||
/// the §8.2 table.
|
||||
/// </summary>
|
||||
private static async Task<PhaseResult> PacedAsync(IScriptRunner runner, int ratePerSecond)
|
||||
/// <param name="runner">The runner.</param>
|
||||
/// <param name="ratePerSecond">The offered rate.</param>
|
||||
/// <param name="batched">
|
||||
/// Submit each tick's evaluations as ONE <see cref="IScriptRunner.RunBatch"/> call instead of one
|
||||
/// <see cref="IScriptRunner.RunAsync"/> each — the shape of the production seams, whose Calculation pump, alarm
|
||||
/// engine and virtual-tag coalescer hand everything that became due together to the runner at once (§4.5,
|
||||
/// lmxopcua#789). Same offered rate, keys, per-result continuation and measurement as the unbatched phase.
|
||||
/// </param>
|
||||
private static async Task<PhaseResult> PacedAsync(IScriptRunner runner, int ratePerSecond, bool batched = false)
|
||||
{
|
||||
var samples = new List<double>(ratePerSecond * (int)Phase.TotalSeconds + 1024);
|
||||
var gate = new object();
|
||||
@@ -168,14 +176,32 @@ public sealed class ScriptRunnerPerfTests
|
||||
while (wall.Elapsed < Phase)
|
||||
{
|
||||
var due = (long)(wall.Elapsed.TotalSeconds * ratePerSecond);
|
||||
List<(int Key, int X)>? tick = batched ? [] : null;
|
||||
while (issued < due)
|
||||
{
|
||||
var key = next++ % Keys;
|
||||
if (Volatile.Read(ref inFlight[key])) continue;
|
||||
Volatile.Write(ref inFlight[key], true);
|
||||
var x = (int)issued++;
|
||||
if (tick is not null) tick.Add((key, x));
|
||||
else Track(key, x, Stopwatch.GetTimestamp(), runner.RunAsync(Invocation(key, x), Ct));
|
||||
}
|
||||
if (tick is { Count: > 0 })
|
||||
{
|
||||
var t0 = Stopwatch.GetTimestamp();
|
||||
pending.Add(runner.RunAsync(Invocation(key, x), Ct).ContinueWith(t =>
|
||||
var outcomes = runner.RunBatch(tick.Select(i => Invocation(i.Key, i.X)).ToArray(), Ct);
|
||||
for (var i = 0; i < tick.Count; i++) Track(tick[i].Key, tick[i].X, t0, outcomes[i]);
|
||||
}
|
||||
await Task.Delay(1, Ct);
|
||||
}
|
||||
await Task.WhenAll(pending);
|
||||
var elapsed = wall.Elapsed;
|
||||
var cpu = (Process.GetCurrentProcess().TotalProcessorTime - cpu0).TotalMilliseconds / elapsed.TotalMilliseconds * 100;
|
||||
samples.Sort();
|
||||
return new PhaseResult(issued / elapsed.TotalSeconds, Percentile(samples, 0.50), Percentile(samples, 0.99), cpu, ok, notOk, firstFailure);
|
||||
|
||||
void Track(int key, int x, long t0, Task<ScriptRunOutcome> run)
|
||||
=> pending.Add(run.ContinueWith(t =>
|
||||
{
|
||||
var us = Stopwatch.GetElapsedTime(t0).TotalMicroseconds;
|
||||
Volatile.Write(ref inFlight[key], false);
|
||||
@@ -188,14 +214,6 @@ public sealed class ScriptRunnerPerfTests
|
||||
Interlocked.CompareExchange(ref firstFailure, $"{o.Status}: {o.Reason}", null);
|
||||
}
|
||||
}, TaskScheduler.Default));
|
||||
}
|
||||
await Task.Delay(1, Ct);
|
||||
}
|
||||
await Task.WhenAll(pending);
|
||||
var elapsed = wall.Elapsed;
|
||||
var cpu = (Process.GetCurrentProcess().TotalProcessorTime - cpu0).TotalMilliseconds / elapsed.TotalMilliseconds * 100;
|
||||
samples.Sort();
|
||||
return new PhaseResult(issued / elapsed.TotalSeconds, Percentile(samples, 0.50), Percentile(samples, 0.99), cpu, ok, notOk, firstFailure);
|
||||
}
|
||||
|
||||
private static string Row(string runner, string phase, PhaseResult r, PhaseResult? inProcess = null)
|
||||
@@ -245,6 +263,8 @@ public sealed class ScriptRunnerPerfTests
|
||||
Add("paced 10,000/s", await PacedAsync(runner, 10_000));
|
||||
var closed = await ClosedLoopAsync(runner, concurrency: 64);
|
||||
Add("closed loop ×64", closed);
|
||||
// After the original four phases, so their rows stay comparable with every earlier record (lmxopcua#789).
|
||||
Add("paced 10,000/s, batched", await PacedAsync(runner, 10_000, batched: true));
|
||||
if (name == "OutOfProcess K=2") k2Closed = closed;
|
||||
}
|
||||
finally
|
||||
@@ -260,6 +280,7 @@ public sealed class ScriptRunnerPerfTests
|
||||
report.AppendLine()
|
||||
.AppendLine("§8.2 budgets: added latency p50 ≤ 150 µs (idle), p99 ≤ 2 ms at 10k/s, ≥ 10,000 evals/s at K=2, " +
|
||||
"host CPU for IPC (the \"vs InProcess\" column) ≤ 25 % of one core at 10k/s. " +
|
||||
"\"batched\": each 1 ms tick's evaluations go in one RunBatch call, as the production seams submit. " +
|
||||
"Accepted gate (owner, §14): ≥ 5,000 evals/s/node.");
|
||||
Report(report);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user