diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs index 42cf1790..1ade6dd0 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs @@ -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(); + // #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 PinnedReferences = new(); + + // The trusted-platform set is process-constant, so the BCL reference list is context-independent. + private static readonly Lazy> BclReferences = new( + static () => EnumerateBclAssemblyPaths().Select(static p => (MetadataReference)MetadataReference.CreateFromFile(p)).ToArray(), + LazyThreadSafetyMode.ExecutionAndPublication); + private static IEnumerable EnumerateBclAssemblyPaths() { // The .NET host advertises the resolved runtime-shared-framework + BCL DLL set diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ScriptSandboxBuildTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ScriptSandboxBuildTests.cs index a3d3e212..5231af8b 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ScriptSandboxBuildTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ScriptSandboxBuildTests.cs @@ -57,4 +57,18 @@ public sealed class ScriptSandboxBuildTests """return (double)ctx.GetTag("X").Value;"""); evaluator.ShouldNotBeNull(); } + + /// #603: the BCL reference set is built once per process, so two Build calls share the same + /// MetadataReference instances rather than re-reading ~170 DLLs per compile. + [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"); + } }