perf(runtime): gate the VT apply-boundary compile-cache clear on a changed expression set (02/P7)

This commit is contained in:
Joseph Doherty
2026-07-13 10:15:47 -04:00
parent 37664e6992
commit a916ae6ebe
2 changed files with 19 additions and 6 deletions
@@ -71,7 +71,7 @@
{
"id": "R2-03-T10",
"subject": "P7 fix: gate the OnApply ClearCompiledScripts on HashSet.SetEquals of desired vs _planByVtag expression sets (ordinal), with explanatory comment (turns T9 green)",
"status": "pending",
"status": "completed",
"blockedBy": [
"R2-03-T9"
]
@@ -85,11 +85,24 @@ public sealed class VirtualTagHostActor : ReceiveActor
private void OnApply(ApplyVirtualTags msg)
{
// Apply-boundary compile-cache drop: each deploy generation may carry edited script sources, and
// the production evaluator roots one collectible AssemblyLoadContext per compiled expression. Drop
// them here (mirroring ScriptedAlarmEngine's per-generation clear) so stale ALCs are reclaimed
// instead of accreting across edits. No-op for evaluators without a cache (NullVirtualTagEvaluator).
(_evaluator as IScriptCacheOwner)?.ClearCompiledScripts();
// Apply-boundary compile-cache drop (gated — 02/P7): the production evaluator roots one
// collectible AssemblyLoadContext per compiled expression, so a changed script set must drop the
// stale ALCs. But clearing on EVERY apply forces a full recompilation even on a byte-identical
// redeploy (and each unnecessary clear is an unnecessary 02/S12 race window). Clear only when the
// deployed EXPRESSION set actually changed. The cache is keyed by source only, so a rename /
// FolderPath move / Historize toggle (which respawns the child but keeps the source) needs no
// recompile; set semantics also mean two vtags sharing one expression keep it alive while either
// remains. _planByVtag still holds the PREVIOUS generation here (it is rebuilt to the desired set
// at the end of this method); on the first apply after (re)start it is empty, so the sets differ
// and the clear fires (harmless — converges the singleton evaluator's cache to this generation).
// NOTE: this gate reduces clear FREQUENCY only; the S12 retry-once in RoslynVirtualTagEvaluator —
// not this gate — is the correctness fix for a clear racing an in-flight evaluation.
var newExpressions = msg.Plans.Select(p => p.Expression).ToHashSet(StringComparer.Ordinal);
var prevExpressions = _planByVtag.Values.Select(p => p.Expression).ToHashSet(StringComparer.Ordinal);
if (!newExpressions.SetEquals(prevExpressions))
{
(_evaluator as IScriptCacheOwner)?.ClearCompiledScripts();
}
var desired = new HashSet<string>(msg.Plans.Select(p => p.VirtualTagId), StringComparer.Ordinal);