perf(ui): shared KPI cache, live-cache-backed alarm summary, coalesced debug renders

This commit is contained in:
Joseph Doherty
2026-08-14 20:53:38 -04:00
parent ee193cd2bb
commit 8c0b36b2fa
19 changed files with 1554 additions and 64 deletions
@@ -179,6 +179,31 @@ public class ScriptAnalysisService
private const int SandboxMaxCallSharedDepth = 16;
/// <summary>
/// Process-wide cap on concurrently executing Test Runs (arch-review WP2.4).
/// <para>
/// A sandbox run occupies a thread-pool thread for its whole duration — the
/// <c>SandboxScriptHost</c> attribute accessors are synchronous by contract and block on
/// cross-site I/O (<c>GetAwaiter().GetResult()</c>), so the thread is <em>parked</em>, not
/// merely busy, for up to the 10s sandbox timeout. Unbounded, a handful of Designers
/// each hitting Run on a slow-bound script could park enough pool threads to starve the
/// Blazor circuits sharing this process. The gate bounds that: excess runs wait for a
/// slot instead of taking a thread, and the wait is charged to the caller's own timeout.
/// </para>
/// <para>
/// Static because <c>ScriptAnalysisService</c> is registered scoped — a per-instance gate
/// would be per-request and bound nothing.
/// </para>
/// </summary>
private static readonly SemaphoreSlim SandboxRunGate =
new(SandboxMaxConcurrentRuns, SandboxMaxConcurrentRuns);
/// <summary>
/// Slot count for <see cref="SandboxRunGate"/>. Deliberately small: Test Run is an
/// authoring convenience on a node whose real job is serving circuits.
/// </summary>
private const int SandboxMaxConcurrentRuns = 4;
/// <summary>
/// Compiles and runs a script in the central process. The globals surface
/// depends on <see cref="SandboxRunRequest.Kind"/>: template and shared
@@ -428,8 +453,15 @@ public class ScriptAnalysisService
using var errorScope = captureError.BeginCapture(captured);
var stopwatch = Stopwatch.StartNew();
var gateAcquired = false;
try
{
// Bound concurrent Test Runs (see SandboxRunGate). The wait is charged to the
// caller's own linked token, so queue time counts against the sandbox timeout
// and a queued run reports Timeout rather than hanging.
await SandboxRunGate.WaitAsync(linkedCts.Token).ConfigureAwait(false);
gateAcquired = true;
// Run on a thread-pool thread with no SynchronizationContext: a
// bound script's Instance.SetAttribute / Attributes[...] block
// synchronously on cross-site I/O (the API surface is sync by
@@ -480,6 +512,10 @@ public class ScriptAnalysisService
$"{inner.GetType().Name}: {inner.Message}",
SandboxErrorKind.RuntimeError, stopwatch.ElapsedMilliseconds, null);
}
finally
{
if (gateAcquired) SandboxRunGate.Release();
}
// outScope / errorScope are disposed by their `using` declarations when the
// method returns, restoring the previous capture scope on this call-tree
// without touching process-global Console state.