From 71284adcc4e8bae440d5b2090cbf1bed441104d3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 12 Aug 2026 16:38:04 -0400 Subject: [PATCH] fix(scripts): memoize missing-assembly resolution on the site compile path --- .../Scripts/ScriptCompilationService.cs | 12 ++++++++++++ .../Scripts/ScriptCompilationServiceTests.cs | 15 +++++++++++++++ 2 files changed, 27 insertions(+) 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); + } }