feat(scripting): batch submit, exact per-item deadlines, scripted-alarm seam on the out-of-process runner (lmxopcua#780)
v2-ci / unit-tests (push) Waiting to run

- IScriptRunner.RunBatch: one EvalBatch frame per worker for every evaluation ready to run; per-item, streamed results.
  The default (InProcessScriptRunner) is RunAsync per item. Used by the Calculation pump (dequeue all, split at a
  repeated tag, publish in dequeue order), the VirtualTag seam (ScriptBatchCoalescer, no timer) and the scripted-alarm
  engine (whole dirty set). In process the pump and the engine stay one at a time, unchanged.
- Exact per-item deadlines: the worker hands work to an idle evaluation thread or defers it (EvalResults
  deferred_eval_ids); the host stops that run's clock and restarts T then G when it starts (started_eval_ids). A run
  queued behind a runaway is neither timed out nor struck and is a victim, not the culprit, at the kill.
- Scripted-alarm seam behind Scripting:Runner:Seams; unset Seams now means VirtualTag + Calculation, so alarms run out of
  process only when listed. Load still compiles each predicate (to PE bytes only) so a broken one fails the load; passes
  batch and apply in the serial order under the gate. Script faults freeze the condition BadInternalError; runner faults
  freeze it BadResourceUnavailable and re-evaluate on recovery; a dead worker never clears an alarm. IScriptRunnerProvider
  carries the runner to DriverHostActor.
- Perf harness: batch throughput (RunBatch vs RunAsync x n) and alarm-pass latency (in process / batched / serial);
  design §12.3 records macOS + windev. On windev the serial out-of-process alarm pass misses 250 ms from ~500 alarms;
  batched p99 <= 53 ms at 1,000.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-10 06:37:36 -05:00
parent 11a5c1d4e2
commit 0796928504
33 changed files with 2168 additions and 117 deletions
@@ -134,8 +134,17 @@ message EvalResult {
uint32 run_micros = 6;
}
// Per-item deadlines are exact for queued work too (lmxopcua#780 batch submit): the host arms an evaluation's soft and
// hard deadlines when it submits, which is when the run starts as long as the worker has an idle evaluation thread. An
// evaluation that arrives while every thread is busy (a batch larger than the scheduler, or work queued behind a
// runaway) is reported in `deferred_eval_ids`; the host then stops its clock, and restarts it — the full T, then G —
// when the evaluation appears in `started_eval_ids`, which an evaluation thread sends alongside the result of the run it
// just finished. So a run waiting behind a sibling is never timed out, struck or killed for the sibling's time.
// Either list may be the only content of a frame. A host that predates these fields ignores them.
message EvalResults {
repeated EvalResult items = 1;
repeated uint64 started_eval_ids = 2;
repeated uint64 deferred_eval_ids = 3;
}
message ScriptError {
@@ -233,7 +233,10 @@ public sealed class ScriptWorkerFrameWriter
break;
case Envelope.BodyOneofCase.Results:
if (evals is not null) { Append(new Envelope { Eval = evals }); evals = null; }
(results ??= new EvalResults()).Items.AddRange(envelope.Results.Items);
results ??= new EvalResults();
results.Items.AddRange(envelope.Results.Items);
results.StartedEvalIds.AddRange(envelope.Results.StartedEvalIds);
results.DeferredEvalIds.AddRange(envelope.Results.DeferredEvalIds);
break;
default:
if (evals is not null) { Append(new Envelope { Eval = evals }); evals = null; }