using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// Process-wide cache of site-side script s, keyed on /// the globals type plus a SHA-256 hash of the script code. Unlike the TemplateEngine's /// ScriptCompileVerdictCache (which caches central-validation verdicts), this /// caches the full compiled result — the immutable Roslyn Script<object?> itself — /// so the synchronous deploy gate and the InstanceActor.PreStart recompile share one /// Roslyn compile per script body per process lifetime (N4). /// /// Soundness rests on three invariants: /// /// A compile result is a pure function of (code, globals type, compile-time-static /// trust policy) — none of which vary at runtime for a given key. /// The stored error text is name-free (Roslyn Diagnostic.GetMessage() / /// "Compilation exception: …"); callers prefix their own script-name context, so a /// shared cached failure renders correctly for every caller. Failures are cached too — /// a bad script body fails identically for every caller. /// Roslyn Script<T> is immutable and thread-safe (RunAsync creates /// per-run state), so one compiled instance may be shared across actors and threads. /// /// /// /// Bounded at entries — deliberately smaller than the verdict cache's /// 4096 because entries pin compiled assemblies, not just verdict strings. On overflow the cache /// is cleared wholesale (results are recomputable, so a coarse reset avoids eviction bookkeeping). /// // are exposed for tests and diagnostics. /// /// internal static class SiteScriptCompileCache { /// Upper bound on cached entries; the cache is cleared wholesale on overflow. internal const int MaxEntries = 1024; private static readonly ConcurrentDictionary Cache = new(); private static long _hits; /// Number of cache hits observed since the last . public static long Hits => Interlocked.Read(ref _hits); /// Current number of cached compile results. public static int Count => Cache.Count; /// /// Returns the cached compile result for against /// , or computes it via and caches /// it on a miss. A hit increments . Both success and failure results are /// cached — the error text is name-free by construction. /// /// The script source code to look up (hashed to form the cache key). /// The Roslyn globals surface the script compiles against; part of the key so identical source under different globals stays distinct. /// Computes the compile result on a cache miss. /// The cached or newly computed compile result. public static ScriptCompilationResult GetOrAdd(string code, Type globalsType, Func factory) { var key = globalsType.FullName + ":" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(code))); if (Cache.TryGetValue(key, out var result)) { Interlocked.Increment(ref _hits); return result; } result = factory(); // Coarse bound: on overflow drop everything rather than track evictions. if (Cache.Count >= MaxEntries) Cache.Clear(); Cache[key] = result; return result; } /// Clears all cached compile results and resets the hit counter. public static void Clear() { Cache.Clear(); Interlocked.Exchange(ref _hits, 0); } }