You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Reactor core (src/Reactor/Core/ and src/Reactor/Hosting/) has hard type and method references to the Charting subsystem (and, more narrowly, to Docking). This violates the "core should know nothing about specific control families" boundary and has measurable downstream effects:
Architectural rot. New control families (charting today, docking partly, the next one tomorrow) need backdoor entry points into the core because we've established that "well, charting got one." Each one extends the surface area that the core has to keep stable.
Cold-start cost. Even when the runtime-deferral patterns work (the PushChartingState pattern in ReactorHost cleverly defers the cctor at runtime — but not the trim), there's a non-trivial maintenance cost in carrying those workarounds.
This issue tracks inverting these couplings so the core has zero references to any specific control family.
Enumerated coupling sites (current state)
Charting → core
src/Reactor/Hosting/ReactorHost.cs:67 — private Charting.ForcedColorsTheme? _forcedColorsTheme;
Field typed on a Charting type. Forces the trimmer to retain ForcedColorsTheme metadata even when no chart is ever mounted.
Three static setter calls on D3Charts. Reachable from RenderLoop. Forces D3Charts type + cctor; cctor reads D3Color.Category10 → forces D3Color (6.4 KB). The comment at line 426 notes the runtime deferral works but doesn't help the trimmer.
src/Reactor/Hosting/ReactorHost.cs:409,923 — direct calls to Charting.ForcedColorsTheme.FromSystem() from InitChartingState and the accessibility-settings change handler.
src/Reactor/Hosting/ChartingActivation.cs — exists solely as a chart-specific entry point into the host. Smaller concern (the file itself is tiny) but it's an example of the core defining a one-off contract for one subsystem.
The Path control's descriptor reaches into Charting for SVG path-data parsing. PathDataParser is conceptually general-purpose but it lives in the Charting namespace and creates a core-to-Charting coupling.
Docking → core
src/Reactor/Core/Element.cs:845-851 — Element record equality is special-cased for Docking.Native.DockSplitterElement and Docking.Native.DockDropTargetOverlayElement:
Same family — a docking-specific equality override in the core Element type.
Measured impact
Against the same samples/apps/hello-world-aot repro used in #497:
Reachable in retail (no charts, no devtools)
Bytes
Reason
Microsoft.UI.Reactor.Charting.D3.D3Color
6,583
D3Charts cctor reads D3Color.Category10
Microsoft.UI.Reactor.Charting.ForcedColorsTheme
1,027
ReactorHost._forcedColorsTheme field type
Microsoft.UI.Reactor.Charting.D3Charts
156
PushChartingState static setter calls
Charting direct total
~7.8 KB
The Charting.Accessibility.* namespace (ChartPalette, ChartSummarizer, etc.) does not appear in hello-world's mstat because nothing reachable invokes AccessibilityScanner's chart-checking arms. The moment an app turns on a11y scanning or instantiates a CanvasElement whose ChartData/CustomPalette slot is populated, the chain extends significantly (palette hardening, color-blind delta-E math, contrast ratio calculation).
Suggested fix shape
The general pattern is indirection at the seam — the core defines a minimal interface and Charting (or Docking) registers an implementation at startup. The interface lives in core; the implementation lives in the subsystem; trimmer sees no static reference to the concrete type from the core.
For Charting
// src/Reactor/Core/Internal/IChartingHostBridge.cs (new)internalinterfaceIChartingHostBridge{voidPushAccessibilityState(boolisForcedColors,boolisReducedMotion,object?forcedColorsThemePayload);}// src/Reactor/Hosting/ReactorHost.csprivatestaticIChartingHostBridge?s_chartingBridge;// set by Charting at startupprivateobject?_forcedColorsTheme;// becomes object?privatevoidPushChartingState()=>s_chartingBridge?.PushAccessibilityState(_isForcedColors,_isReducedMotion,_forcedColorsTheme);// src/Reactor/Charting/D3ChartsHostBridge.cs (new, lives in Charting)internalsealedclassD3ChartsHostBridge:IChartingHostBridge{ ...}// Registered once from ChartingActivation.RequestActivation
Similarly:
CanvasElement.ChartData/CustomPalette move into a separate ChartCanvasElement : CanvasElement (or attach via the existing Attached dictionary, which is already how the scanner reads ChartScannerHint).
AccessibilityScanner chart-checking logic moves out to Charting.Accessibility.ChartAccessibilityChecker and registers as an IScannerExtension.
PathDataParser either moves into core (it's general SVG-path parsing) or the PathDescriptor calls through a delegate.
For Docking
Element equality special-cases move into the Docking subsystem itself by overriding Equals/GetHashCode directly on DockSplitterElement/DockDropTargetOverlayElement (they're records — they can author their own equality without the core knowing about them).
Examine obj\x64\Release\net10.0-windows10.0.22621.0\win-x64\native\HelloWorldAot.mstat in sizoscope. Search for Charting and Docking namespaces. Today: 3 charting types reachable (~7.8 KB). After the fix: zero charting types should be reachable in a chart-free app.
(The csproj already has IlcGenerateMstatFile=true, IlcGenerateDgmlFile=true, PublishAot=true, StackTraceSupport=false, InvariantGlobalization=true, WindowsAppSDKSelfContained=false, ML-transitive exclusions, and the WindowsAppSDK#6394 .pri/.xbf copy target. See HelloWorldAot.csproj for the full property list.)
Validation checklist
After the indirection fix lands, the hello-world-aot mstat must show:
No type in Microsoft.UI.Reactor.Charting.* namespace is reachable (today: 3 types, ~7.8 KB).
docs/aot-support.md — should be updated alongside this work to call out the new "subsystems register into core via interfaces, not the other way around" rule
Summary
The Reactor core (
src/Reactor/Core/andsrc/Reactor/Hosting/) has hard type and method references to the Charting subsystem (and, more narrowly, to Docking). This violates the "core should know nothing about specific control families" boundary and has measurable downstream effects:D3Color,ForcedColorsTheme, and theD3Chartscctor cascade in their AOT-published binary (~7.6 KB measured insamples/apps/hello-world-aot, larger if the accessibility scanner runs). This is not subsumed by Devtools subsystem leaks ~1.5 MB into AOT retail publishes (10% of EXE) — needs FeatureGuard or package split #497 — it leaks regardless of whether devtools is trimmed out.PushChartingStatepattern inReactorHostcleverly defers the cctor at runtime — but not the trim), there's a non-trivial maintenance cost in carrying those workarounds.This issue tracks inverting these couplings so the core has zero references to any specific control family.
Enumerated coupling sites (current state)
Charting → core
src/Reactor/Hosting/ReactorHost.cs:67—private Charting.ForcedColorsTheme? _forcedColorsTheme;Field typed on a Charting type. Forces the trimmer to retain
ForcedColorsThememetadata even when no chart is ever mounted.src/Reactor/Hosting/ReactorHost.cs:430-434—PushChartingState():Three static setter calls on
D3Charts. Reachable fromRenderLoop. ForcesD3Chartstype + cctor; cctor readsD3Color.Category10→ forcesD3Color(6.4 KB). The comment at line 426 notes the runtime deferral works but doesn't help the trimmer.src/Reactor/Hosting/ReactorHost.cs:409,923— direct calls toCharting.ForcedColorsTheme.FromSystem()fromInitChartingStateand the accessibility-settings change handler.src/Reactor/Hosting/ChartingActivation.cs— exists solely as a chart-specific entry point into the host. Smaller concern (the file itself is tiny) but it's an example of the core defining a one-off contract for one subsystem.src/Reactor/Core/Element.cs:2900,2913—CanvasElementhas:A general-purpose
CanvasElementcarries chart-specific data slots. Forces both Charting types to be reachable whenever CanvasElement is reachable.src/Reactor/Core/AccessibilityScanner.cs:451-768— extensive chart-specific scanner logic (CheckChartTitle,CheckChartDescription,Charting.Accessibility.ChartPalette.ContrastRatio/Harden/MinColorblindDeltaE,ChartSummarizer.Summarize, hardcodednew Charting.D3.D3Color(...)light/dark backgrounds). The core accessibility scanner has chart accessibility logic baked in.src/Reactor/Core/V1Protocol/Descriptor/Descriptors/PathDescriptor.cs:188—The Path control's descriptor reaches into Charting for SVG path-data parsing.
PathDataParseris conceptually general-purpose but it lives in the Charting namespace and creates a core-to-Charting coupling.Docking → core
src/Reactor/Core/Element.cs:845-851—Elementrecord equality is special-cased forDocking.Native.DockSplitterElementandDocking.Native.DockDropTargetOverlayElement:Measured impact
Against the same
samples/apps/hello-world-aotrepro used in #497:Microsoft.UI.Reactor.Charting.D3.D3ColorD3Chartscctor readsD3Color.Category10Microsoft.UI.Reactor.Charting.ForcedColorsThemeReactorHost._forcedColorsThemefield typeMicrosoft.UI.Reactor.Charting.D3ChartsPushChartingStatestatic setter callsThe
Charting.Accessibility.*namespace (ChartPalette, ChartSummarizer, etc.) does not appear in hello-world's mstat because nothing reachable invokesAccessibilityScanner's chart-checking arms. The moment an app turns on a11y scanning or instantiates aCanvasElementwhoseChartData/CustomPaletteslot is populated, the chain extends significantly (palette hardening, color-blind delta-E math, contrast ratio calculation).Suggested fix shape
The general pattern is indirection at the seam — the core defines a minimal interface and Charting (or Docking) registers an implementation at startup. The interface lives in core; the implementation lives in the subsystem; trimmer sees no static reference to the concrete type from the core.
For Charting
Similarly:
CanvasElement.ChartData/CustomPalettemove into a separateChartCanvasElement : CanvasElement(or attach via the existingAttacheddictionary, which is already how the scanner readsChartScannerHint).AccessibilityScannerchart-checking logic moves out toCharting.Accessibility.ChartAccessibilityCheckerand registers as anIScannerExtension.PathDataParsereither moves into core (it's general SVG-path parsing) or thePathDescriptorcalls through a delegate.For Docking
Elementequality special-cases move into the Docking subsystem itself by overridingEquals/GetHashCodedirectly onDockSplitterElement/DockDropTargetOverlayElement(they're records — they can author their own equality without the core knowing about them).Reproduction setup (identical to #497)
Use
samples/apps/hello-world-aoton branchsamples/hello-world-aot. Publish:Examine
obj\x64\Release\net10.0-windows10.0.22621.0\win-x64\native\HelloWorldAot.mstatin sizoscope. Search forChartingandDockingnamespaces. Today: 3 charting types reachable (~7.8 KB). After the fix: zero charting types should be reachable in a chart-free app.(The csproj already has
IlcGenerateMstatFile=true,IlcGenerateDgmlFile=true,PublishAot=true,StackTraceSupport=false,InvariantGlobalization=true,WindowsAppSDKSelfContained=false, ML-transitive exclusions, and the WindowsAppSDK#6394 .pri/.xbf copy target. SeeHelloWorldAot.csprojfor the full property list.)Validation checklist
After the indirection fix lands, the
hello-world-aotmstat must show:Microsoft.UI.Reactor.Charting.*namespace is reachable (today: 3 types, ~7.8 KB).Microsoft.UI.Reactor.Docking.*namespace is reachable after Devtools subsystem leaks ~1.5 MB into AOT retail publishes (10% of EXE) — needs FeatureGuard or package split #497 also lands (today: 7 types reachable viaDevtoolsDockingTools— Devtools subsystem leaks ~1.5 MB into AOT retail publishes (10% of EXE) — needs FeatureGuard or package split #497 trimsDevtoolsDockingTools, this fix removes the residualElement.csequality coupling).grep -r "Charting\." src/Reactor/Core/ src/Reactor/Hosting/ReactorHost.csreturns zero non-comment hits.grep -r "Docking\." src/Reactor/Core/returns zero non-comment hits.Related
docs/aot-support.md— should be updated alongside this work to call out the new "subsystems register into core via interfaces, not the other way around" rule