perf(ui): shared KPI cache, live-cache-backed alarm summary, coalesced debug renders
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user