3834400f05
Swap MemProbe's ProjectReference from Core.VirtualTags to Core.Scripting.Abstractions so the heavy-mode globalsType (ScriptGlobals<VirtualTagContext>) resolves from the post-A0 Roslyn-free assembly. Measured results (2026-06-07, N=50, Release): heavy (post-A0): 2.40 / 2.53 MiB/script (was ~18 MiB) lean: 1.64 / 1.65 MiB/script => heavy ≈ lean; both well under the 3 MiB gate. PASS.
85 lines
4.1 KiB
C#
85 lines
4.1 KiB
C#
// A0 result (2026-06-07): heavy 18.2 MiB -> 2.4 MiB/script; lean 1.65 MiB; ~7.6x drop
|
|
// (within noise of theoretical 11x). Proves globalsType-closure isolation: ScriptGlobals<VirtualTagContext>
|
|
// now resolves from Core.Scripting.Abstractions (Roslyn-free); heavy ≈ lean as expected.
|
|
// Pre-A0 baseline: heavy ~18 MiB (Core.Scripting + Core.VirtualTags both -> Roslyn).
|
|
// Post-A0 (this run): heavy 2.40/2.53 MiB, lean 1.64/1.65 MiB — both well under the 3 MiB gate.
|
|
|
|
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
|
using Microsoft.CodeAnalysis.Scripting;
|
|
|
|
// Memory measurement probe for Roslyn C# scripting per dotnet/roslyn#22219.
|
|
// Compiles + RETAINS N distinct scripts (like the prod compiled-delegate cache) and
|
|
// measures the per-script working-set cost. The ONLY thing that varies between "heavy"
|
|
// and "lean" is the globalsType's assembly closure:
|
|
// heavy = VirtualTagContext (closure pulls in Roslyn via Core.Scripting)
|
|
// lean = LeanCtx (closure = {LeanContext, Core.Abstractions} only)
|
|
|
|
static long Rss() => System.Diagnostics.Process.GetCurrentProcess().WorkingSet64;
|
|
|
|
static void Settle()
|
|
{
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
GC.Collect(2, GCCollectionMode.Forced, true, true);
|
|
GC.WaitForPendingFinalizers();
|
|
}
|
|
}
|
|
|
|
var mode = args.Length > 0 ? args[0] : "heavy";
|
|
int N = args.Length > 1 && int.TryParse(args[1], out var n) ? n : 50;
|
|
|
|
// The globalsType is what Roslyn's reference manager loads the transitive closure of
|
|
// (dotnet/roslyn#22219). We mirror production's choice: prod uses the WRAPPER
|
|
// ScriptGlobals<TContext> (which exposes the named `ctx` property), NOT the raw context.
|
|
// heavy = ScriptGlobals<VirtualTagContext> -> AFTER A0: both wrapper + generic arg now
|
|
// live in Core.Scripting.Abstractions (Roslyn-free); closure = lean.
|
|
// lean = LeanGlobals -> closure {LeanContext, Core.Abstractions},
|
|
// NO Roslyn. This is the A0 "globals type in a lean assembly" treatment.
|
|
var globalsType = mode == "lean"
|
|
? typeof(LeanContext.LeanGlobals)
|
|
: typeof(ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptGlobals<ZB.MOM.WW.OtOpcUa.Core.VirtualTags.VirtualTagContext>);
|
|
|
|
// The script reads ctx.GetTag("x").Value. We must reference: the globalsType's own
|
|
// assembly, the assemblies of its generic type arguments (so `ctx`'s property type
|
|
// resolves), and Core.Abstractions (DataValueSnapshot, the return type of GetTag).
|
|
// References are minimal + identical in spirit for both modes; the real difference is
|
|
// the transitive UNMANAGED closure of the globalsType's assembly that Roslyn's reference
|
|
// manager loads per compilation (the #22219 effect).
|
|
var snapshotAssembly = typeof(ZB.MOM.WW.OtOpcUa.Core.Abstractions.DataValueSnapshot).Assembly;
|
|
var refAssemblies = new System.Collections.Generic.HashSet<System.Reflection.Assembly>
|
|
{
|
|
globalsType.Assembly,
|
|
snapshotAssembly,
|
|
};
|
|
foreach (var ga in globalsType.GetGenericArguments())
|
|
refAssemblies.Add(ga.Assembly);
|
|
var opts = ScriptOptions.Default
|
|
.WithReferences(refAssemblies)
|
|
.WithImports();
|
|
|
|
// Warm up Roslyn once (compile 1 throwaway) so the baseline excludes one-time Roslyn init.
|
|
_ = CSharpScript.Create<object>("return 0;", opts, globalsType).GetCompilation().GetDiagnostics();
|
|
Settle();
|
|
|
|
long baseRss = Rss();
|
|
long baseGc = GC.GetTotalMemory(true);
|
|
|
|
var held = new System.Collections.Generic.List<object>(N);
|
|
for (int i = 0; i < N; i++)
|
|
{
|
|
var src = $"return ctx.GetTag(\"ref_{i}\").Value;";
|
|
var script = CSharpScript.Create<object>(src, opts, globalsType);
|
|
script.Compile(); // force compilation / emit
|
|
held.Add(script.CreateDelegate()); // retain the compiled delegate (like the prod cache)
|
|
}
|
|
|
|
Settle();
|
|
long afterRss = Rss();
|
|
long afterGc = GC.GetTotalMemory(true);
|
|
GC.KeepAlive(held);
|
|
|
|
Console.WriteLine($"MODE={mode} N={N}");
|
|
Console.WriteLine($" baseline RSS={baseRss / 1048576.0:F1} MiB managed={baseGc / 1048576.0:F1} MiB");
|
|
Console.WriteLine($" afterN RSS={afterRss / 1048576.0:F1} MiB managed={afterGc / 1048576.0:F1} MiB");
|
|
Console.WriteLine($" PER-SCRIPT: RSS={(afterRss - baseRss) / 1048576.0 / N:F2} MiB/script managed={(afterGc - baseGc) / 1048576.0 / N:F2} MiB/script");
|