docs(design): VirtualTag/script memory scalability (A0+A+guardrail; C2 deferred) + measurement harness
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
*/bin/
|
||||
*/obj/
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<!-- Throwaway memory probe: keep build noise low. -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Closure of THIS assembly = {LeanContext, Core.Abstractions}. No Roslyn. -->
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,36 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace LeanContext;
|
||||
|
||||
/// <summary>
|
||||
/// LEAN globals type for the memory probe. Its transitive reference closure is only
|
||||
/// {LeanContext, Core.Abstractions} — deliberately NO Roslyn — so the per-script cost
|
||||
/// of the Roslyn reference-manager loading the globalsType's closure (dotnet/roslyn#22219)
|
||||
/// can be measured against the heavy <c>VirtualTagContext</c>, whose closure pulls in
|
||||
/// Microsoft.CodeAnalysis.CSharp.Scripting.
|
||||
/// <para>
|
||||
/// <see cref="GetTag"/> returns the same <see cref="DataValueSnapshot"/> type that
|
||||
/// <c>VirtualTagContext.GetTag</c> returns, so the probe's script source
|
||||
/// (<c>ctx.GetTag("x").Value</c>) is byte-identical for both modes — the ONLY
|
||||
/// difference is which assembly closure the globalsType lives in.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class LeanCtx
|
||||
{
|
||||
private readonly System.Collections.Generic.Dictionary<string, DataValueSnapshot> _d = new();
|
||||
|
||||
public DataValueSnapshot GetTag(string p) =>
|
||||
_d.TryGetValue(p, out var v) ? v : new DataValueSnapshot(null, 0u, null, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LEAN analogue of the prod <c>ScriptGlobals<TContext></c> wrapper: exposes a
|
||||
/// named <c>ctx</c> property so the script source can be byte-identical to the heavy
|
||||
/// path (<c>ctx.GetTag(...).Value</c>). Lives in the LeanContext assembly, so its
|
||||
/// reference closure is {LeanContext, Core.Abstractions} — NO Roslyn. This is the A0
|
||||
/// "globals type in a lean assembly" treatment.
|
||||
/// </summary>
|
||||
public sealed class LeanGlobals
|
||||
{
|
||||
public LeanCtx ctx { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Same Roslyn version the repo pins (Directory.Packages.props => 4.12.0, CPM). -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LeanContext\LeanContext.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Core.VirtualTags\ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,78 @@
|
||||
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> -> wrapper lives in Core.Scripting AND the
|
||||
// generic arg VirtualTagContext lives in Core.VirtualTags; both -> Roslyn.
|
||||
// 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");
|
||||
Reference in New Issue
Block a user