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);
}