using System.Collections.Concurrent; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Scripting; namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis; /// /// Process-wide memoizing decorator over Roslyn's default script metadata /// resolver, closing the second (dominant) half of the script-compile native /// memory leak fixed in part by 5a781c70. /// /// /// Why this exists. The shared ScriptOptions on every compile /// surface carry only the direct API-surface references; each /// script.Compile() binds their transitive closure, and every /// transitively-referenced assembly is resolved through the options' /// . Roslyn's /// RuntimeMetadataReferenceResolver has NO cross-compilation cache: each /// resolution calls MetadataReference.CreateFromFile, which eagerly /// copies the whole assembly into native memory (AssemblyMetadata → /// PEReader → NativeHeapMemoryBlock). Measured on Roslyn 5.0.0: /// a 5-assembly explicit set resolves a 74-assembly closure afresh on EVERY /// compile — ~74 fresh native metadata copies per compiled script, pinned for /// the process lifetime by the compile caches. This decorator memoizes, so each /// distinct assembly is materialized once per process regardless of compile count. /// /// /// /// Trust model unaffected. Resolution RESULTS are identical to the inner /// resolver's — only object identity is de-duplicated. The resolver can never /// resolve anything the undecorated options would not have resolved. /// /// /// /// Cache-correctness assumptions. The missing-assembly cache keys on the /// assembly identity display name and deliberately ignores the requesting /// definition (whose directory is a search path in the inner resolver): /// every ScadaBridge node runs from a single publish directory plus the shared /// framework, so identity → path is stable process-wide. A /// GetOrAdd factory race can mint one duplicate — bounded, benign. /// Entries are never disposed; the cache is bounded by the distinct assemblies /// on disk, the same order as the static . /// /// public sealed class CachingScriptMetadataResolver : MetadataReferenceResolver { /// /// The shared process-wide instance every script-compile surface attaches via /// ScriptOptions.WithMetadataResolver. Decorates /// 's resolver — the exact resolver those /// surfaces used implicitly before this fix. /// public static readonly CachingScriptMetadataResolver Instance = new(ScriptOptions.Default.MetadataResolver); private readonly MetadataReferenceResolver _inner; // Keyed on the assembly identity display name, compared case-insensitively to // match .NET assembly-name binding, which treats simple names case-insensitively. // A null VALUE is cached too: "not found" is a legitimate, sticky result. private readonly ConcurrentDictionary _missingByIdentity = new(StringComparer.OrdinalIgnoreCase); // Deliberately case-SENSITIVE (the tuple's default comparer): baseFilePath is a // filesystem path, and on the Linux containers these nodes run in, two paths // differing only in case are two different files. private readonly ConcurrentDictionary<(string Reference, string? BaseFilePath, MetadataReferenceProperties Properties), ImmutableArray> _referencesByArgs = new(); /// /// Creates a decorator over the given inner resolver. Exposed for tests; /// production code must NEVER call this constructor directly — always attach /// . The cache lives on the instance, so a second /// instance has a second (empty) cache and silently reintroduces the /// per-compile native-metadata leak on whichever surface uses it. /// /// The resolver whose results are memoized. public CachingScriptMetadataResolver(MetadataReferenceResolver inner) => _inner = inner; /// public override bool ResolveMissingAssemblies => _inner.ResolveMissingAssemblies; /// public override PortableExecutableReference? ResolveMissingAssembly( MetadataReference definition, AssemblyIdentity referenceIdentity) => _missingByIdentity.GetOrAdd( referenceIdentity.GetDisplayName(), _ => _inner.ResolveMissingAssembly(definition, referenceIdentity)); /// public override ImmutableArray ResolveReference( string reference, string? baseFilePath, MetadataReferenceProperties properties) => _referencesByArgs.GetOrAdd( (reference, baseFilePath, properties), _ => _inner.ResolveReference(reference, baseFilePath, properties)); /// public override bool Equals(object? other) => ReferenceEquals(this, other); /// public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); }