diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptCompilationService.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptCompilationService.cs index af159e77..1d66655c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptCompilationService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptCompilationService.cs @@ -89,9 +89,21 @@ public class ScriptCompilationService /// ScriptAnalysisService.DefaultOptions and /// ScriptTrustPolicy.DefaultReferences already follow this pattern. /// + /// + /// + /// Static options alone are NOT enough: every script.Compile() binds the + /// transitive closure of these references, and Roslyn's default resolver + /// re-resolves that closure through MetadataReference.CreateFromFile on + /// EVERY compile (~74–105 fresh native metadata copies per compiled script, + /// confirmed live post-5a781c70 on 2026-08-12: 21 scripts held 6,640 + /// AssemblyMetadata/PEReader/MetadataImageReference objects). The shared + /// memoizes those resolutions + /// process-wide; see its doc for the full mechanism. + /// /// private static readonly ScriptOptions SharedScriptOptions = ScriptOptions.Default .WithReferences(ScriptAssemblies) + .WithMetadataResolver(CachingScriptMetadataResolver.Instance) .WithImports( "System", "System.Collections.Generic", diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScriptCompilationServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScriptCompilationServiceTests.cs index 782b03d4..5b8dff9d 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScriptCompilationServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScriptCompilationServiceTests.cs @@ -251,4 +251,19 @@ public class ScriptCompilationServiceTests // The options object itself is cached, so it must not be rebuilt either. Assert.Same(first.CompiledScript.Options, second.CompiledScript.Options); } + + [Fact] + public void Compile_UsesProcessWideCachingMetadataResolver() + { + SiteScriptCompileCache.Clear(); + var result = _service.Compile("resolver-pin", "return 41 + 1;"); + + Assert.True(result.IsSuccess); + // Without the shared caching resolver, EVERY compile re-resolves the + // transitive assembly closure via MetadataReference.CreateFromFile — + // ~74+ fresh native metadata copies per compiled script (2026-08-12 dump). + Assert.Same( + ZB.MOM.WW.ScadaBridge.ScriptAnalysis.CachingScriptMetadataResolver.Instance, + result.CompiledScript!.Options.MetadataResolver); + } }