[deferred: further tuning] Out-of-process script runner misses §8.2 Windows host-CPU, Windows idle-latency and compiled-worker RSS budgets #789
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Measured 2026-11-10 with the new opt-in perf harness,
ScriptRunnerPerfTests(Category=Perf,OTOPCUA_SCRIPT_PERF=1, lmxopcua#780). Full tables are in design doc §12.1.The accepted gate passes: at K=2 the runner does at least 5,000 evals/s/node with every result correct. macOS closed loop: 336k/s. windev closed loop: 51k/s. The other §8.2 budgets miss:
What the CPU is spent on. A sequential probe on macOS used 156 µs of host CPU per evaluation, and 63 µs with
DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=0. Thread-pool spin-waiting between hops is therefore the larger part. At idle the host is about 1 %.Candidates:
WaitAsync(timeout)timer and the hard-deadline timer allocation (one timer wheel instead);RunAsync.RSS. Roslyn stays resident because compiles run in the worker (§14 #7). Either accept a revised budget, or measure a worker that served only warm-cache PE loads.
Windows latency and kill timing. Re-measure on a non-VM box before treating these as real misses.
Not blocking P2 (Shadow). These are the inputs to the P3 flip decision.
Partial fix landed on
master@8582ce91. The owner deferred further tuning, so this issue stays open for the budgets below that are still missed. The full record and tables are in design doc §12.2.What was costing host CPU. A probe run with
DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=0showed the IPC work itself was only about 10 µs per evaluation. The rest was thread-pool spin-waiting, caused by several pool work items per evaluation. On Unix there was a second cost: the socket under a .NET pipe turns non-blocking after its first async call, so every read went through the socket engine.Changes:
ScriptWorkerFrameWriter). The thread that produces a frame now writes it itself, merging any evaluations or results queued meanwhile, with one write per pass. The old loop also calledPipeStream.FlushAsync; PipeStream does not override it, so that queued one pool work item per pass.ScriptWorkerFrameReader). One buffered read now takes every frame that has arrived.Tried for worker memory, and not kept, because none of them changed RSS: a ReadyToRun worker,
GCgen0size,GCConserveMemory,TieredPGO=0, and an aggressive GC after a compile burst.Before → after, §8 harness, K=2. "vs InProcess" is the new harness column: out-of-process host CPU minus in-process host CPU in the same phase, i.e. the IPC share.
Notes on the numbers:
1ead8f45.Still missed (open, further tuning deferred):
Evidence:
ZB.MOM.WW.OtOpcUa.slnxwith--no-incremental: 0 warnings, 0 errors.AKKA_TEST_TIMEFACTOR=3, after the rebase onto963b0d66: ScriptWorker 151 passed / 3 skipped, Core.Scripting 221 / 1, Host 269, Runtime 993 / 3, OpcUaServer.IntegrationTests 132/132.A_caller_that_blocks_after_its_result_does_not_starve_the_reader;A_caller_that_awaited_its_run_is_never_refused_by_it.Out-of-process script runner misses §8.2 host-CPU, idle-latency (Windows), kill-latency (Windows) and RSS budgetsto [deferred: further tuning] Out-of-process script runner misses §8.2 Windows host-CPU, Windows idle-latency and compiled-worker RSS budgets