Repository navigation
[Feature] Inspector diagnostics: call-site column in SourceLocation + per-component ComponentRendered event #1326
Description
Problem
External WinUI inspectors (winapp DevTools' Reactor support) need two pieces of diagnostics Reactor does not expose today:
1. The call-site column. Element.CallSite / ReactorSourceMap.GetSource return SourceLocation(FilePath, LineNumber) only. An inspector cannot tell several factory calls on one line apart — Row(Button("a"), Button("b")) yields two identical locations — and cannot anchor "go to source" or a review comment on the exact call. SourceLocation's own doc comment says the column was left out only to match the [CallerLineNumber] route, and that "a Roslyn-interceptor provider does know the column and can add it later as a purely additive member". Spec 010 retired that CallerInfo route; only the interceptor generator ships, and SourceMapInterceptorGenerator already holds the mapped FileLinePositionSpan and discards Character.
2. A per-component render event that carries a reason. A "highlight updates when components render" overlay (React DevTools-style) and why-did-render diagnostics need every component render, why it happened, and which on-screen component it was. ReactorEventSource has an Informational ComponentRenderStart/Stop timing pair, but it:
- fires only on the reconciler's update path (not on mount, not for a host's root render);
- reports only
trigger = "self" | "parent"(no props/context/hot-reload distinction); - carries no instance identity, so an inspector cannot find the control to flash;
- is Informational under
Render, together with theEffectsFlushspans, so an overlay that enables it pays four Stopwatch-timed events per render.
ReactorFeatureFlags.HighlightReconcileChanges and Reconciler.LastMounted/ModifiedElements highlight controls touched by reconcile, not component renders; they draw Reactor's own overlay and need the Reconciler instance, so an external inspector cannot consume them as a render signal.
Verified on origin/main @ 03026eb18.
Proposed solution
Column — ColumnNumber becomes the third positional member of SourceLocation (1-based, 0 = unknown). Updated during review: the maintainer chose the positional shape over an init-only property plus an extra constructor, because SourceLocation has only shipped in the 0.1.0-preview.16 prerelease. ToString() (file:line) and ToShortString() stay unchanged, and the struct stays 16 bytes on 64-bit. The generator stamps the column of the invoked method's name. The line already follows the open paren (for [CallerLineNumber] parity), so when the name and paren sit on different lines the paren's column is used and (line, column) always names a real position. Argument-position stamps (implicit string → Element) use the argument's column.
Render event — ComponentRendered, EventId 40 (next free), EventLevel.Verbose, keywords Render | RenderDetail (new 0x4000). The payload is componentName, componentId (process-unique long, stable across re-renders), reason, and elapsedMicroseconds. reason is one of mount, state, props, context, parent, hotReload, forced, derived from state the reconciler already computes. Two new methods, ReactorTrace.GetComponentControl(long) and ReactorTrace.TryGetComponentId(UIElement, out long), map an id to the component's wrapper control and back. The mapping is populated only while the event is enabled and dropped on unmount. When nothing is listening, the cost is one IsEnabled check per component render.
public readonly record struct SourceLocation(string FilePath, int LineNumber, int ColumnNumber);
public static class ReactorTrace
{
public static UIElement? GetComponentControl(long componentId);
public static bool TryGetComponentId(UIElement control, out long componentId);
}Alternatives considered
Column:
- Open-paren column (mirrors the line rule exactly): lands the caret after the name and reads oddly as a review anchor.
- Invocation-start column: for
Factories.TextBlock(...)that is the receiver, not the call. - Additive init-only property plus a three-argument constructor (the original proposal): avoids changing the positional shape, but keeps two ways to build a location. Not worth it for a type that has only shipped in a prerelease.
file:line:colinToString(): breaks consumers that split on the last:.- Start+end span: not needed to disambiguate; it can be added additively later.
Render event:
- Extend
ComponentRenderStart/Stop: it is Informational, two events per render, and shares a keyword with the timed spans. Changing its emission sites would also change what existing perf traces measure. - Identity by call site (file:line:col): ambiguous for lists/
ForEach, needs source mapping, and puts paths into ETW. - In-process managed callback instead of ETW: a second diagnostics channel beside
ReactorTrace, invisible todotnet-trace/WPA. - Split
stateinto own vs. descendant state: needs per-render closure allocations or changes at every hook invocation site, so it is left as a follow-up.stateis documented to cover both.
Additional context
- Part of the winapp DevTools Reactor support that [Feature] Retire Reactor's built-in DevTools in favour of the WinUI DevTools (winapp) #1319 depends on.
- Amends
docs/specs/010-source-mapping-design.md(§1.1 "As shipped"). - Related: Diagnostics: enumerate Reactor hosts and their roots; tag component boundaries for inspectors #1318 (enumerate hosts/roots, tag component boundaries for inspectors) and Name the component, not its element, in RenderError — and report mount, host-root and ErrorBoundary-caught render errors #1323 (name the component in
RenderError). Two small follow-ups build on this change plus one of those: stamping the column on root mount sites (needs Diagnostics: enumerate Reactor hosts and their roots; tag component boundaries for inspectors #1318) and namingComponentRenderedcomponents the same wayRenderErrordoes (needs Name the component, not its element, in RenderError — and report mount, host-root and ErrorBoundary-caught render errors #1323). They will land once both prerequisites are merged.
Confirmation
- I have searched existing issues and specs for prior discussion of this idea.
Activity
Report ComponentRendered on every error path; changelog
Inspector diagnostics: call-site column in SourceLocation + Component…
Inspector diagnostics without a managed agent: ReactorSource on every…