Skip to content

[Feature] Inspector diagnostics: call-site column in SourceLocation + per-component ComponentRendered event #1326

Closed

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 the EffectsFlush spans, 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:col in ToString(): 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 to dotnet-trace/WPA.
  • Split state into own vs. descendant state: needs per-render closure allocations or changes at every hook invocation site, so it is left as a follow-up. state is documented to cover both.

Additional context

Confirmation

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions