Skip to content

Commit 39b69aa

Browse files
committed
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. Because of the above-mentioned approach, when we inspect nested object values, we request the component's signal graph on each property expansion. This may results in a lot of redundant `ng.getSignalGraph` calls, that in the case of large graphs may have negative performance impact. The change prevents that by caching the last signal graph on the backend after the first request for nested properties. The cache is then cleared when the selected/focused component is changed.
1 parent 71f8d48 commit 39b69aa

26 files changed

Lines changed: 174 additions & 55 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: 34 additions & 4 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} from '@angular/core';
910
import {
1011
ComponentExplorerViewQuery,
1112
ComponentType,
@@ -57,10 +58,11 @@ import {getRouterCallableConstructRef, parseRoutes, RoutePropertyType} from './r
5758
import {ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api';
5859
import {setConsoleReference} from './set-console-reference';
5960
import {serializeDirectiveState, serializeValue} from './state-serializer/state-serializer';
60-
import {runOutsideAngular, unwrapSignal} from './utils';
61+
import {runOutsideAngular, unwrapSignal} from './utils/general';
6162
import {DirectiveForestHooks} from './hooks/hooks';
6263
import {getSupportedApis} from './ng-debug-api/supported-apis';
63-
import {sanitizeObject} from './serialization-utils';
64+
import {sanitizeObject} from './utils/serialization';
65+
import {SignalGraphRef} from './utils/signal-graph-ref';
6466

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

@@ -262,7 +264,21 @@ const getSignalNestedPropertiesCallback =
262264

263265
const ng = ngDebugClient();
264266

265-
const signalGraph = ng.ɵgetSignalGraph?.(injector);
267+
let signalGraph: ɵDebugSignalGraph | undefined;
268+
269+
// Considering that the inspection of signal value nested properties
270+
// usually involves multiple requests, we store the signal graph
271+
// during the first call. We keep only the last requested signal graph
272+
// to avoid filling the heap with graphs that may not be needed.
273+
if (componentSignalGraphRef.exists(node.nativeElement!)) {
274+
signalGraph = componentSignalGraphRef.deref(node.nativeElement!);
275+
} else {
276+
signalGraph = ng.ɵgetSignalGraph?.(injector);
277+
if (signalGraph) {
278+
componentSignalGraphRef.set(node.nativeElement!, signalGraph);
279+
}
280+
}
281+
266282
if (!signalGraph) {
267283
return emitEmpty();
268284
}
@@ -493,7 +509,7 @@ const getInjectorProvidersCallback =
493509

494510
const serializedProviderRecords: SerializedProviderRecord[] = [];
495511

496-
for (const [token, records] of tokenToRecords.entries()) {
512+
for (const [, records] of tokenToRecords.entries()) {
497513
const multiRecords = records.filter((record) => record.multi);
498514
const nonMultiRecords = records.filter((record) => !record.multi);
499515

@@ -617,6 +633,10 @@ const getInjectorInstance = (
617633
};
618634

619635
const getSignalGraphCallback = (messageBus: MessageBus<Events>) => (element: ElementPosition) => {
636+
// We assume that a new request for a signal graph
637+
// should invalidate the current ref cache.
638+
componentSignalGraphRef.clear();
639+
620640
const ng = ngDebugClient();
621641

622642
// get injector from position
@@ -664,3 +684,13 @@ export function sanitizeRouteData(route: Route): Route {
664684

665685
return route;
666686
}
687+
688+
/**
689+
* Keeps a reference to the last requested signal graph.
690+
* This should save us from needlessly calling `ng.ɵgetSignalGraph`
691+
* when we are still managing the same/last graph (e.g. inspecting
692+
* signal value nested properties). The ref is tied to the host element.
693+
*
694+
* Note: If the element is destroyed, the graph is garbage collected.
695+
*/
696+
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)