perf(scripting): cache ScriptSandbox BCL + pinned metadata references (#603)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 06:52:35 -04:00
parent a8703c6afe
commit 1b37b2f2b0
2 changed files with 26 additions and 3 deletions
@@ -81,10 +81,12 @@ public static class ScriptSandbox
// surface (operator-authored hosting helpers, third-party packages, app code)
// is not on the list and stays unreachable.
var references = new List<MetadataReference>();
// #603: both halves are cached. CreateFromFile reads + maps the DLL, and the BCL half is ~170
// assemblies, so doing it per compile was the dominant fixed cost of every ScriptEvaluator build.
// MetadataReference is immutable and thread-safe, so sharing instances across compilations is safe.
foreach (var asm in pinnedAssemblies)
references.Add(MetadataReference.CreateFromFile(asm.Location));
foreach (var path in EnumerateBclAssemblyPaths())
references.Add(MetadataReference.CreateFromFile(path));
references.Add(PinnedReferences.GetOrAdd(asm, static a => MetadataReference.CreateFromFile(a.Location)));
references.AddRange(BclReferences.Value);
var imports = new[]
{
@@ -97,6 +99,13 @@ public static class ScriptSandbox
return new SandboxConfig(references, imports);
}
private static readonly System.Collections.Concurrent.ConcurrentDictionary<System.Reflection.Assembly, MetadataReference> PinnedReferences = new();
// The trusted-platform set is process-constant, so the BCL reference list is context-independent.
private static readonly Lazy<IReadOnlyList<MetadataReference>> BclReferences = new(
static () => EnumerateBclAssemblyPaths().Select(static p => (MetadataReference)MetadataReference.CreateFromFile(p)).ToArray(),
LazyThreadSafetyMode.ExecutionAndPublication);
private static IEnumerable<string> EnumerateBclAssemblyPaths()
{
// The .NET host advertises the resolved runtime-shared-framework + BCL DLL set
@@ -57,4 +57,18 @@ public sealed class ScriptSandboxBuildTests
"""return (double)ctx.GetTag("X").Value;""");
evaluator.ShouldNotBeNull();
}
/// <summary>#603: the BCL reference set is built once per process, so two Build calls share the same
/// <c>MetadataReference</c> instances rather than re-reading ~170 DLLs per compile.</summary>
[Fact]
public void Build_reuses_metadata_reference_instances_across_calls()
{
var first = ScriptSandbox.Build(typeof(ScriptContext));
var second = ScriptSandbox.Build(typeof(ScriptContext));
first.References.Count.ShouldBeGreaterThan(50);
first.References.Count.ShouldBe(second.References.Count);
for (var i = 0; i < first.References.Count; i++)
ReferenceEquals(first.References[i], second.References[i]).ShouldBeTrue($"reference {i} was rebuilt");
}
}