perf(scripting): cache ScriptSandbox BCL + pinned metadata references (#603)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -81,10 +81,12 @@ public static class ScriptSandbox
|
|||||||
// surface (operator-authored hosting helpers, third-party packages, app code)
|
// surface (operator-authored hosting helpers, third-party packages, app code)
|
||||||
// is not on the list and stays unreachable.
|
// is not on the list and stays unreachable.
|
||||||
var references = new List<MetadataReference>();
|
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)
|
foreach (var asm in pinnedAssemblies)
|
||||||
references.Add(MetadataReference.CreateFromFile(asm.Location));
|
references.Add(PinnedReferences.GetOrAdd(asm, static a => MetadataReference.CreateFromFile(a.Location)));
|
||||||
foreach (var path in EnumerateBclAssemblyPaths())
|
references.AddRange(BclReferences.Value);
|
||||||
references.Add(MetadataReference.CreateFromFile(path));
|
|
||||||
|
|
||||||
var imports = new[]
|
var imports = new[]
|
||||||
{
|
{
|
||||||
@@ -97,6 +99,13 @@ public static class ScriptSandbox
|
|||||||
return new SandboxConfig(references, imports);
|
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()
|
private static IEnumerable<string> EnumerateBclAssemblyPaths()
|
||||||
{
|
{
|
||||||
// The .NET host advertises the resolved runtime-shared-framework + BCL DLL set
|
// 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;""");
|
"""return (double)ctx.GetTag("X").Value;""");
|
||||||
evaluator.ShouldNotBeNull();
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user