perf(site-runtime): warm the next startup batch during the stagger delay and compile a batch's distinct units in parallel (review 2026-10-21 01/SR-P3)

DeployCompileValidator.WarmCompileCache dedupes script bodies/trigger
expressions across a batch and compiles them with Parallel.ForEach (half the
cores). CreateInstanceActorBatch starts the next batch's warm as it arms the
stagger timer; creation order and delay are unchanged. The warm's parsed
configs are reused at creation.
This commit is contained in:
Joseph Doherty
2026-10-22 08:22:26 -04:00
parent dde22a3dd1
commit 999653a6b9
5 changed files with 256 additions and 29 deletions
@@ -154,6 +154,69 @@ public sealed class DeployCompileValidator
return errors;
}
/// <summary>
/// Populates the shared compile cache for a whole startup batch (review 2026-10-21 01/SR-P3):
/// every DISTINCT script body and trigger expression across <paramref name="configs"/> is
/// compiled once, up to <paramref name="maxDegreeOfParallelism"/> at a time. Same compile units
/// as <see cref="Validate(FlattenedConfiguration, string?)"/> — scripts and alarm on-trigger
/// scripts against <c>ScriptGlobals</c>, Expression triggers against
/// <c>TriggerExpressionGlobals</c> — but deduplicated first, because the cache is keyed on
/// code: N instances of one template would otherwise race N identical cold compiles in
/// parallel. Best-effort: verdicts are discarded and a throwing unit is skipped, since every
/// Instance Actor's own compile remains the correctness backstop.
/// </summary>
/// <param name="configs">The batch's parsed configurations.</param>
/// <param name="maxDegreeOfParallelism">Upper bound on concurrent Roslyn compiles.</param>
public void WarmCompileCache(IReadOnlyList<FlattenedConfiguration> configs, int maxDegreeOfParallelism)
{
var units = new Dictionary<(bool IsTrigger, string Code), string>();
foreach (var config in configs)
{
foreach (var script in config.Scripts)
{
units.TryAdd((false, script.Code), script.CanonicalName);
AddTriggerUnit(units, script.TriggerType, script.TriggerConfiguration,
$"script-trigger-{script.CanonicalName}");
}
// Alarm on-trigger scripts compile the referenced SCRIPT's body against ScriptGlobals,
// i.e. the same cache key as that script — already a unit above.
foreach (var alarm in config.Alarms)
AddTriggerUnit(units, alarm.TriggerType, alarm.TriggerConfiguration,
$"alarm-trigger-expr-{alarm.CanonicalName}");
}
Parallel.ForEach(
units,
new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, maxDegreeOfParallelism) },
unit =>
{
try
{
if (unit.Key.IsTrigger)
_compilationService.CompileTriggerExpression(unit.Value, unit.Key.Code);
else
_compilationService.Compile(unit.Value, unit.Key.Code);
}
catch
{
// Best-effort warm — the Instance Actor's PreStart compiles it the hard way.
}
});
}
private static void AddTriggerUnit(
Dictionary<(bool IsTrigger, string Code), string> units,
string? triggerType, string? triggerConfigJson, string compileName)
{
if (!string.Equals(triggerType, "Expression", StringComparison.OrdinalIgnoreCase))
return;
var expression = TriggerExpressionGlobals.ExtractExpression(triggerConfigJson);
if (expression != null)
units.TryAdd((true, expression), compileName);
}
/// <summary>
/// Review 2026-10-21 01/SR-C1: every name that becomes an Akka actor name on the site — the
/// Instance Actor itself (<c>DeploymentManagerActor.CreateInstanceActor</c>) and its