perf(siteruntime): deploy gate + InstanceActor PreStart share one Roslyn compile via the compile cache (plan R2-03 T6)
This commit is contained in:
@@ -518,7 +518,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// delete/disable commands, breaking that ordering. A deploy is an
|
||||
// infrequent admin command, so briefly holding the singleton for a pure
|
||||
// Roslyn compile is acceptable; the central deployer already Asks and
|
||||
// waits for the DeploymentStatusResponse.
|
||||
// waits for the DeploymentStatusResponse. Redeploys and multi-instance
|
||||
// deploys of unchanged scripts hit the process-wide compile cache
|
||||
// (SiteScriptCompileCache), so the synchronous gate recompiles only
|
||||
// genuinely new code and the Instance Actor's PreStart reuses the gate's
|
||||
// compile (N4).
|
||||
var errors = _deployCompileValidator.Validate(command.FlattenedConfigurationJson);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,13 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
/// <see cref="ScriptTrustValidator"/>; this service keeps the real
|
||||
/// execution-path compile of the script against <see cref="ScriptGlobals"/> /
|
||||
/// <see cref="TriggerExpressionGlobals"/>.
|
||||
///
|
||||
/// Compile results — success AND failure — are memoised process-wide in
|
||||
/// <see cref="SiteScriptCompileCache"/> (keyed on code hash + globals type), so an
|
||||
/// unchanged script body compiles once per process lifetime and the deploy gate,
|
||||
/// the Instance Actor's PreStart recompile, and SharedScriptLibrary all reuse one
|
||||
/// compiled <c>Script<object?></c>. Error text is name-free by construction, so a
|
||||
/// cached failure renders correctly for every caller.
|
||||
/// </summary>
|
||||
public class ScriptCompilationService
|
||||
{
|
||||
@@ -92,10 +99,33 @@ public class ScriptCompilationService
|
||||
=> CompileCore(name, expression, typeof(TriggerExpressionGlobals));
|
||||
|
||||
/// <summary>
|
||||
/// Shared compilation path: validates the trust model, builds the script
|
||||
/// against the given globals type, and returns the compiled result.
|
||||
/// Shared compilation funnel for every site-side compile (the deploy gate, the
|
||||
/// Instance Actor's PreStart recompile, and SharedScriptLibrary). Results — success
|
||||
/// AND failure — are memoised process-wide in <see cref="SiteScriptCompileCache"/>,
|
||||
/// keyed on code hash + globals type: an unchanged script body compiles once per
|
||||
/// process lifetime and the compiled <c>Script<object?></c> is shared across all
|
||||
/// call sites (N4). Cached error text is name-free by construction (Roslyn diagnostics /
|
||||
/// the compile-exception message never embed the caller's script name), so a cached
|
||||
/// failure renders correctly for every caller.
|
||||
/// </summary>
|
||||
private ScriptCompilationResult CompileCore(string name, string code, Type globalsType)
|
||||
{
|
||||
var result = SiteScriptCompileCache.GetOrAdd(code, globalsType,
|
||||
() => CompileUncached(name, code, globalsType));
|
||||
if (!result.IsSuccess)
|
||||
_logger.LogDebug(
|
||||
"Script {Script}: returning cached compile failure ({ErrorCount} error(s))",
|
||||
name, result.Errors.Count); // miss-path logs name the FIRST caller; keep this caller visible
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The uncached compile: validates the trust model, builds the script against the
|
||||
/// given globals type, and returns the compiled result. The verdict is deterministic
|
||||
/// on the code (plus the compile-time-static trust policy and globals surface), so it
|
||||
/// is safe to cache alongside the compiled script.
|
||||
/// </summary>
|
||||
private ScriptCompilationResult CompileUncached(string name, string code, Type globalsType)
|
||||
{
|
||||
// Validate trust model
|
||||
var violations = ValidateTrustModel(code);
|
||||
|
||||
+23
@@ -16,6 +16,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
/// <c>Stopwatch</c> stays allowed. The real execution-path compile against
|
||||
/// <c>ScriptGlobals</c> / <c>TriggerExpressionGlobals</c> is unchanged.
|
||||
/// </summary>
|
||||
[Collection("SiteScriptCompileCache")]
|
||||
public class ScriptCompilationServiceTests
|
||||
{
|
||||
private readonly ScriptCompilationService _service;
|
||||
@@ -34,6 +35,28 @@ public class ScriptCompilationServiceTests
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_SameCodeTwice_SharesOneRoslynCompile()
|
||||
{
|
||||
SiteScriptCompileCache.Clear();
|
||||
var r1 = _service.Compile("deploy-gate-copy", "return 1 + 1;");
|
||||
var r2 = _service.Compile("prestart-copy", "return 1 + 1;");
|
||||
|
||||
Assert.True(r1.IsSuccess);
|
||||
Assert.Same(r1.CompiledScript, r2.CompiledScript); // one compile, shared Script<T> (N4)
|
||||
Assert.Equal(1, SiteScriptCompileCache.Hits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_ScriptAndTriggerExpression_DoNotCrossContaminate()
|
||||
{
|
||||
SiteScriptCompileCache.Clear();
|
||||
var script = _service.Compile("s", "1 > 0");
|
||||
var trigger = _service.CompileTriggerExpression("t", "1 > 0");
|
||||
|
||||
Assert.NotSame(script.CompiledScript, trigger.CompiledScript); // different globals surfaces
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_InvalidSyntax_ReturnsErrors()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user