[FEATURE] Debug render tree: helper nodes and reactivity introspection#21484
Open
NullVoxPopuli-ai-agent wants to merge 1 commit into
Open
Conversation
Debug tooling (notably the Ember Inspector) has two long-standing gaps
it currently works around by patching VM internals from the outside:
helpers never appear in the debug render tree, and there is no way to
tell how often a render node re-rendered or which of its arguments
caused the most recent re-render.
Helper nodes ('helper'):
- Template helper invocations register debug render tree nodes in both
the resolved (VM_HELPER_OP) and dynamic/curried (VM_DYNAMIC_HELPER_OP)
paths, DEBUG-only and only when the tree is active.
- Helper nodes have no DOM bounds; didRender/captureBounds accept null
bounds for them.
- Only user-observable helpers are captured: VM-internal helpers (fn,
hash, array, ...) are excluded via an isInternalHelper marker that
Ember's own internal helpers (the machinery behind {{outlet}} etc.)
now share, and references without a real debug name are treated as
implementation details.
- References now carry an untransformed debugName (the raw label they
were created with) so helper nodes can be labeled; the helper's
update opcodes and a destructor keep the node in sync with its
surrounding block.
Reactivity introspection (CapturedRenderNode.reactivity):
- The tree records, per node, how many times it updated and the global
revision at its last two renders.
- Each capture walks the node's argument references (after the existing
reifyArgsDebug pass, so revisions are current) and reports debugLabel,
revision, and a changed flag: whether that argument was invalidated
since the node's previous render - i.e. whether it caused the most
recent re-render.
The shared DebugRenderTreeUpdateOpcode/DebugRenderTreeDidRenderOpcode
moved from component.ts into their own module so the helper opcodes can
use them too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Debug tooling — most concretely the Ember Inspector — currently has to answer two questions by monkeypatching VM internals from the outside, which is fragile and increasingly impossible (module namespaces are read-only in modern builds):
emberjs/ember-inspector#2776 ships "why did this render?" introspection built on instance-level patches of the debug render tree (
create/update/captureNode). This PR moves those capabilities where they belong — into the VM — so the inspector (and any other tooling) can consume plain captured data instead of patching. It also closes the helper gap, which was impossible to patch in from the outside.All new work is DEBUG-gated and only active when the debug render tree exists (
_DEBUG_RENDER_TREE), same as the existing component/modifier bookkeeping.Helper nodes (
type: 'helper')Template helper invocations now register debug render tree nodes, in both invocation paths:
VM_HELPER_OP): the produced reference is the node bucket; nodes getcreate/didRenderat append,DebugRenderTreeUpdateOpcode/DidRenderOpcodefor re-renders, and a destructor tied to the surrounding block.VM_DYNAMIC_HELPER_OP): same lifecycle keyed on the helper instance reference, named from the callee reference'sdebugLabel(the lexical name in strict-mode templates).Two deliberate exclusions keep the tree meaningful:
fn,hash,array, …) are excluded via a newisInternalHelpermarker ininternalHelper. Ember's own internal helpers (the machinery behind{{outlet}}etc.) now register through the same function, so they get the same treatment — they're implementation details, not user-observable invocations. (This is also load-bearing: capturing the anonymous outlet-machinery reference as a tree bucket breaks outlet rendering.)debugNamealongside the display-orienteddebugLabel((result of a \x` helper)`).Helper nodes have no DOM bounds —
didRenderandcaptureBoundsacceptnullbounds for them;CapturedRenderNode.boundswas already nullable.Reactivity introspection (
CapturedRenderNode.reactivity)Every captured node now reports:
create/updaterecord the revision bookkeeping (valueForTag(CURRENT_TAG)reads — no tag consumption).captureNodewalks the node's argument references after the existingreifyArgsDebugpass, so revisions are current;changedis pure revision arithmetic: an argument caused the most recent re-render iff its revision is greater than the node's previous render revision.This applies to components, modifiers, keywords, and the new helper nodes uniformly — e.g. a helper's captured node directly shows which of its inputs invalidated it.
Implementation notes
DebugRenderTreeUpdateOpcode/DebugRenderTreeDidRenderOpcodemoved out ofcomponent.tsintocompiled/opcodes/debug-render-tree.tsso the helper opcodes can share them (no behavior change).lastRevisionForRef(ref)(revision accessor for the debug render tree) and thedebugNamefield described above.reactivityis a new field, and the existing per-field test matchers pass unchanged.Relationship to other work
EnvironmentDelegatevs.DebugRenderTreeImpl+ opcodes) and should merge independently.create/update/captureNodeinstance patches and readreactivitystraight off captured nodes. Helper nodes additionally unlock helper-level display in the inspector, which is impossible today.Testing
@glimmer-workspace/integration-tests(debug render treesuite): helper node capture (args, null bounds, update count, changed-argument flagging across a re-render) and reactivity introspection on component nodes.Application test: debug render tree: user helpers captured in loose-mode templates while{{array}}and the outlet machinery are not.vite build+testem ci).🤖 Generated with Claude Code