Skip to content

Commit fe50a7e

Browse files
authored
perf(devtools): optimize signal graph nodes value inspection
This change is based on the presumption that a signal graph can be significantly large memory-wise sometimes. This is the reason why we don't send the full graph to the FE but rather serialize its values and then lazy load them when they are needed, that is, during value inspection.
1 parent b3748e9 commit fe50a7e

26 files changed

Lines changed: 177 additions & 58 deletions

devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ ts_project(
5656
name = "property_mutation",
5757
srcs = ["property-mutation.ts"],
5858
deps = [
59-
":utils",
6059
"//:node_modules/@angular/core",
60+
"//devtools/projects/ng-devtools-backend/src/lib/utils",
6161
],
6262
)
6363

@@ -111,35 +111,6 @@ ts_project(
111111
],
112112
)
113113

114-
ts_project(
115-
name = "utils",
116-
srcs = [
117-
"serialization-utils.ts",
118-
"utils.ts",
119-
],
120-
deps = [
121-
"//:node_modules/@angular/core",
122-
"//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api",
123-
],
124-
)
125-
126-
zoneless_web_test_suite(
127-
name = "utils_test",
128-
deps = [
129-
":utils_test_lib",
130-
],
131-
)
132-
133-
ts_test_library(
134-
name = "utils_test_lib",
135-
srcs = [
136-
"serialization-utils.spec.ts",
137-
],
138-
deps = [
139-
":utils",
140-
],
141-
)
142-
143114
ts_project(
144115
name = "version",
145116
srcs = ["version.ts"],
@@ -174,13 +145,14 @@ ts_project(
174145
":interfaces",
175146
":router_tree",
176147
":set_console_reference",
177-
":utils",
148+
"//:node_modules/@angular/core",
178149
"//:node_modules/rxjs",
179150
"//devtools/projects/ng-devtools-backend/src/lib/component-inspector",
180151
"//devtools/projects/ng-devtools-backend/src/lib/component-tree",
181152
"//devtools/projects/ng-devtools-backend/src/lib/hooks",
182153
"//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api",
183154
"//devtools/projects/ng-devtools-backend/src/lib/state-serializer",
155+
"//devtools/projects/ng-devtools-backend/src/lib/utils",
184156
"//devtools/projects/protocol",
185157
"//devtools/projects/shared-utils",
186158
],

devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {ɵDebugSignalGraph as InternalDebugSignalGraph} from '@angular/core';
910
import {debounceTime} from 'rxjs/operators';
1011
import {
1112
ComponentExplorerViewQuery,
@@ -57,10 +58,11 @@ import {ComponentTreeNode} from './interfaces';
5758
import {ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api';
5859
import {getSupportedApis} from './ng-debug-api/supported-apis';
5960
import {getRouterCallableConstructRef, parseRoutes, RoutePropertyType} from './router-tree';
60-
import {sanitizeObject} from './serialization-utils';
6161
import {setConsoleReference} from './set-console-reference';
6262
import {serializeDirectiveState, serializeValue} from './state-serializer/state-serializer';
63-
import {runOutsideAngular, unwrapSignal} from './utils';
63+
import {runOutsideAngular, unwrapSignal} from './utils/general';
64+
import {sanitizeObject} from './utils/serialization';
65+
import {SignalGraphRef} from './utils/signal-graph-ref';
6466

6567
type InspectorRef = {ref: ComponentInspector | null};
6668

@@ -256,23 +258,37 @@ const getSignalNestedPropertiesCallback =
256258
position.element,
257259
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
258260
);
259-
if (!node) {
261+
if (!node || !node.nativeElement) {
260262
return emitEmpty();
261263
}
262264

263-
const injector = getInjectorFromElementNode(node.nativeElement!);
265+
const injector = getInjectorFromElementNode(node.nativeElement);
264266
if (!injector) {
265267
return emitEmpty();
266268
}
267269

268270
const ng = ngDebugClient();
269271

270-
const signalGraph = ng.ɵgetSignalGraph?.(injector);
272+
let signalGraph: InternalDebugSignalGraph | undefined;
273+
274+
// Considering that the inspection of signal value nested properties
275+
// usually involves multiple requests, we store the signal graph
276+
// during the first call. We keep only the last requested signal graph
277+
// to avoid filling the heap with graphs that may not be needed.
278+
if (componentSignalGraphRef.exists(node.nativeElement)) {
279+
signalGraph = componentSignalGraphRef.deref(node.nativeElement);
280+
} else {
281+
signalGraph = ng.ɵgetSignalGraph?.(injector);
282+
if (signalGraph) {
283+
componentSignalGraphRef.set(node.nativeElement, signalGraph);
284+
}
285+
}
286+
271287
if (!signalGraph) {
272288
return emitEmpty();
273289
}
274290

275-
const current = signalGraph.nodes.find((node) => node.id === position.signalId);
291+
const current = signalGraph.nodes.find((n) => n.id === position.signalId);
276292
if (!current) {
277293
return emitEmpty();
278294
}
@@ -498,7 +514,7 @@ const getInjectorProvidersCallback =
498514

499515
const serializedProviderRecords: SerializedProviderRecord[] = [];
500516

501-
for (const [token, records] of tokenToRecords.entries()) {
517+
for (const records of tokenToRecords.values()) {
502518
const multiRecords = records.filter((record) => record.multi);
503519
const nonMultiRecords = records.filter((record) => !record.multi);
504520

@@ -622,6 +638,10 @@ const getInjectorInstance = (
622638
};
623639

624640
const getSignalGraphCallback = (messageBus: MessageBus<Events>) => (element: ElementPosition) => {
641+
// We assume that a new request for a signal graph
642+
// should invalidate the current ref cache.
643+
componentSignalGraphRef.clear();
644+
625645
const ng = ngDebugClient();
626646

627647
// get injector from position
@@ -669,3 +689,13 @@ export function sanitizeRouteData(route: Route): Route {
669689

670690
return route;
671691
}
692+
693+
/**
694+
* Keeps a reference to the last requested signal graph.
695+
* This should save us from needlessly calling `ng.ɵgetSignalGraph`
696+
* when we are still managing the same/last graph (e.g. inspecting
697+
* signal value nested properties). The ref is tied to the host element.
698+
*
699+
* Note: If the element is destroyed, the graph is garbage collected.
700+
*/
701+
const componentSignalGraphRef = new SignalGraphRef<Node>();

devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ ts_test_library(
2424
"//:node_modules/@angular/core",
2525
"//devtools/projects/ng-devtools-backend/src/lib:highlighter",
2626
"//devtools/projects/ng-devtools-backend/src/lib:interfaces",
27-
"//devtools/projects/ng-devtools-backend/src/lib:utils",
2827
"//devtools/projects/ng-devtools-backend/src/lib/component-tree",
2928
"//devtools/projects/ng-devtools-backend/src/lib/hooks",
29+
"//devtools/projects/ng-devtools-backend/src/lib/utils",
3030
"//devtools/projects/protocol",
3131
],
3232
)

devtools/projects/ng-devtools-backend/src/lib/component-tree/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ ts_project(
2121
"//:node_modules/@angular/core",
2222
"//devtools/projects/ng-devtools-backend/src/lib:interfaces",
2323
"//devtools/projects/ng-devtools-backend/src/lib:property_mutation",
24-
"//devtools/projects/ng-devtools-backend/src/lib:utils",
2524
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest",
2625
"//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api",
2726
"//devtools/projects/ng-devtools-backend/src/lib/state-serializer",
27+
"//devtools/projects/ng-devtools-backend/src/lib/utils",
2828
"//devtools/projects/protocol",
2929
],
3030
)

devtools/projects/ng-devtools-backend/src/lib/component-tree/component-tree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {mutateNestedProp} from '../property-mutation';
4949
import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from '../interfaces';
5050
import {getAppRoots} from './get-roots';
5151
import {AcxChangeDetectionStrategy, ChangeDetectionStrategy, Framework} from './core-enums';
52-
import {unwrapSignal} from '../utils';
52+
import {unwrapSignal} from '../utils/general';
5353

5454
export const injectorToId = new WeakMap<Injector | HTMLElement, string>();
5555
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();

devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ ts_project(
1414
"//:node_modules/semver",
1515
"//devtools/projects/ng-devtools-backend/src/lib:highlighter",
1616
"//devtools/projects/ng-devtools-backend/src/lib:interfaces",
17-
"//devtools/projects/ng-devtools-backend/src/lib:utils",
1817
"//devtools/projects/ng-devtools-backend/src/lib:version",
1918
"//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api",
2019
"//devtools/projects/ng-devtools-backend/src/lib/state-serializer",
20+
"//devtools/projects/ng-devtools-backend/src/lib/utils",
2121
"//devtools/projects/protocol",
2222
],
2323
)
@@ -31,7 +31,7 @@ ts_test_library(
3131
":directive-forest",
3232
"//:node_modules/@angular/core",
3333
"//devtools/projects/ng-devtools-backend/src/lib:interfaces",
34-
"//devtools/projects/ng-devtools-backend/src/lib:utils",
34+
"//devtools/projects/ng-devtools-backend/src/lib/utils",
3535
],
3636
)
3737

devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import semver from 'semver';
1010

1111
import {getDirectiveName} from '../highlighter';
1212
import {ComponentInstanceType, ComponentTreeNode, DirectiveInstanceType} from '../interfaces';
13-
import {isCustomElement} from '../utils';
13+
import {isCustomElement} from '../utils/general';
1414
import {VERSION} from '../version';
1515

1616
let HEADER_OFFSET = 19;

devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {HydrationStatus} from '../../../../protocol';
1414

1515
import {ComponentTreeNode} from '../interfaces';
1616
import {ngDebugClient} from '../ng-debug-api/ng-debug-api';
17-
import {isCustomElement} from '../utils';
17+
import {isCustomElement} from '../utils/general';
1818
import {
1919
ControlFlowBlocksIterator,
2020
createControlFlowTreeNode,

devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ ts_project(
1515
":identity_tracker",
1616
"//devtools/projects/ng-devtools-backend/src/lib:highlighter",
1717
"//devtools/projects/ng-devtools-backend/src/lib:interfaces",
18-
"//devtools/projects/ng-devtools-backend/src/lib:utils",
1918
"//devtools/projects/ng-devtools-backend/src/lib/hooks/profiler",
19+
"//devtools/projects/ng-devtools-backend/src/lib/utils",
2020
"//devtools/projects/protocol",
2121
],
2222
)

devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717

1818
import {getDirectiveName} from '../highlighter';
1919
import {ComponentTreeNode} from '../interfaces';
20-
import {isCustomElement, runOutsideAngular} from '../utils';
20+
import {isCustomElement, runOutsideAngular} from '../utils/general';
2121

2222
import {initializeOrGetDirectiveForestHooks} from '.';
2323
import {DirectiveForestHooks} from './hooks';

0 commit comments

Comments
 (0)