From 39b69aa278c3aaed96c2c5bbead38e51812b3039 Mon Sep 17 00:00:00 2001 From: hawkgs Date: Fri, 15 May 2026 15:07:46 +0300 Subject: [PATCH] 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. --- .../ng-devtools-backend/src/lib/BUILD.bazel | 34 ++------------- .../src/lib/client-event-subscribers.ts | 38 +++++++++++++++-- .../src/lib/component-inspector/BUILD.bazel | 2 +- .../src/lib/component-tree/BUILD.bazel | 2 +- .../src/lib/component-tree/component-tree.ts | 2 +- .../src/lib/directive-forest/BUILD.bazel | 4 +- .../src/lib/directive-forest/ltree.ts | 2 +- .../src/lib/directive-forest/render-tree.ts | 2 +- .../src/lib/hooks/BUILD.bazel | 2 +- .../src/lib/hooks/capture.ts | 2 +- .../src/lib/hooks/profiler/BUILD.bazel | 2 +- .../src/lib/hooks/profiler/native.ts | 2 +- .../src/lib/hooks/profiler/polyfill.ts | 2 +- .../src/lib/property-mutation.ts | 2 +- .../src/lib/state-serializer/BUILD.bazel | 2 +- .../src/lib/state-serializer/object-utils.ts | 2 +- .../src/lib/state-serializer/prop-type.ts | 2 +- .../serialized-descriptor-factory.ts | 2 +- .../lib/state-serializer/state-serializer.ts | 2 +- .../src/lib/utils/BUILD.bazel | 34 +++++++++++++++ .../src/lib/{utils.ts => utils/general.ts} | 2 +- .../serialization.spec.ts} | 2 +- .../serialization.ts} | 0 .../src/lib/utils/signal-graph-ref.spec.ts | 41 +++++++++++++++++++ .../src/lib/utils/signal-graph-ref.ts | 37 +++++++++++++++++ .../core/src/core_render3_private_export.ts | 5 +++ 26 files changed, 174 insertions(+), 55 deletions(-) create mode 100644 devtools/projects/ng-devtools-backend/src/lib/utils/BUILD.bazel rename devtools/projects/ng-devtools-backend/src/lib/{utils.ts => utils/general.ts} (94%) rename devtools/projects/ng-devtools-backend/src/lib/{serialization-utils.spec.ts => utils/serialization.spec.ts} (96%) rename devtools/projects/ng-devtools-backend/src/lib/{serialization-utils.ts => utils/serialization.ts} (100%) create mode 100644 devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.spec.ts create mode 100644 devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.ts diff --git a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel index c849306bd654..1581f435e133 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel @@ -56,8 +56,8 @@ ts_project( name = "property_mutation", srcs = ["property-mutation.ts"], deps = [ - ":utils", "//:node_modules/@angular/core", + "//devtools/projects/ng-devtools-backend/src/lib/utils", ], ) @@ -111,35 +111,6 @@ ts_project( ], ) -ts_project( - name = "utils", - srcs = [ - "serialization-utils.ts", - "utils.ts", - ], - deps = [ - "//:node_modules/@angular/core", - "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", - ], -) - -zoneless_web_test_suite( - name = "utils_test", - deps = [ - ":utils_test_lib", - ], -) - -ts_test_library( - name = "utils_test_lib", - srcs = [ - "serialization-utils.spec.ts", - ], - deps = [ - ":utils", - ], -) - ts_project( name = "version", srcs = ["version.ts"], @@ -174,13 +145,14 @@ ts_project( ":interfaces", ":router_tree", ":set_console_reference", - ":utils", + "//:node_modules/@angular/core", "//:node_modules/rxjs", "//devtools/projects/ng-devtools-backend/src/lib/component-inspector", "//devtools/projects/ng-devtools-backend/src/lib/component-tree", "//devtools/projects/ng-devtools-backend/src/lib/hooks", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", + "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", "//devtools/projects/shared-utils", ], diff --git a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts index ef7a8f4a3fcb..681bddd99946 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import {ɵDebugSignalGraph} from '@angular/core'; import { ComponentExplorerViewQuery, ComponentType, @@ -57,10 +58,11 @@ import {getRouterCallableConstructRef, parseRoutes, RoutePropertyType} from './r import {ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api'; import {setConsoleReference} from './set-console-reference'; import {serializeDirectiveState, serializeValue} from './state-serializer/state-serializer'; -import {runOutsideAngular, unwrapSignal} from './utils'; +import {runOutsideAngular, unwrapSignal} from './utils/general'; import {DirectiveForestHooks} from './hooks/hooks'; import {getSupportedApis} from './ng-debug-api/supported-apis'; -import {sanitizeObject} from './serialization-utils'; +import {sanitizeObject} from './utils/serialization'; +import {SignalGraphRef} from './utils/signal-graph-ref'; type InspectorRef = {ref: ComponentInspector | null}; @@ -262,7 +264,21 @@ const getSignalNestedPropertiesCallback = const ng = ngDebugClient(); - const signalGraph = ng.ɵgetSignalGraph?.(injector); + let signalGraph: ɵDebugSignalGraph | undefined; + + // Considering that the inspection of signal value nested properties + // usually involves multiple requests, we store the signal graph + // during the first call. We keep only the last requested signal graph + // to avoid filling the heap with graphs that may not be needed. + if (componentSignalGraphRef.exists(node.nativeElement!)) { + signalGraph = componentSignalGraphRef.deref(node.nativeElement!); + } else { + signalGraph = ng.ɵgetSignalGraph?.(injector); + if (signalGraph) { + componentSignalGraphRef.set(node.nativeElement!, signalGraph); + } + } + if (!signalGraph) { return emitEmpty(); } @@ -493,7 +509,7 @@ const getInjectorProvidersCallback = const serializedProviderRecords: SerializedProviderRecord[] = []; - for (const [token, records] of tokenToRecords.entries()) { + for (const [, records] of tokenToRecords.entries()) { const multiRecords = records.filter((record) => record.multi); const nonMultiRecords = records.filter((record) => !record.multi); @@ -617,6 +633,10 @@ const getInjectorInstance = ( }; const getSignalGraphCallback = (messageBus: MessageBus) => (element: ElementPosition) => { + // We assume that a new request for a signal graph + // should invalidate the current ref cache. + componentSignalGraphRef.clear(); + const ng = ngDebugClient(); // get injector from position @@ -664,3 +684,13 @@ export function sanitizeRouteData(route: Route): Route { return route; } + +/** + * Keeps a reference to the last requested signal graph. + * This should save us from needlessly calling `ng.ɵgetSignalGraph` + * when we are still managing the same/last graph (e.g. inspecting + * signal value nested properties). The ref is tied to the host element. + * + * Note: If the element is destroyed, the graph is garbage collected. + */ +const componentSignalGraphRef = new SignalGraphRef(); diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel index 53226669e92d..1a2169d23291 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel @@ -24,9 +24,9 @@ ts_test_library( "//:node_modules/@angular/core", "//devtools/projects/ng-devtools-backend/src/lib:highlighter", "//devtools/projects/ng-devtools-backend/src/lib:interfaces", - "//devtools/projects/ng-devtools-backend/src/lib:utils", "//devtools/projects/ng-devtools-backend/src/lib/component-tree", "//devtools/projects/ng-devtools-backend/src/lib/hooks", + "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-tree/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/component-tree/BUILD.bazel index 0a8da72fb1c4..7f494380261b 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-tree/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/component-tree/BUILD.bazel @@ -21,10 +21,10 @@ ts_project( "//:node_modules/@angular/core", "//devtools/projects/ng-devtools-backend/src/lib:interfaces", "//devtools/projects/ng-devtools-backend/src/lib:property_mutation", - "//devtools/projects/ng-devtools-backend/src/lib:utils", "//devtools/projects/ng-devtools-backend/src/lib/directive-forest", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", + "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-tree/component-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/component-tree/component-tree.ts index fb6012f417f4..cebe604fd5eb 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-tree/component-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-tree/component-tree.ts @@ -49,7 +49,7 @@ import {mutateNestedProp} from '../property-mutation'; import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from '../interfaces'; import {getAppRoots} from './get-roots'; import {AcxChangeDetectionStrategy, ChangeDetectionStrategy, Framework} from './core-enums'; -import {unwrapSignal} from '../utils'; +import {unwrapSignal} from '../utils/general'; export const injectorToId = new WeakMap(); export const nodeInjectorToResolutionPath = new WeakMap(); diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel index c998453c6dcb..8f972bfd63a5 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel @@ -14,10 +14,10 @@ ts_project( "//:node_modules/semver", "//devtools/projects/ng-devtools-backend/src/lib:highlighter", "//devtools/projects/ng-devtools-backend/src/lib:interfaces", - "//devtools/projects/ng-devtools-backend/src/lib:utils", "//devtools/projects/ng-devtools-backend/src/lib:version", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", + "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", ], ) @@ -31,7 +31,7 @@ ts_test_library( ":directive-forest", "//:node_modules/@angular/core", "//devtools/projects/ng-devtools-backend/src/lib:interfaces", - "//devtools/projects/ng-devtools-backend/src/lib:utils", + "//devtools/projects/ng-devtools-backend/src/lib/utils", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts index 19ef2b9199a2..af57206c52d3 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts @@ -10,7 +10,7 @@ import semver from 'semver'; import {getDirectiveName} from '../highlighter'; import {ComponentInstanceType, ComponentTreeNode, DirectiveInstanceType} from '../interfaces'; -import {isCustomElement} from '../utils'; +import {isCustomElement} from '../utils/general'; import {VERSION} from '../version'; let HEADER_OFFSET = 19; diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts index e4d117e94f9f..eb2e7fb24609 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts @@ -14,7 +14,7 @@ import {HydrationStatus} from '../../../../protocol'; import {ComponentTreeNode} from '../interfaces'; import {ngDebugClient} from '../ng-debug-api/ng-debug-api'; -import {isCustomElement} from '../utils'; +import {isCustomElement} from '../utils/general'; import { ControlFlowBlocksIterator, createControlFlowTreeNode, diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel index 0f738e23487c..29e1936ed211 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel @@ -15,8 +15,8 @@ ts_project( ":identity_tracker", "//devtools/projects/ng-devtools-backend/src/lib:highlighter", "//devtools/projects/ng-devtools-backend/src/lib:interfaces", - "//devtools/projects/ng-devtools-backend/src/lib:utils", "//devtools/projects/ng-devtools-backend/src/lib/hooks/profiler", + "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts index b222aef8f1d3..dc23aab837d5 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts @@ -17,7 +17,7 @@ import { import {getDirectiveName} from '../highlighter'; import {ComponentTreeNode} from '../interfaces'; -import {isCustomElement, runOutsideAngular} from '../utils'; +import {isCustomElement, runOutsideAngular} from '../utils/general'; import {initializeOrGetDirectiveForestHooks} from '.'; import {DirectiveForestHooks} from './hooks'; diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel index 7c513a05daf3..07dea802c5ac 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel @@ -11,10 +11,10 @@ ts_project( deps = [ "//:node_modules/@angular/core", "//:node_modules/rxjs", - "//devtools/projects/ng-devtools-backend/src/lib:utils", "//devtools/projects/ng-devtools-backend/src/lib/directive-forest", "//devtools/projects/ng-devtools-backend/src/lib/hooks:identity_tracker", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", + "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts index 79064b4f161d..6c415a0599ac 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts @@ -9,7 +9,7 @@ import {ɵProfilerEvent} from '@angular/core'; import {getDirectiveHostElement} from '../../directive-forest'; import {ngDebugClient} from '../../ng-debug-api/ng-debug-api'; -import {runOutsideAngular} from '../../utils'; +import {runOutsideAngular} from '../../utils/general'; import {IdentityTracker, NodeArray} from '../identity-tracker'; import {getLifeCycleName, Hooks, Profiler} from './shared'; diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/polyfill.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/polyfill.ts index c27330bcae02..837dabe6718c 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/polyfill.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/polyfill.ts @@ -11,7 +11,7 @@ import { getLViewFromDirectiveOrElementInstance, METADATA_PROPERTY_NAME, } from '../../directive-forest'; -import {runOutsideAngular} from '../../utils'; +import {runOutsideAngular} from '../../utils/general'; import {IdentityTracker, NodeArray} from '../identity-tracker'; import {getLifeCycleName, Profiler} from './shared'; diff --git a/devtools/projects/ng-devtools-backend/src/lib/property-mutation.ts b/devtools/projects/ng-devtools-backend/src/lib/property-mutation.ts index be991938ae3d..cf77c0f43b28 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/property-mutation.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/property-mutation.ts @@ -7,7 +7,7 @@ */ import type {Signal, WritableSignal} from '@angular/core'; -import {isSignal} from './utils'; +import {isSignal} from './utils/general'; /** Represents a property access operation. */ interface PropertyAccess { diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/BUILD.bazel index 188f16a5a000..d633a4456dbc 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/BUILD.bazel @@ -9,7 +9,7 @@ ng_project( exclude = ["*.spec.ts"], ), deps = [ - "//devtools/projects/ng-devtools-backend/src/lib:utils", + "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts index 3a9532363cee..231a195f0459 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {unwrapSignal} from '../utils'; +import {unwrapSignal} from '../utils/general'; // Intentionally do not include all the prototype (Except for getters and setters) // because it contains inherited methods (hasOwnProperty, etc.). Also ignore symbols diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/prop-type.ts b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/prop-type.ts index e1f3972ec72e..f97718d34c51 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/prop-type.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/prop-type.ts @@ -8,7 +8,7 @@ import {PropType} from '../../../../protocol'; -import {isSignal, safelyReadSignalValue} from '../utils'; +import {isSignal, safelyReadSignalValue} from '../utils/general'; const commonTypes = { boolean: PropType.Boolean, diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts index 40bf8a47020d..89b1249fadc7 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts @@ -8,7 +8,7 @@ import {ContainerType, Descriptor, NestedProp, PropType} from '../../../../protocol'; -import {isSignal, safelyReadSignalValue, unwrapSignal} from '../utils'; +import {isSignal, safelyReadSignalValue, unwrapSignal} from '../utils/general'; import {getDescriptor, getKeys} from './object-utils'; diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts index e8da6552e509..2813d7d501a7 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts @@ -8,7 +8,7 @@ import {ContainerType, Descriptor, NestedProp, PropType} from '../../../../protocol'; -import {isSignal, unwrapSignal} from '../utils'; +import {isSignal, unwrapSignal} from '../utils/general'; import {getKeys} from './object-utils'; import {getPropType} from './prop-type'; diff --git a/devtools/projects/ng-devtools-backend/src/lib/utils/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/utils/BUILD.bazel new file mode 100644 index 000000000000..f4fecdf98412 --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/utils/BUILD.bazel @@ -0,0 +1,34 @@ +load("//devtools/tools:defaults.bzl", "ng_project", "ts_test_library", "zoneless_web_test_suite") + +package(default_visibility = ["//devtools:__subpackages__"]) + +ng_project( + name = "utils", + srcs = glob( + include = ["*.ts"], + exclude = ["*.spec.ts"], + ), + deps = [ + "//:node_modules/@angular/core", + "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", + ], +) + +ts_test_library( + name = "test_lib", + srcs = [ + "serialization.spec.ts", + "signal-graph-ref.spec.ts", + ], + deps = [ + ":utils", + "//:node_modules/@angular/core", + ], +) + +zoneless_web_test_suite( + name = "test", + deps = [ + ":test_lib", + ], +) diff --git a/devtools/projects/ng-devtools-backend/src/lib/utils.ts b/devtools/projects/ng-devtools-backend/src/lib/utils/general.ts similarity index 94% rename from devtools/projects/ng-devtools-backend/src/lib/utils.ts rename to devtools/projects/ng-devtools-backend/src/lib/utils/general.ts index 087ff34cb572..ba4ca32f5406 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/utils.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/utils/general.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ngDebugApiIsSupported, ngDebugClient} from './ng-debug-api/ng-debug-api'; +import {ngDebugApiIsSupported, ngDebugClient} from '../ng-debug-api/ng-debug-api'; export const runOutsideAngular = (f: () => void): void => { const w = window as any; diff --git a/devtools/projects/ng-devtools-backend/src/lib/serialization-utils.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/utils/serialization.spec.ts similarity index 96% rename from devtools/projects/ng-devtools-backend/src/lib/serialization-utils.spec.ts rename to devtools/projects/ng-devtools-backend/src/lib/utils/serialization.spec.ts index 99c1c82d330b..0780d47ce1e2 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/serialization-utils.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/utils/serialization.spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {sanitizeObject} from './serialization-utils'; +import {sanitizeObject} from './serialization'; describe('sanitizeObject', () => { it('should not change valid object', () => { diff --git a/devtools/projects/ng-devtools-backend/src/lib/serialization-utils.ts b/devtools/projects/ng-devtools-backend/src/lib/utils/serialization.ts similarity index 100% rename from devtools/projects/ng-devtools-backend/src/lib/serialization-utils.ts rename to devtools/projects/ng-devtools-backend/src/lib/utils/serialization.ts diff --git a/devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.spec.ts new file mode 100644 index 000000000000..115a43822925 --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.spec.ts @@ -0,0 +1,41 @@ +/** + * @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 {ɵDebugSignalGraph} from '@angular/core'; +import {SignalGraphRef} from './signal-graph-ref'; + +describe('SignalGraphRef', () => { + let key: object; + let graph: ɵDebugSignalGraph; + let ref: SignalGraphRef; + + beforeEach(() => { + key = {}; + graph = {} as ɵDebugSignalGraph; + ref = new SignalGraphRef(); + }); + + it('should store a ref', () => { + ref.set(key, graph); + + expect(ref.deref(key)).toBe(graph); + }); + + it('should clear a ref', () => { + ref.set(key, graph); + ref.clear(); + + expect(ref.deref(key)).toBe(undefined); + }); + + it('should tell if a ref exists', () => { + ref.set(key, graph); + + expect(ref.exists(key)).toBe(true); + }); +}); diff --git a/devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.ts b/devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.ts new file mode 100644 index 000000000000..606e257ee18f --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/utils/signal-graph-ref.ts @@ -0,0 +1,37 @@ +/** + * @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 {ɵDebugSignalGraph as InternalDebugSignalGraph} from '@angular/core'; + +/** + * Keeps a `ɵDebugSignalGraph` reference tied to a weak-referenced key. + * If the key is garbage collected, the graph is also cleared. + */ +export class SignalGraphRef { + private ref: WeakMap = new WeakMap(); + + /** Gets the key-bound signal graph reference. */ + deref(key: T): InternalDebugSignalGraph | undefined { + return this.ref.get(key); + } + + /** Sets the signal graph. */ + set(key: T, graph: InternalDebugSignalGraph) { + this.ref = new WeakMap([[key, graph]]); + } + + /** Checks if the key-bound signal graph exists. */ + exists(key: T) { + return !!this.ref.get(key); + } + + /** Clears the reference. */ + clear() { + this.ref = new WeakMap(); + } +} diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index c455bc48a8e3..5f0152bb3540 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -291,6 +291,11 @@ export { FrameworkAgnosticGlobalUtils as ɵFrameworkAgnosticGlobalUtils, GlobalDevModeUtils as ɵGlobalDevModeUtils, } from './render3/util/global_utils'; +export { + DebugSignalGraph as ɵDebugSignalGraph, + DebugSignalGraphEdge as ɵDebugSignalGraphEdge, + DebugSignalGraphNode as ɵDebugSignalGraphNode, +} from './render3/util/signal_debug'; export {getTransferState as ɵgetTransferState} from './render3/util/transfer_state_utils'; export { isViewDirty as ɵisViewDirty,