docs(script-runner): §8.2 worker idle RSS budget revised to 200 MB + scripts (#789)
The owner revised the per-shard worker idle RSS budget from 80 MB to 200 MB + scripts on 2026-11-11: compiles run in the worker, so Roslyn's code and metadata stay resident (measured 159-187 MB) and no GC setting moves it. The perf harness now reports both RSS rows against the 200 MB constant with a PASS/MISS mark; A-3 records what the measurement showed. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
@@ -536,7 +536,7 @@ size for 2× headroom (10k/s). The passthrough fast path is excluded because it
|
||||
| Host CPU for IPC | ≤ 25% of one core at 10k evals/s | Serialisation + pipe I/O, measured by an opt-in perf test. Since lmxopcua#789 the harness reports it as the out-of-process runner's host CPU minus the in-process runner's in the same phase ("vs InProcess"): the raw figure includes the harness's own pacing loop and pool, which InProcess pays too (§12.2) |
|
||||
| Bytes per eval on the wire | ≤ 64 B + 24 B per input | Positional slots, §4.4 |
|
||||
| Shard restart → first result, 1,000 cached scripts | ≤ 2 s | §7 lazy PE load |
|
||||
| Worker idle RSS | ≤ 80 MB + scripts **[A-3]** | Per shard. **Proposed revision (lmxopcua#789, §12.2):** ≤ 80 MB + scripts for a worker that has only loaded cached PEs (measured 36–58 MB); ≤ 200 MB + scripts once Roslyn has run in it (measured 159–187 MB), because compiling in the worker (§14 #7) keeps Roslyn's code and metadata resident and no GC setting moves it |
|
||||
| Worker idle RSS | ≤ 200 MB + scripts | Per shard. **Revised by the owner 2026-11-11 (lmxopcua#789)** from the original ≤ 80 MB + scripts **[A-3]**. Compiling in the worker (§14 #7) keeps Roslyn's code and metadata resident once it has run (measured 159–187 MB), and no GC setting moves it (§12.2). A worker that has only loaded cached PEs sits far below that (measured 36–58 MB). The perf harness reports both against 200 MB |
|
||||
| Kill latency after `T+G` | ≤ 100 ms | Host timer resolution plus `Process.Kill` |
|
||||
|
||||
**Comparison with today:** the in-process cost per compiled run is one `Task.Run` hop plus `WaitAsync`, which
|
||||
@@ -1367,7 +1367,7 @@ Each has a recommendation, marked ★.
|
||||
|---|---|---|
|
||||
| A-1 | Unbounded recursion in a script throws `StackOverflowException` and kills the host today | Reproduce in a throwaway process (not the test host) |
|
||||
| A-2 | A collectible ALC whose code is still executing on an orphan thread cannot be unloaded | .NET docs / a `WeakReference` test with a spinning orphan |
|
||||
| A-3 | A minimal .NET 10 worker idles at tens of MB RSS | Measure the P1 worker |
|
||||
| A-3 | A minimal .NET 10 worker idles at tens of MB RSS | Measured (§12.1/§12.2): true only for a worker that has never compiled (36–58 MB); a worker that has run Roslyn idles at 159–187 MB, so §8.2 was revised to 200 MB |
|
||||
| A-4 | `P = max(2, cores/K)` evaluation threads per worker is enough | P1 perf harness |
|
||||
| A-5 | Passthrough scripts are a large share of evaluations on real deployments | Count `PassthroughScript.TryMatch` hits against compiled runs on QA (add a counter in P0) |
|
||||
| A-6 | A local pipe round trip costs tens of µs | P1 perf harness |
|
||||
|
||||
@@ -32,6 +32,12 @@ public sealed class ScriptRunnerPerfTests
|
||||
{
|
||||
private const string OptInVariable = "OTOPCUA_SCRIPT_PERF";
|
||||
private const int AcceptedGateEvalsPerSecond = 5_000;
|
||||
|
||||
/// <summary>
|
||||
/// §8.2 "Worker idle RSS ≤ 200 MB + scripts", per shard (revised by the owner 2026-11-11, lmxopcua#789: compiles
|
||||
/// run in the worker, so Roslyn stays resident; the original 80 MB [A-3] is only met by a warm-PE-cache worker).
|
||||
/// </summary>
|
||||
private const int WorkerIdleRssBudgetMb = 200;
|
||||
private const int Keys = 2_000;
|
||||
private const int DistinctSources = 50;
|
||||
|
||||
@@ -481,7 +487,7 @@ public sealed class ScriptRunnerPerfTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// §8.2 "Shard restart → first result, 1,000 cached scripts ≤ 2 s", "Worker idle RSS ≤ 80 MB + scripts" and
|
||||
/// §8.2 "Shard restart → first result, 1,000 cached scripts ≤ 2 s", "Worker idle RSS ≤ 200 MB + scripts" and
|
||||
/// "Kill latency after T+G ≤ 100 ms". Report-only.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -559,14 +565,20 @@ public sealed class ScriptRunnerPerfTests
|
||||
}
|
||||
}
|
||||
|
||||
static string RssCell(long bytes)
|
||||
{
|
||||
var mb = bytes / 1048576.0;
|
||||
return $"{mb:N0} MB {(mb <= WorkerIdleRssBudgetMb ? "PASS" : "MISS")}";
|
||||
}
|
||||
|
||||
report.AppendLine("| Measure | Result | §8.2 budget |")
|
||||
.AppendLine("|---|---:|---|")
|
||||
.AppendLine($"| {scripts} scripts, empty cache, all first results (compile + write) | {cold.TotalMilliseconds:N0} ms | — |")
|
||||
.AppendLine($"| {scripts} scripts, fresh runner on a warm cache, all first results | {warmFresh.TotalMilliseconds:N0} ms | ≤ 2,000 ms |")
|
||||
.AppendLine($"| kill both shards → every script's next correct result | {killRestart.TotalMilliseconds:N0} ms | ≤ 2,000 ms |")
|
||||
.AppendLine($"| worker RSS after first eval, compiled (max of 2 shards) | {rssIdle / 1048576.0:N0} MB | ≤ 80 MB + scripts |")
|
||||
.AppendLine($"| worker RSS after first eval, compiled (max of 2 shards) | {RssCell(rssIdle)} | ≤ {WorkerIdleRssBudgetMb} MB + scripts |")
|
||||
.AppendLine($"| worker RSS with {scripts / 2} scripts compiled per shard (max) | {rssLoaded / 1048576.0:N0} MB | — |")
|
||||
.AppendLine($"| worker RSS after first eval, warm PE cache, no compile (max) | {rssWarmIdle / 1048576.0:N0} MB | ≤ 80 MB + scripts |")
|
||||
.AppendLine($"| worker RSS after first eval, warm PE cache, no compile (max) | {RssCell(rssWarmIdle)} | ≤ {WorkerIdleRssBudgetMb} MB + scripts |")
|
||||
.AppendLine($"| worker RSS with {scripts / 2} scripts loaded from the cache per shard (max) | {rssWarmLoaded / 1048576.0:N0} MB | — |")
|
||||
.AppendLine($"| kill latency after T+G (5 runs, ms) | {string.Join(" / ", killLatencies.Select(l => l.ToString("N0")))} | ≤ 100 ms |");
|
||||
Report(report);
|
||||
|
||||
Reference in New Issue
Block a user