Skip to content

Commit 33167d9

Browse files
AleksanderBodurrialxhub
authored andcommitted
refactor(core): implement experimental getSignalGraph debug API (#57074)
Creates a debug api that returns an arrays of nodes and edges that represents a signal graph in the context of a particular injector. Starts by discovering the consumer nodes for each injector, and then traverses their dependencies to discover each producer. PR Close #57074
1 parent 47b4b3e commit 33167d9

File tree

8 files changed

+557
-6
lines changed

8 files changed

+557
-6
lines changed

packages/core/src/render3/debug/framework_injector_profiler.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {getComponentDef} from '../def_getters';
1515
import {getNodeInjectorLView, getNodeInjectorTNode, NodeInjector} from '../di';
1616
import {TNode} from '../interfaces/node';
1717
import {LView} from '../interfaces/view';
18+
import {EffectRef} from '../reactivity/effect';
1819

1920
import {
2021
InjectedService,
@@ -67,6 +68,7 @@ class DIDebugData {
6768
WeakMap<Type<unknown>, InjectedService[]>
6869
>();
6970
resolverToProviders = new WeakMap<Injector | TNode, ProviderRecord[]>();
71+
resolverToEffects = new WeakMap<Injector | LView, EffectRef[]>();
7072
standaloneInjectorToComponent = new WeakMap<Injector, Type<unknown>>();
7173

7274
reset() {
@@ -113,9 +115,26 @@ function handleInjectorProfilerEvent(injectorProfilerEvent: InjectorProfilerEven
113115
handleInstanceCreatedByInjectorEvent(context, injectorProfilerEvent.instance);
114116
} else if (type === InjectorProfilerEventType.ProviderConfigured) {
115117
handleProviderConfiguredEvent(context, injectorProfilerEvent.providerRecord);
118+
} else if (type === InjectorProfilerEventType.EffectCreated) {
119+
handleEffectCreatedEvent(context, injectorProfilerEvent.effect);
116120
}
117121
}
118122

123+
function handleEffectCreatedEvent(context: InjectorProfilerContext, effect: EffectRef): void {
124+
const diResolver = getDIResolver(context.injector);
125+
if (diResolver === null) {
126+
throwError('An EffectCreated event must be run within an injection context.');
127+
}
128+
129+
const {resolverToEffects} = frameworkDIDebugData;
130+
131+
if (!resolverToEffects.has(diResolver)) {
132+
resolverToEffects.set(diResolver, []);
133+
}
134+
135+
resolverToEffects.get(diResolver)!.push(effect);
136+
}
137+
119138
/**
120139
*
121140
* Stores the injected service in frameworkDIDebugData.resolverToTokenToDependencies

packages/core/src/render3/debug/injector_profiler.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {Type} from '../../interface/type';
1616
import {throwError} from '../../util/assert';
1717
import type {TNode} from '../interfaces/node';
1818
import type {LView} from '../interfaces/view';
19+
import type {EffectRef} from '../reactivity/effect';
1920

2021
/**
2122
* An enum describing the types of events that can be emitted from the injector profiler
@@ -35,6 +36,11 @@ export const enum InjectorProfilerEventType {
3536
* Emits when an injector configures a provider.
3637
*/
3738
ProviderConfigured,
39+
40+
/**
41+
* Emits when an effect is created.
42+
*/
43+
EffectCreated,
3844
}
3945

4046
/**
@@ -74,14 +80,21 @@ export interface ProviderConfiguredEvent {
7480
providerRecord: ProviderRecord;
7581
}
7682

83+
export interface EffectCreatedEvent {
84+
type: InjectorProfilerEventType.EffectCreated;
85+
context: InjectorProfilerContext;
86+
effect: EffectRef;
87+
}
88+
7789
/**
7890
* An object representing an event that is emitted through the injector profiler
7991
*/
8092

8193
export type InjectorProfilerEvent =
8294
| InjectedServiceEvent
8395
| InjectorCreatedInstanceEvent
84-
| ProviderConfiguredEvent;
96+
| ProviderConfiguredEvent
97+
| EffectCreatedEvent;
8598

8699
/**
87100
* An object that contains information about a provider that has been configured
@@ -272,6 +285,16 @@ export function emitInjectEvent(token: Type<unknown>, value: unknown, flags: Inj
272285
});
273286
}
274287

288+
export function emitEffectCreatedEvent(effect: EffectRef): void {
289+
!ngDevMode && throwError('Injector profiler should never be called in production mode');
290+
291+
injectorProfiler({
292+
type: InjectorProfilerEventType.EffectCreated,
293+
context: getInjectorProfilerContext(),
294+
effect,
295+
});
296+
}
297+
275298
export function runInInjectorProfilerContext(
276299
injector: Injector,
277300
token: Type<unknown>,

packages/core/src/render3/reactive_lview_consumer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function maybeReturnReactiveLViewConsumer(consumer: ReactiveLViewConsumer
4848
freeConsumers.push(consumer);
4949
}
5050

51-
const REACTIVE_LVIEW_CONSUMER_NODE: Omit<ReactiveLViewConsumer, 'lView'> = {
51+
export const REACTIVE_LVIEW_CONSUMER_NODE: Omit<ReactiveLViewConsumer, 'lView'> = {
5252
...REACTIVE_NODE,
5353
consumerIsAlwaysLive: true,
5454
kind: 'template',
@@ -78,7 +78,7 @@ export function getOrCreateTemporaryConsumer(lView: LView): ReactiveLViewConsume
7878
return consumer;
7979
}
8080

81-
const TEMPORARY_CONSUMER_NODE = {
81+
export const TEMPORARY_CONSUMER_NODE = {
8282
...REACTIVE_NODE,
8383
consumerIsAlwaysLive: true,
8484
kind: 'template',

packages/core/src/render3/reactivity/effect.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {assertInInjectionContext} from '../../di/contextual';
2727
import {DestroyRef, NodeInjectorDestroyRef} from '../../linker/destroy_ref';
2828
import {ViewContext} from '../view_context';
2929
import {noop} from '../../util/noop';
30-
import {ErrorHandler} from '../../error_handler';
3130
import {
3231
ChangeDetectionScheduler,
3332
NotificationSource,
@@ -38,6 +37,7 @@ import {USE_MICROTASK_EFFECT_BY_DEFAULT} from './patch';
3837
import {microtaskEffect} from './microtask_effect';
3938

4039
let useMicrotaskEffectsByDefault = USE_MICROTASK_EFFECT_BY_DEFAULT;
40+
import {emitEffectCreatedEvent, setInjectorProfilerContext} from '../debug/injector_profiler';
4141

4242
/**
4343
* Toggle the flag on whether to use microtask effects (for testing).
@@ -60,7 +60,7 @@ export interface EffectRef {
6060
destroy(): void;
6161
}
6262

63-
class EffectRefImpl implements EffectRef {
63+
export class EffectRefImpl implements EffectRef {
6464
[SIGNAL]: EffectNode;
6565

6666
constructor(node: EffectNode) {
@@ -199,11 +199,19 @@ export function effect(
199199
node.onDestroyFn = destroyRef.onDestroy(() => node.destroy());
200200
}
201201

202+
const effectRef = new EffectRefImpl(node);
203+
202204
if (ngDevMode) {
203205
node.debugName = options?.debugName ?? '';
206+
const prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null});
207+
try {
208+
emitEffectCreatedEvent(effectRef);
209+
} finally {
210+
setInjectorProfilerContext(prevInjectorProfilerContext);
211+
}
204212
}
205213

206-
return new EffectRefImpl(node);
214+
return effectRef;
207215
}
208216

209217
export interface EffectNode extends ReactiveNode, SchedulableEffect {

packages/core/src/render3/util/global_utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
getInjectorProviders,
3030
getInjectorResolutionPath,
3131
} from './injector_discovery_utils';
32+
import {getSignalGraph} from './signal_debug';
3233

3334
/**
3435
* This file introduces series of globally accessible debug tools
@@ -65,6 +66,7 @@ const globalUtilsFunctions = {
6566
'ɵgetInjectorResolutionPath': getInjectorResolutionPath,
6667
'ɵgetInjectorMetadata': getInjectorMetadata,
6768
'ɵsetProfiler': setProfiler,
69+
'ɵgetSignalGraph': getSignalGraph,
6870

6971
'getDirectiveMetadata': getDirectiveMetadata,
7072
'getComponent': getComponent,
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
import {
9+
REACTIVE_LVIEW_CONSUMER_NODE,
10+
ReactiveLViewConsumer,
11+
TEMPORARY_CONSUMER_NODE,
12+
} from '../reactive_lview_consumer';
13+
import {assertTNode, assertLView} from '../assert';
14+
import {getFrameworkDIDebugData} from '../debug/framework_injector_profiler';
15+
import {NodeInjector, getNodeInjectorTNode, getNodeInjectorLView} from '../di';
16+
import {REACTIVE_TEMPLATE_CONSUMER, HOST, LView} from '../interfaces/view';
17+
import {EffectNode, EffectRefImpl, ROOT_EFFECT_NODE, VIEW_EFFECT_NODE} from '../reactivity/effect';
18+
import {Injector} from '../../di/injector';
19+
import {R3Injector} from '../../di/r3_injector';
20+
import {throwError} from '../../util/assert';
21+
import {
22+
ComputedNode,
23+
ReactiveNode,
24+
SIGNAL,
25+
SIGNAL_NODE,
26+
SignalNode,
27+
} from '@angular/core/primitives/signals';
28+
29+
export interface DebugSignalGraphNode {
30+
kind: string;
31+
label?: string;
32+
value?: unknown;
33+
}
34+
35+
export interface DebugSignalGraphEdge {
36+
/**
37+
* Index of a signal node in the `nodes` array that is a consumer of the signal produced by the producer node.
38+
*/
39+
consumer: number;
40+
41+
/**
42+
* Index of a signal node in the `nodes` array that is a producer of the signal consumed by the consumer node.
43+
*/
44+
producer: number;
45+
}
46+
47+
/**
48+
* A debug representation of the signal graph.
49+
*/
50+
export interface DebugSignalGraph {
51+
nodes: DebugSignalGraphNode[];
52+
edges: DebugSignalGraphEdge[];
53+
}
54+
55+
function isComputedNode(node: ReactiveNode): node is ComputedNode<unknown> {
56+
return node.kind === 'computed';
57+
}
58+
59+
function isTemplateEffectNode(node: ReactiveNode): node is ReactiveLViewConsumer {
60+
return node.kind === 'template';
61+
}
62+
63+
function isEffectNode(node: ReactiveNode): node is EffectNode {
64+
return node.kind === 'effect';
65+
}
66+
67+
function isSignalNode(node: ReactiveNode): node is SignalNode<unknown> {
68+
return node.kind === 'signal';
69+
}
70+
71+
/**
72+
*
73+
* @param injector
74+
* @returns Template consumer of given NodeInjector
75+
*/
76+
function getTemplateConsumer(injector: NodeInjector): ReactiveLViewConsumer | null {
77+
const tNode = getNodeInjectorTNode(injector)!;
78+
assertTNode(tNode);
79+
const lView = getNodeInjectorLView(injector)!;
80+
assertLView(lView);
81+
const templateLView = lView[tNode.index]!;
82+
assertLView(templateLView);
83+
84+
return templateLView[REACTIVE_TEMPLATE_CONSUMER];
85+
}
86+
87+
function getNodesAndEdgesFromSignalMap(signalMap: ReadonlyMap<ReactiveNode, ReactiveNode[]>): {
88+
nodes: DebugSignalGraphNode[];
89+
edges: DebugSignalGraphEdge[];
90+
} {
91+
const nodes = Array.from(signalMap.keys());
92+
const debugSignalGraphNodes: DebugSignalGraphNode[] = [];
93+
const edges: DebugSignalGraphEdge[] = [];
94+
95+
for (const [consumer, producers] of signalMap.entries()) {
96+
const consumerIndex = nodes.indexOf(consumer);
97+
98+
// collect node
99+
if (isComputedNode(consumer) || isSignalNode(consumer)) {
100+
debugSignalGraphNodes.push({
101+
label: consumer.debugName,
102+
value: consumer.value,
103+
kind: consumer.kind,
104+
});
105+
} else if (isTemplateEffectNode(consumer)) {
106+
debugSignalGraphNodes.push({
107+
label: consumer.debugName ?? consumer.lView?.[HOST]?.tagName?.toLowerCase?.(),
108+
kind: consumer.kind,
109+
});
110+
} else if (isEffectNode(consumer)) {
111+
debugSignalGraphNodes.push({
112+
label: consumer.debugName,
113+
kind: consumer.kind,
114+
});
115+
} else {
116+
debugSignalGraphNodes.push({
117+
label: consumer.debugName,
118+
kind: consumer.kind,
119+
});
120+
}
121+
122+
// collect edges for node
123+
for (const producer of producers) {
124+
edges.push({consumer: consumerIndex, producer: nodes.indexOf(producer)});
125+
}
126+
}
127+
128+
return {nodes: debugSignalGraphNodes, edges};
129+
}
130+
131+
function extractEffectsFromInjector(injector: Injector): ReactiveNode[] {
132+
let diResolver: Injector | LView<unknown> = injector;
133+
if (injector instanceof NodeInjector) {
134+
const lView = getNodeInjectorLView(injector)!;
135+
diResolver = lView;
136+
}
137+
138+
const resolverToEffects = getFrameworkDIDebugData().resolverToEffects as Map<
139+
Injector | LView<unknown>,
140+
EffectRefImpl[]
141+
>;
142+
const effects = resolverToEffects.get(diResolver) ?? [];
143+
144+
return effects.map((effect: EffectRefImpl) => effect[SIGNAL]);
145+
}
146+
147+
function extractSignalNodesAndEdgesFromRoots(
148+
nodes: ReactiveNode[],
149+
signalDependenciesMap: Map<ReactiveNode, ReactiveNode[]> = new Map(),
150+
): Map<ReactiveNode, ReactiveNode[]> {
151+
for (const node of nodes) {
152+
if (signalDependenciesMap.has(node)) {
153+
continue;
154+
}
155+
156+
const producerNodes = (node.producerNode ?? []) as ReactiveNode[];
157+
signalDependenciesMap.set(node, producerNodes);
158+
extractSignalNodesAndEdgesFromRoots(producerNodes, signalDependenciesMap);
159+
}
160+
161+
return signalDependenciesMap;
162+
}
163+
164+
/**
165+
* Returns a debug representation of the signal graph for the given injector.
166+
*
167+
* Currently only supports element injectors. Starts by discovering the consumer nodes
168+
* and then traverses their producer nodes to build the signal graph.
169+
*
170+
* @param injector The injector to get the signal graph for.
171+
* @returns A debug representation of the signal graph.
172+
* @throws If the injector is an environment injector.
173+
*/
174+
export function getSignalGraph(injector: Injector): DebugSignalGraph {
175+
let templateConsumer: ReactiveLViewConsumer | null = null;
176+
177+
if (!(injector instanceof NodeInjector) && !(injector instanceof R3Injector)) {
178+
return throwError('getSignalGraph must be called with a NodeInjector or R3Injector');
179+
}
180+
181+
if (injector instanceof NodeInjector) {
182+
templateConsumer = getTemplateConsumer(injector as NodeInjector);
183+
}
184+
185+
const nonTemplateEffectNodes = extractEffectsFromInjector(injector);
186+
187+
const signalNodes = templateConsumer
188+
? [templateConsumer, ...nonTemplateEffectNodes]
189+
: nonTemplateEffectNodes;
190+
191+
const signalDependenciesMap = extractSignalNodesAndEdgesFromRoots(signalNodes);
192+
193+
return getNodesAndEdgesFromSignalMap(signalDependenciesMap);
194+
}

0 commit comments

Comments
 (0)