8000 refactor(core): implement experimental getSignalGraph debug API by AleksanderBodurri · Pull Request #57074 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

refactor(core): implement experimental getSignalGraph debug API #57074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/core/src/render3/debug/framework_injector_profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {getComponentDef} from '../def_getters';
import {getNodeInjectorLView, getNodeInjectorTNode, NodeInjector} from '../di';
import {TNode} from '../interfaces/node';
import {LView} from '../interfaces/view';
import {EffectRef} from '../reactivity/effect';

import {
InjectedService,
Expand Down Expand Up @@ -67,6 +68,7 @@ class DIDebugData {
WeakMap<Type<unknown>, InjectedService[]>
>();
resolverToProviders = new WeakMap<Injector | TNode, ProviderRecord[]>();
resolverToEffects = new WeakMap<Injector | LView, EffectRef[]>();
standaloneInjectorToComponent = new WeakMap<Injector, Type<unknown>>();

reset() {
Expand Down Expand Up @@ -113,9 +115,26 @@ function handleInjectorProfilerEvent(injectorProfilerEvent: InjectorProfilerEven
handleInstanceCreatedByInjectorEvent(context, injectorProfilerEvent.instance);
} else if (type === InjectorProfilerEventType.ProviderConfigured) {
handleProviderConfiguredEvent(context, injectorProfilerEvent.providerRecord);
} else if (type === InjectorProfilerEventType.EffectCreated) {
handleEffectCreatedEvent(context, injectorProfilerEvent.effect);
}
}

function handleEffectCreatedEvent(context: InjectorProfilerContext, effect: EffectRef): void {
const diResolver = getDIResolver(context.injector);
if (diResolver === null) {
throwError('An EffectCreated event must be run within an injection context.');
}

const {resolverToEffects} = frameworkDIDebugData;

if (!resolverToEffects.has(diResolver)) {
resolverToEffects.set(diResolver, []);
}

resolverToEffects.get(diResolver)!.push(effect);
}

/**
*
* Stores the injected service in frameworkDIDebugData.resolverToTokenToDependencies
Expand Down
25 changes: 24 additions & 1 deletion packages/core/src/render3/debug/injector_profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {Type} from '../../interface/type';
import {throwError} from '../../util/assert';
import type {TNode} from '../interfaces/node';
import type {LView} from '../interfaces/view';
import type {EffectRef} from '../reactivity/effect';

/**
* An enum describing the types of events that can be emitted from the injector profiler
Expand All @@ -35,6 +36,11 @@ export const enum InjectorProfilerEventType {
* Emits when an injector configures a provider.
*/
ProviderConfigured,

/**
* Emits when an effect is created.
*/
EffectCreated,
}

/**
Expand Down Expand Up @@ -74,14 +80,21 @@ export interface ProviderConfiguredEvent {
providerRecord: ProviderRecord;
}

export interface EffectCreatedEvent {
type: InjectorProfilerEventType.EffectCreated;
context: InjectorProfilerContext;
effect: EffectRef;
}

/**
* An object representing an event that is emitted through the injector profiler
*/

export type InjectorProfilerEvent =
| InjectedServiceEvent
| InjectorCreatedInstanceEvent
| ProviderConfiguredEvent;
| ProviderConfiguredEvent
| EffectCreatedEvent;

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

export function emitEffectCreatedEvent(effect: EffectRef): void {
!ngDevMode && throwError('Injector profiler should never be called in production mode');

injectorProfiler({
type: InjectorProfilerEventType.EffectCreated,
context: getInjectorProfilerContext(),
effect,
});
}

export function runInInjectorProfilerContext(
injector: Injector,
token: Type<unknown>,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/reactive_lview_consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function maybeReturnReactiveLViewConsumer(consumer: ReactiveLViewConsumer
freeConsumers.push(consumer);
}

const REACTIVE_LVIEW_CONSUMER_NODE: Omit<ReactiveLViewConsumer, 'lView'> = {
export const REACTIVE_LVIEW_CONSUMER_NODE: Omit<ReactiveLViewConsumer, 'lView'> = {
...REACTIVE_NODE,
consumerIsAlwaysLive: true,
kind: 'template',
Expand Down Expand Up @@ -78,7 +78,7 @@ export function getOrCreateTemporaryConsumer(lView: LView): ReactiveLViewConsume
return consumer;
}

const TEMPORARY_CONSUMER_NODE = {
export const TEMPORARY_CONSUMER_NODE = {
...REACTIVE_NODE,
consumerIsAlwaysLive: true,
kind: 'template',
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/render3/reactivity/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {assertInInjectionContext} from '../../di/contextual';
import {DestroyRef, NodeInjectorDestroyRef} from '../../linker/destroy_ref';
import {ViewContext} from '../view_context';
import {noop} from '../../util/noop';
import {ErrorHandler} from '../../error_handler';
import {
ChangeDetectionScheduler,
NotificationSource,
Expand All @@ -38,6 +37,7 @@ import {USE_MICROTASK_EFFECT_BY_DEFAULT} from './patch';
import {microtaskEffect} from './microtask_effect';

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

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

class EffectRefImpl implements EffectRef {
export class EffectRefImpl implements EffectRef {
[SIGNAL]: EffectNode;

constructor(node: EffectNode) {
Expand Down Expand Up @@ -199,11 +199,19 @@ export function effect(
node.onDestroyFn = destroyRef.onDestroy(() => node.destroy());
}

const effectRef = new EffectRefImpl(node);

if (ngDevMode) {
node.debugName = options?.debugName ?? '';
const prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null});
try {
emitEffectCreatedEvent(effectRef);
} finally {
setInjectorProfilerContext(prevInjectorProfilerContext);
}
}

return new EffectRefImpl(node);
return effectRef;
}

export interface EffectNode extends ReactiveNode, SchedulableEffect {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/render3/util/global_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getInjectorProviders,
getInjectorResolutionPath,
} from './injector_discovery_utils';
import {getSignalGraph} from './signal_debug';

/**
* This file introduces series of globally accessible debug tools
Expand Down Expand Up @@ -65,6 +66,7 @@ const globalUtilsFunctions = {
'ɵgetInjectorResolutionPath': getInjectorResolutionPath,
'ɵgetInjectorMetadata': getInjectorMetadata,
'ɵsetProfiler': setProfiler,
'ɵgetSignalGraph': getSignalGraph,

'getDirectiveMetadata': getDirectiveMetadata,
'getComponent': getComponent,
Expand Down
194 changes: 194 additions & 0 deletions packages/core/src/render3/util/signal_debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
REACTIVE_LVIEW_CONSUMER_NODE,
ReactiveLViewConsumer,
TEMPORARY_CONSUMER_NODE,
} from '../reactive_lview_consumer';
import {assertTNode, assertLView} from '../assert';
import {getFrameworkDIDebugData} from '../debug/framework_injector_profiler';
import {NodeInjector, getNodeInjectorTNode, getNodeInjectorLView} from '../di';
import {REACTIVE_TEMPLATE_CONSUMER, HOST, LView} from '../interfaces/view';
import {EffectNode, EffectRefImpl, ROOT_EFFECT_NODE, VIEW_EFFECT_NODE} from '../reactivity/effect';
import {Injector} from '../../di/injector';
import {R3Injector} from '../../di/r3_injector';
import {throwError} from '../../util/assert';
import {
ComputedNode,
ReactiveNode,
SIGNAL,
SIGNAL_NODE,
SignalNode,
} from '@angular/core/primitives/signals';

export interface DebugSignalGraphNode {
kind: string;
label?: string;
value?: unknown;
}

export interface DebugSignalGraphEdge {
/**
* Index of a signal node in the `nodes` array that is a consumer of the signal produced by the producer node.
*/
consumer: number;

/**
* Index of a signal node in the `nodes` array that is a producer of the signal consumed by the consumer node.
*/
producer: number;
}

/**
* A debug representation of the signal graph.
*/
export interface DebugSignalGraph {
nodes: DebugSignalGraphNode[];
edges: DebugSignalGraphEdge[];
}

function isComputedNode(node: ReactiveNode): node is ComputedNode<unknown> {
return node.kind === 'computed';
}

function isTemplateEffectNode(node: ReactiveNode): node is ReactiveLViewConsumer {
return node.kind === 'template';
}

function isEffectNode(node: ReactiveNode): node is EffectNode {
return node.kind === 'effect';
}

function isSignalNode(node: ReactiveNode): node is SignalNode<unknown> {
return node.kind === 'signal';
}

/**
*
* @param injector
* @returns Template consumer of given NodeInjector
*/
function getTemplateConsumer(injector: NodeInjector): ReactiveLViewConsumer | null {
const tNode = getNodeInjectorTNode(injector)!;
assertTNode(tNode);
const lView = getNodeInjectorLView(injector)!;
assertLView(lView);
const templateLView = lView[tNode.index]!;
assertLView(templateLView);

return templateLView[REACTIVE_TEMPLATE_CONSUMER];
}

function getNodesAndEdgesFromSignalMap(signalMap: ReadonlyMap<ReactiveNode, ReactiveNode[]>): {
nodes: DebugSignalGraphNode[];
edges: DebugSignalGraphEdge[];
} {
const nodes = Array.from(signalMap.keys());
const debugSignalGraphNodes: DebugSignalGraphNode[] = [];
const edges: DebugSignalGraphEdge[] = [];

for (const [consumer, producers] of signalMap.entries()) {
const consumerIndex = nodes.indexOf(consumer);

// collect node
if (isComputedNode(consumer) || isSignalNode(consumer)) {
debugSignalGraphNodes.push({
label: consumer.debugName,
value: consumer.value,
kind: consumer.kind,
});
} else if (isTemplateEffectNode(consumer)) {
debugSignalGraphNodes.push({
label: consumer.debugName ?? consumer.lView?.[HOST]?.tagName?.toLowerCase?.(),
kind: consumer.kind,
});
} else if (isEffectNode(consumer)) {
debugSignalGraphNodes.push({
label: consumer.debugName,
kind: consumer.kind,
});
} else {
debugSignalGraphNodes.push({
label: consumer.debugName,
kind: consumer.kind,
});
}

// collect edges for node
for (const producer of producers) {
edges.push({consumer: consumerIndex, producer: nodes.indexOf(producer)});
}
}

return {nodes: debugSignalGraphNodes, edges};
}

function extractEffectsFromInjector(injector: Injector): ReactiveNode[] {
let diResolver: Injector | LView<unknown> = injector;
if (injector instanceof NodeInjector) {
const lView = getNodeInjectorLView(injector)!;
diResolver = lView;
}

const resolverToEffects = getFrameworkDIDebugData().resolverToEffects as Map<
Injector | LView<unknown>,
EffectRefImpl[]
>;
const effects = resolverToEffects.get(diResolver) ?? [];

return effects.map((effect: EffectRefImpl) => effect[SIGNAL]);
}

function extractSignalNodesAndEdgesFromRoots(
nodes: ReactiveNode[],
signalDependenciesMap: Map<ReactiveNode, ReactiveNode[]> = new Map(),
): Map<ReactiveNode, ReactiveNode[]> {
for (const node of nodes) {
if (signalDependenciesMap.has(node)) {
continue;
}

const producerNodes = (node.producerNode ?? []) as ReactiveNode[];
signalDependenciesMap.set(node, producerNodes);
extractSignalNodesAndEdgesFromRoots(producerNodes, signalDependenciesMap);
}

return signalDependenciesMap;
}

/**
* Returns a debug representation of the signal graph for the given injector.
*
* Currently only supports element injectors. Starts by discovering the consumer nodes
* and then traverses their producer nodes to build the signal graph.
*
* @param injector The injector to get the signal graph for.
* @returns A debug representation of the signal graph.
* @throws If the injector is an environment injector.
*/
export function getSignalGraph(injector: Injector): DebugSignalGraph {
let templateConsumer: ReactiveLViewConsumer | null = null;

if (!(injector instanceof NodeInjector) && !(injector instanceof R3Injector)) {
return throwError('getSignalGraph must be called with a NodeInjector or R3Injector');
}

if (injector instanceof NodeInjector) {
templateConsumer = getTemplateConsumer(injector as NodeInjector);
}

const nonTemplateEffectNodes = extractEffectsFromInjector(injector);

const signalNodes = templateConsumer
? [templateConsumer, ...nonTemplateEffectNodes]
: nonTemplateEffectNodes;

const signalDependenciesMap = extractSignalNodesAndEdgesFromRoots(signalNodes);

return getNodesAndEdgesFromSignalMap(signalDependenciesMap);
}
Loading
Loading
0