fix(scripts): memoize missing-assembly resolution on the site compile path

This commit is contained in:
Joseph Doherty
2026-08-12 16:38:04 -04:00
parent 089b16980f
commit 71284adcc4
2 changed files with 27 additions and 0 deletions
@@ -89,9 +89,21 @@ public class ScriptCompilationService
/// <c>ScriptAnalysisService.DefaultOptions</c> and /// <c>ScriptAnalysisService.DefaultOptions</c> and
/// <c>ScriptTrustPolicy.DefaultReferences</c> already follow this pattern. /// <c>ScriptTrustPolicy.DefaultReferences</c> already follow this pattern.
/// </para> /// </para>
///
/// <para>
/// Static options alone are NOT enough: every <c>script.Compile()</c> binds the
/// transitive closure of these references, and Roslyn's default resolver
/// re-resolves that closure through <c>MetadataReference.CreateFromFile</c> on
/// EVERY compile (~74105 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
/// <see cref="CachingScriptMetadataResolver"/> memoizes those resolutions
/// process-wide; see its doc for the full mechanism.
/// </para>
/// </summary> /// </summary>
private static readonly ScriptOptions SharedScriptOptions = ScriptOptions.Default private static readonly ScriptOptions SharedScriptOptions = ScriptOptions.Default
.WithReferences(ScriptAssemblies) .WithReferences(ScriptAssemblies)
.WithMetadataResolver(CachingScriptMetadataResolver.Instance)
.WithImports( .WithImports(
"System", "System",
"System.Collections.Generic", "System.Collections.Generic",
@@ -251,4 +251,19 @@ public class ScriptCompilationServiceTests
// The options object itself is cached, so it must not be rebuilt either. // The options object itself is cached, so it must not be rebuilt either.
Assert.Same(first.CompiledScript.Options, second.CompiledScript.Options); 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);
}
} }