chrome-devtools-frontend 1.0.1697998 → 1.0.1698733

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.agents/skills/version-control/SKILL.md +21 -9
  2. package/front_end/core/platform/HostRuntime.ts +12 -0
  3. package/front_end/core/platform/api/HostRuntime.ts +13 -0
  4. package/front_end/core/platform/browser/HostRuntime.ts +34 -0
  5. package/front_end/core/platform/node/HostRuntime.ts +40 -0
  6. package/front_end/core/sdk/AnimationModel.ts +12 -98
  7. package/front_end/core/sdk/CSSModel.ts +4 -2
  8. package/front_end/core/sdk/CSSPropertyParser.ts +4 -4
  9. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +4 -44
  10. package/front_end/core/sdk/Connections.ts +6 -1
  11. package/front_end/core/sdk/ConsoleModel.ts +1 -12
  12. package/front_end/core/sdk/DOMModel.ts +3 -51
  13. package/front_end/core/sdk/DebuggerModel.ts +9 -8
  14. package/front_end/core/sdk/NetworkRequest.ts +1 -1
  15. package/front_end/core/sdk/PageFunctions.ts +187 -0
  16. package/front_end/core/sdk/Resource.ts +1 -1
  17. package/front_end/core/sdk/RuntimeModel.ts +1 -20
  18. package/front_end/core/sdk/Script.ts +21 -0
  19. package/front_end/core/sdk/SourceMap.ts +15 -2
  20. package/front_end/core/sdk/SourceMapCache.ts +13 -10
  21. package/front_end/core/sdk/SourceMapManager.ts +11 -9
  22. package/front_end/core/sdk/SourceMapScopeChainEntry.ts +59 -17
  23. package/front_end/core/sdk/SourceMapScopesInfo.ts +8 -2
  24. package/front_end/entrypoints/heap_snapshot_worker/ContextAnalyzer.ts +635 -0
  25. package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +26 -12
  26. package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshotLoader.ts +16 -2
  27. package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshotWorkerDispatcher.ts +4 -4
  28. package/front_end/entrypoints/heap_snapshot_worker/heap_snapshot_worker.ts +2 -0
  29. package/front_end/entrypoints/shell/shell.ts +1 -1
  30. package/front_end/models/ai_assistance/DOMHelpers.ts +95 -0
  31. package/front_end/models/ai_assistance/EvaluateAction.ts +5 -42
  32. package/front_end/models/ai_assistance/ExtensionScope.ts +2 -31
  33. package/front_end/models/ai_assistance/agents/AiAgent.ts +2 -1
  34. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +3 -4
  35. package/front_end/models/ai_assistance/ai_assistance.ts +2 -0
  36. package/front_end/models/ai_assistance/contexts/DOMNodeContext.ts +10 -7
  37. package/front_end/models/ai_assistance/contexts/FileContext.ts +2 -10
  38. package/front_end/models/ai_assistance/tools/GetSourceContent.ts +3 -2
  39. package/front_end/models/ai_assistance/tools/ListSources.ts +21 -14
  40. package/front_end/models/bindings/CompilerScriptMapping.ts +3 -10
  41. package/front_end/models/bindings/DebuggerLanguagePlugins.ts +48 -17
  42. package/front_end/models/bindings/ResourceScriptMapping.ts +2 -2
  43. package/front_end/models/bindings/StylesSourceMapping.ts +1 -1
  44. package/front_end/models/comment_manager/CD4ABridge.ts +55 -12
  45. package/front_end/models/comment_manager/CommentManager.ts +46 -91
  46. package/front_end/models/comment_manager/CommentThread.ts +145 -0
  47. package/front_end/models/comment_manager/comment_manager.ts +2 -0
  48. package/front_end/models/emulation/DeviceModeModel.ts +53 -0
  49. package/front_end/models/extensions/ExtensionAPI.ts +8 -6
  50. package/front_end/models/extensions/ExtensionEndpoint.ts +11 -8
  51. package/front_end/models/extensions/HostUrlPattern.ts +4 -3
  52. package/front_end/models/extensions/LanguageExtensionEndpoint.ts +3 -2
  53. package/front_end/models/extensions/RecorderExtensionEndpoint.ts +2 -1
  54. package/front_end/models/formatter/FormatterWorkerPool.ts +4 -6
  55. package/front_end/models/heap_snapshot/HeapSnapshotModel.ts +55 -0
  56. package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +18 -14
  57. package/front_end/models/issues_manager/IssuesManager.ts +5 -0
  58. package/front_end/models/issues_manager/WebInstallIssue.ts +63 -0
  59. package/front_end/models/issues_manager/descriptions/webInstallManifestMissingId.md +3 -0
  60. package/front_end/models/issues_manager/descriptions/webInstallManifestMissingNameOrShortName.md +3 -0
  61. package/front_end/models/issues_manager/descriptions/webInstallManifestParsingOrNetworkError.md +3 -0
  62. package/front_end/models/issues_manager/descriptions/webInstallNoManifest.md +3 -0
  63. package/front_end/models/issues_manager/descriptions/webInstallStartUrlInvalid.md +3 -0
  64. package/front_end/models/issues_manager/issues_manager.ts +2 -0
  65. package/front_end/models/workspace/UISourceCode.ts +20 -0
  66. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +4 -6
  67. package/front_end/panels/ai_assistance/components/AIv2MarkdownRenderer.ts +4 -10
  68. package/front_end/{ui/comments → panels/common}/CommentThreadWidget.ts +4 -4
  69. package/front_end/{ui/comments → panels/common}/CommentsOverlayWidget.ts +39 -62
  70. package/front_end/{ui/comments → panels/common}/CommentsStatusBarPill.ts +5 -5
  71. package/front_end/panels/common/ExtensionServer.ts +2 -1
  72. package/front_end/{ui/comments → panels/common}/commentsOverlay.css +2 -1
  73. package/front_end/{ui/comments/comments-meta.ts → panels/common/common-meta.ts} +11 -11
  74. package/front_end/panels/common/common.ts +3 -0
  75. package/front_end/panels/elements/AdoptedStyleSheetTreeElement.ts +3 -3
  76. package/front_end/panels/elements/ElementsPanel.ts +5 -1
  77. package/front_end/panels/elements/ElementsTreeElement.ts +4 -1
  78. package/front_end/panels/elements/ElementsTreeOutline.ts +58 -51
  79. package/front_end/panels/elements/PropertiesWidget.ts +11 -10
  80. package/front_end/panels/elements/PropertyRenderer.ts +29 -25
  81. package/front_end/panels/elements/StylePropertyTreeElement.ts +6 -6
  82. package/front_end/panels/elements/elementsTreeOutline.css +7 -1
  83. package/front_end/panels/emulation/DeviceModeToolbar.ts +2 -3
  84. package/front_end/panels/event_listeners/EventListenersView.ts +33 -44
  85. package/front_end/panels/linear_memory_inspector/LinearMemoryInspectorController.ts +6 -7
  86. package/front_end/panels/linear_memory_inspector/components/linearMemoryInspector.css +5 -1
  87. package/front_end/panels/linear_memory_inspector/linear_memory_inspector-meta.ts +1 -1
  88. package/front_end/panels/recorder/stepView.css +1 -0
  89. package/front_end/panels/security/SecurityPanel.ts +5 -5
  90. package/front_end/panels/security/SecurityPanelSidebar.ts +2 -11
  91. package/front_end/panels/sources/DebuggerPlugin.ts +1 -1
  92. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +11 -16
  93. package/front_end/panels/sources/sources-meta.ts +1 -1
  94. package/front_end/panels/timeline/overlays/OverlaysImpl.ts +0 -3
  95. package/front_end/third_party/chromium/README.chromium +1 -1
  96. package/front_end/ui/comments/CommentOverlayManager.ts +28 -110
  97. package/front_end/ui/comments/comments.ts +0 -6
  98. package/front_end/ui/legacy/Treeoutline.ts +4 -0
  99. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +14 -7
  100. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  101. package/package.json +1 -1
  102. /package/front_end/{ui/comments → panels/common}/commentThreadWidget.css +0 -0
@@ -0,0 +1,635 @@
1
+ // Copyright 2026 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import type * as Platform from '../../core/platform/platform.js';
6
+ import type * as HeapSnapshotModel from '../../models/heap_snapshot/heap_snapshot.js';
7
+
8
+ import type {HeapSnapshot, HeapSnapshotNode} from './HeapSnapshot.js';
9
+
10
+ /**
11
+ * Index of a `system / Script` node within the heap snapshot's node array.
12
+ */
13
+ type ScriptNodeIndex = Platform.Brand.Brand<number, 'ScriptNodeIndex'>;
14
+
15
+ /**
16
+ * Identifier of a scope, as emitted by V8 in the snapshot's scope table. Only
17
+ * unique within a single script.
18
+ */
19
+ type ScopeId = Platform.Brand.Brand<number, 'ScopeId'>;
20
+
21
+ /**
22
+ * Index of a `system / ScopeInfo` node within the heap snapshot's node array.
23
+ */
24
+ type ScopeInfoNodeIndex = Platform.Brand.Brand<number, 'ScopeInfoNodeIndex'>;
25
+
26
+ /**
27
+ * Index of a V8 `Context` node within the heap snapshot's node array.
28
+ */
29
+ type ContextNodeIndex = Platform.Brand.Brand<number, 'ContextNodeIndex'>;
30
+
31
+ /**
32
+ * Values addressed by scope. Since a `ScopeId` is only unique within its
33
+ * script, scopes are keyed by script first.
34
+ */
35
+ type ByScope<T> = Map<ScriptNodeIndex, Map<ScopeId, T>>;
36
+
37
+ interface EmbeddedScope {
38
+ scriptNodeIndex: ScriptNodeIndex;
39
+ scopeId: ScopeId;
40
+ parent?: EmbeddedScope;
41
+ children: EmbeddedScope[];
42
+ variables: VariableDefinition[];
43
+ }
44
+
45
+ interface VariableDefinition {
46
+ name: string;
47
+ slotIndex: number;
48
+ uses: EmbeddedScope[];
49
+ }
50
+
51
+ interface ScriptInfo {
52
+ name: string;
53
+ nodeId: number;
54
+ }
55
+
56
+ interface MatchedContext {
57
+ contextNodeIndex: ContextNodeIndex;
58
+ contextNodeId: number;
59
+ fieldValueNodeIndexes: number[];
60
+ }
61
+
62
+ interface ScopeAccumulator {
63
+ scopeInfoNodeIndex: ScopeInfoNodeIndex;
64
+ scopeInfoNodeId: number;
65
+ scriptNodeIndex: ScriptNodeIndex;
66
+ scriptNodeId: number;
67
+ scriptName: string;
68
+ scopeName?: string;
69
+ scopeStart: number;
70
+ scopeEnd: number;
71
+ scope: EmbeddedScope;
72
+ fieldNames: string[];
73
+ contexts: MatchedContext[];
74
+ }
75
+
76
+ interface LiveClosure {
77
+ contextNodeIndex: ContextNodeIndex;
78
+ scriptNodeIndex: ScriptNodeIndex;
79
+ scopeId: ScopeId;
80
+ }
81
+
82
+ interface LiveFunction {
83
+ // All context objects reachable through live closures/JSFunctions of this function
84
+ // definition. This includes all context objects reachable through their 'previous'
85
+ // predecessors.
86
+ contextNodeIndexes: Set<ContextNodeIndex>;
87
+ }
88
+
89
+ interface ContextNodeInfo {
90
+ contextNodeIndex: ContextNodeIndex;
91
+ contextNodeId: number;
92
+ scopeInfoNodeIndex: ScopeInfoNodeIndex;
93
+ }
94
+
95
+ /**
96
+ * Everything collected while walking over all nodes of the snapshot once.
97
+ */
98
+ interface HeapScan {
99
+ scripts: Map<ScriptNodeIndex, ScriptInfo>;
100
+ contextNodes: ContextNodeInfo[];
101
+ liveClosures: LiveClosure[];
102
+ // Maps a ScopeInfo to the script it belongs to. Only contains the ScopeInfos
103
+ // directly referenced by a SharedFunctionInfo, the remaining ones are resolved
104
+ // and memoized on demand by `resolveScopeInfoScriptNodeIndex`.
105
+ //
106
+ // A stored `undefined` is a memoized failed lookup, i.e. walking the
107
+ // `outer_scope_info` chain of that ScopeInfo did not reach a script. It is
108
+ // therefore not the same as an absent entry, which merely means "not resolved
109
+ // yet", so lookups have to distinguish the two through `has()`.
110
+ scopeInfoScriptNodeIndexes: Map<ScopeInfoNodeIndex, ScriptNodeIndex|undefined>;
111
+ }
112
+
113
+ export function analyzeContexts(snapshot: HeapSnapshot): HeapSnapshotModel.HeapSnapshotModel.ContextAnalysisResult {
114
+ // (1) Parse embedded scope metadata from the snapshot.
115
+ const scopesByScript = parseEmbeddedScopes(snapshot);
116
+
117
+ // (2) Scan the heap to collect scripts, contexts, live closures, and associate function ScopeInfos
118
+ // with scripts.
119
+ const scan = scanHeap(snapshot);
120
+
121
+ // (3) Group live closures by function scope and record the context chains they can reach.
122
+ const liveFunctionsByScript = buildLiveFunctions(snapshot, scan.liveClosures);
123
+
124
+ // (4) Correlate contexts and their field values with embedded scopes.
125
+ const {scopes, scriptsWithoutScopes} = correlateContextsWithScopes(snapshot, scan, scopesByScript);
126
+
127
+ // (5) Classify fields per context and build the result objects.
128
+ const scopeAnalyses = classifyFields(snapshot, scopes, liveFunctionsByScript);
129
+
130
+ // (6) Sort dead fields, contexts, and scopes, then return the analysis.
131
+ return sortAndBuildResult(scopeAnalyses, scriptsWithoutScopes);
132
+ }
133
+
134
+ function parseEmbeddedScopes(snapshot: HeapSnapshot): ByScope<EmbeddedScope> {
135
+ const scopesByScript: ByScope<EmbeddedScope> = new Map();
136
+ const profile = snapshot.profile;
137
+ const rawScopes = profile.scopes ?? [];
138
+ const rawVars = profile.scope_context_vars ?? [];
139
+ const rawUses = profile.scope_uses ?? [];
140
+ const meta = profile.snapshot.meta;
141
+
142
+ const scopeFields = meta.scope_fields ?? [];
143
+ const varFields = meta.scope_context_var_fields ?? [];
144
+ const useFields = meta.scope_use_fields ?? [];
145
+
146
+ const scopeStride = scopeFields.length;
147
+ const varStride = varFields.length;
148
+ const useStride = useFields.length;
149
+
150
+ if (scopeStride === 0) {
151
+ return scopesByScript;
152
+ }
153
+
154
+ const scriptNodeIndexOffset = scopeFields.indexOf('script_node_index');
155
+ const scopeIdOffset = scopeFields.indexOf('scope_id');
156
+ const depthOffset = scopeFields.indexOf('depth');
157
+ const varsCountOffset = scopeFields.indexOf('scope_context_vars_count');
158
+ const usesCountOffset = scopeFields.indexOf('scope_uses_count');
159
+
160
+ const varNameOffset = varFields.indexOf('name');
161
+ const useDeclaringScopeIdOffset = useFields.indexOf('declaring_scope_id');
162
+ const useSlotIndexOffset = useFields.indexOf('slot_index');
163
+
164
+ const strings = snapshot.strings;
165
+
166
+ // Pass 1: Create all scopes and variable definitions, and link parents via depth.
167
+ let varOffset = 0;
168
+ let currentScriptNodeIndex: ScriptNodeIndex|undefined;
169
+ const scopeStack: EmbeddedScope[] = [];
170
+
171
+ for (let i = 0; i < rawScopes.length; i += scopeStride) {
172
+ const scriptNodeIndex = rawScopes[i + scriptNodeIndexOffset] as ScriptNodeIndex;
173
+ const scopeId = rawScopes[i + scopeIdOffset] as ScopeId;
174
+ const depth = rawScopes[i + depthOffset];
175
+ const varsCount = rawScopes[i + varsCountOffset];
176
+
177
+ const variables: VariableDefinition[] = [];
178
+ for (let j = 0; j < varsCount; ++j) {
179
+ const nameIndex = rawVars[varOffset + j * varStride + varNameOffset];
180
+ variables.push({
181
+ name: strings[nameIndex],
182
+ slotIndex: j,
183
+ uses: [],
184
+ });
185
+ }
186
+ varOffset += varsCount * varStride;
187
+
188
+ const scope: EmbeddedScope = {
189
+ scriptNodeIndex,
190
+ scopeId,
191
+ children: [],
192
+ variables,
193
+ };
194
+
195
+ if (currentScriptNodeIndex !== scriptNodeIndex) {
196
+ currentScriptNodeIndex = scriptNodeIndex;
197
+ scopeStack.length = 0;
198
+ }
199
+
200
+ if (depth > 0) {
201
+ const parent = scopeStack[depth - 1];
202
+ scope.parent = parent;
203
+ parent.children.push(scope);
204
+ }
205
+
206
+ // The tree hierarchy is encoded through a depth field in the heap snapshot. Here we
207
+ // set the scope as the innermost scope by trimming the stack.
208
+ scopeStack[depth] = scope;
209
+ scopeStack.length = depth + 1; // Trim the stack.
210
+
211
+ let scriptScopes = scopesByScript.get(scriptNodeIndex);
212
+ if (!scriptScopes) {
213
+ scriptScopes = new Map<ScopeId, EmbeddedScope>();
214
+ scopesByScript.set(scriptNodeIndex, scriptScopes);
215
+ }
216
+ scriptScopes.set(scopeId, scope);
217
+ }
218
+
219
+ // Pass 2: Resolve variable uses directly from rawUses.
220
+ let useOffset = 0;
221
+ for (let i = 0; i < rawScopes.length; i += scopeStride) {
222
+ const scriptNodeIndex = rawScopes[i + scriptNodeIndexOffset] as ScriptNodeIndex;
223
+ const scopeId = rawScopes[i + scopeIdOffset] as ScopeId;
224
+ const usesCount = rawScopes[i + usesCountOffset];
225
+
226
+ const scope = scopesByScript.get(scriptNodeIndex)?.get(scopeId);
227
+ if (scope) {
228
+ for (let j = 0; j < usesCount; ++j) {
229
+ const declaringScopeId = rawUses[useOffset + j * useStride + useDeclaringScopeIdOffset] as ScopeId;
230
+ const slotIndex = rawUses[useOffset + j * useStride + useSlotIndexOffset];
231
+ const declaringScope = scopesByScript.get(scriptNodeIndex)?.get(declaringScopeId);
232
+ const variable = declaringScope?.variables[slotIndex];
233
+ if (variable && !variable.uses.includes(scope)) {
234
+ variable.uses.push(scope);
235
+ }
236
+ }
237
+ }
238
+ useOffset += usesCount * useStride;
239
+ }
240
+
241
+ return scopesByScript;
242
+ }
243
+
244
+ function scanHeap(snapshot: HeapSnapshot): HeapScan {
245
+ const scripts = new Map<ScriptNodeIndex, ScriptInfo>();
246
+ const contextNodes: ContextNodeInfo[] = [];
247
+ const liveClosures: LiveClosure[] = [];
248
+ const scopeInfoScriptNodeIndexes = new Map<ScopeInfoNodeIndex, ScriptNodeIndex|undefined>();
249
+ const node = snapshot.createNode();
250
+ const nodes = snapshot.nodes;
251
+ const nodeFieldCount = snapshot.nodeFieldCount;
252
+ const nodeClosureType = snapshot.nodeClosureType;
253
+
254
+ for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) {
255
+ node.nodeIndex = nodeIndex;
256
+ const rawName = node.rawName();
257
+
258
+ if (rawName.startsWith('system / Script')) {
259
+ processScript(scripts, node);
260
+ } else if (snapshot.isContextObject(node)) {
261
+ processContext(contextNodes, node);
262
+ } else if (node.rawType() === nodeClosureType) {
263
+ processClosure(liveClosures, node);
264
+ } else if (rawName.startsWith('system / SharedFunctionInfo')) {
265
+ processSharedFunctionInfo(scopeInfoScriptNodeIndexes, node);
266
+ }
267
+ }
268
+
269
+ return {scripts, contextNodes, liveClosures, scopeInfoScriptNodeIndexes};
270
+ }
271
+
272
+ function processScript(scripts: Map<ScriptNodeIndex, ScriptInfo>, node: HeapSnapshotNode): void {
273
+ const rawName = node.rawName();
274
+ const scriptNodeIndex = node.nodeIndex as ScriptNodeIndex;
275
+ const scriptNamePrefix = 'system / Script / ';
276
+ let name = rawName.startsWith(scriptNamePrefix) ? rawName.substring(scriptNamePrefix.length) : '';
277
+ if (!name) {
278
+ name = node.findInternalEdgeTarget('source_url')?.name() ?? '';
279
+ }
280
+ scripts.set(scriptNodeIndex, {
281
+ name,
282
+ nodeId: node.id(),
283
+ });
284
+ }
285
+
286
+ function processContext(contextNodes: ContextNodeInfo[], node: HeapSnapshotNode): void {
287
+ const scopeInfoNodeIndex = node.findInternalEdgeTarget('scope_info')?.nodeIndex as ScopeInfoNodeIndex | undefined;
288
+ if (scopeInfoNodeIndex === undefined) {
289
+ return;
290
+ }
291
+ contextNodes.push({
292
+ contextNodeIndex: node.nodeIndex as ContextNodeIndex,
293
+ contextNodeId: node.id(),
294
+ scopeInfoNodeIndex,
295
+ });
296
+ }
297
+
298
+ function processClosure(liveClosures: LiveClosure[], node: HeapSnapshotNode): void {
299
+ const sharedFunctionInfo = node.findInternalEdgeTarget('shared');
300
+ const closureContext = node.findInternalEdgeTarget('context');
301
+ if (!sharedFunctionInfo || !closureContext) {
302
+ return;
303
+ }
304
+ const script = sharedFunctionInfo.findInternalEdgeTarget('script');
305
+ if (!script) {
306
+ return;
307
+ }
308
+ const scopeId = sharedFunctionInfo.findInternalEdgeTarget('scope_id')?.nodeValueAsInt() as ScopeId | undefined;
309
+ if (scopeId !== undefined) {
310
+ liveClosures.push({
311
+ contextNodeIndex: closureContext.nodeIndex as ContextNodeIndex,
312
+ scriptNodeIndex: script.nodeIndex as ScriptNodeIndex,
313
+ scopeId,
314
+ });
315
+ }
316
+ }
317
+
318
+ function processSharedFunctionInfo(scopeInfoScriptNodeIndexes: Map<ScopeInfoNodeIndex, ScriptNodeIndex|undefined>,
319
+ node: HeapSnapshotNode): void {
320
+ const scopeInfo = node.findInternalEdgeTarget('name_or_scope_info');
321
+ const script = node.findInternalEdgeTarget('script');
322
+ if (scopeInfo?.rawName() !== 'system / ScopeInfo' || !script?.rawName().startsWith('system / Script')) {
323
+ return;
324
+ }
325
+ scopeInfoScriptNodeIndexes.set(scopeInfo.nodeIndex as ScopeInfoNodeIndex, script.nodeIndex as ScriptNodeIndex);
326
+ }
327
+
328
+ function buildLiveFunctions(snapshot: HeapSnapshot, liveClosures: LiveClosure[]): ByScope<LiveFunction> {
329
+ const liveFunctionsByScript: ByScope<LiveFunction> = new Map();
330
+ const node = snapshot.createNode();
331
+
332
+ for (const closure of liveClosures) {
333
+ let scriptFunctions = liveFunctionsByScript.get(closure.scriptNodeIndex);
334
+ if (!scriptFunctions) {
335
+ scriptFunctions = new Map<ScopeId, LiveFunction>();
336
+ liveFunctionsByScript.set(closure.scriptNodeIndex, scriptFunctions);
337
+ }
338
+ let liveFunction = scriptFunctions.get(closure.scopeId);
339
+ if (!liveFunction) {
340
+ liveFunction = {
341
+ contextNodeIndexes: new Set(),
342
+ };
343
+ scriptFunctions.set(closure.scopeId, liveFunction);
344
+ }
345
+ let contextNodeIndex: ContextNodeIndex|undefined = closure.contextNodeIndex;
346
+ while (contextNodeIndex !== undefined && !liveFunction.contextNodeIndexes.has(contextNodeIndex)) {
347
+ node.nodeIndex = contextNodeIndex;
348
+ if (!snapshot.isContextObject(node)) {
349
+ break;
350
+ }
351
+ liveFunction.contextNodeIndexes.add(contextNodeIndex);
352
+ contextNodeIndex = node.findInternalEdgeTarget('previous')?.nodeIndex as ContextNodeIndex | undefined;
353
+ }
354
+ }
355
+
356
+ return liveFunctionsByScript;
357
+ }
358
+
359
+ function correlateContextsWithScopes(snapshot: HeapSnapshot, scan: HeapScan, scopesByScript: ByScope<EmbeddedScope>): {
360
+ scopes: Map<ScopeInfoNodeIndex, ScopeAccumulator>,
361
+ scriptsWithoutScopes: HeapSnapshotModel.HeapSnapshotModel.ScriptWithoutScopes[],
362
+ } {
363
+ const scriptsWithoutScopesByScript =
364
+ new Map<ScriptNodeIndex, HeapSnapshotModel.HeapSnapshotModel.ScriptWithoutScopes>();
365
+ const scopes = new Map<ScopeInfoNodeIndex, ScopeAccumulator>();
366
+ const node = snapshot.createNode();
367
+
368
+ for (const context of scan.contextNodes) {
369
+ const scopeInfoNodeIndex = context.scopeInfoNodeIndex;
370
+ const existingScope = scopes.get(scopeInfoNodeIndex);
371
+ if (existingScope) {
372
+ const fieldValueNodeIndexes: number[] = [];
373
+ node.nodeIndex = context.contextNodeIndex;
374
+ for (const edges = node.edges(); edges.hasNext(); edges.next()) {
375
+ const edge = edges.item();
376
+ if (edge.type() === 'context') {
377
+ fieldValueNodeIndexes.push(edge.nodeIndex());
378
+ }
379
+ }
380
+ existingScope.contexts.push({
381
+ contextNodeIndex: context.contextNodeIndex,
382
+ contextNodeId: context.contextNodeId,
383
+ fieldValueNodeIndexes,
384
+ });
385
+ continue;
386
+ }
387
+
388
+ node.nodeIndex = scopeInfoNodeIndex;
389
+ const scopeId = node.findInternalEdgeTarget('scope_id')?.nodeValueAsInt() as ScopeId | undefined;
390
+ if (scopeId === undefined) {
391
+ continue;
392
+ }
393
+
394
+ const scopeInfoNodeId = node.id();
395
+ const scopeStart = node.findInternalEdgeTarget('start_position')?.nodeValueAsInt() ?? 0;
396
+ const scopeEnd = node.findInternalEdgeTarget('end_position')?.nodeValueAsInt() ?? 0;
397
+ const scopeName = node.findInternalEdgeTarget('function_name')?.name();
398
+
399
+ const scriptNodeIndex =
400
+ resolveScopeInfoScriptNodeIndex(snapshot, scan.scopeInfoScriptNodeIndexes, scopeInfoNodeIndex);
401
+ const script = scriptNodeIndex !== undefined ? scan.scripts.get(scriptNodeIndex) : undefined;
402
+ if (scriptNodeIndex === undefined || !script) {
403
+ continue;
404
+ }
405
+
406
+ const existingScriptWithoutScopes = scriptsWithoutScopesByScript.get(scriptNodeIndex);
407
+ if (existingScriptWithoutScopes) {
408
+ existingScriptWithoutScopes.contextCount++;
409
+ continue;
410
+ }
411
+
412
+ const embeddedScopes = scopesByScript.get(scriptNodeIndex);
413
+ if (!embeddedScopes) {
414
+ scriptsWithoutScopesByScript.set(scriptNodeIndex, {
415
+ scriptNodeIndex,
416
+ scriptNodeId: script.nodeId,
417
+ scriptName: script.name,
418
+ contextCount: 1,
419
+ });
420
+ continue;
421
+ }
422
+
423
+ const embeddedScope = embeddedScopes.get(scopeId);
424
+ if (!embeddedScope) {
425
+ continue;
426
+ }
427
+
428
+ const fieldNames: string[] = [];
429
+ const fieldValueNodeIndexes: number[] = [];
430
+ node.nodeIndex = context.contextNodeIndex;
431
+ for (const edges = node.edges(); edges.hasNext(); edges.next()) {
432
+ const edge = edges.item();
433
+ if (edge.type() === 'context') {
434
+ fieldNames.push(edge.name());
435
+ fieldValueNodeIndexes.push(edge.nodeIndex());
436
+ }
437
+ }
438
+
439
+ scopes.set(scopeInfoNodeIndex, {
440
+ scopeInfoNodeIndex,
441
+ scopeInfoNodeId,
442
+ scriptNodeIndex,
443
+ scriptNodeId: script.nodeId,
444
+ scriptName: script.name,
445
+ scopeName,
446
+ scopeStart,
447
+ scopeEnd,
448
+ scope: embeddedScope,
449
+ fieldNames,
450
+ contexts: [{
451
+ contextNodeIndex: context.contextNodeIndex,
452
+ contextNodeId: context.contextNodeId,
453
+ fieldValueNodeIndexes,
454
+ }],
455
+ });
456
+ }
457
+
458
+ return {
459
+ scopes,
460
+ scriptsWithoutScopes: [...scriptsWithoutScopesByScript.values()],
461
+ };
462
+ }
463
+
464
+ function resolveScopeInfoScriptNodeIndex(
465
+ snapshot: HeapSnapshot,
466
+ scopeInfoScriptNodeIndexes: Map<ScopeInfoNodeIndex, ScriptNodeIndex|undefined>,
467
+ scopeInfoNodeIndex: ScopeInfoNodeIndex,
468
+ ): ScriptNodeIndex|undefined {
469
+ if (scopeInfoScriptNodeIndexes.has(scopeInfoNodeIndex)) {
470
+ return scopeInfoScriptNodeIndexes.get(scopeInfoNodeIndex);
471
+ }
472
+ const visited: ScopeInfoNodeIndex[] = [];
473
+ const seen = new Set<ScopeInfoNodeIndex>();
474
+ const node = snapshot.createNode();
475
+ let currentNodeIndex = scopeInfoNodeIndex;
476
+ let scriptNodeIndex: ScriptNodeIndex|undefined;
477
+ while (!seen.has(currentNodeIndex)) {
478
+ if (scopeInfoScriptNodeIndexes.has(currentNodeIndex)) {
479
+ scriptNodeIndex = scopeInfoScriptNodeIndexes.get(currentNodeIndex);
480
+ break;
481
+ }
482
+ seen.add(currentNodeIndex);
483
+ visited.push(currentNodeIndex);
484
+ node.nodeIndex = currentNodeIndex;
485
+ const outerScopeInfo = node.findInternalEdgeTarget('outer_scope_info');
486
+ if (!outerScopeInfo) {
487
+ break;
488
+ }
489
+ currentNodeIndex = outerScopeInfo.nodeIndex as ScopeInfoNodeIndex;
490
+ }
491
+ for (const visitedNodeIndex of visited) {
492
+ scopeInfoScriptNodeIndexes.set(visitedNodeIndex, scriptNodeIndex);
493
+ }
494
+ return scriptNodeIndex;
495
+ }
496
+
497
+ function classifyFields(
498
+ snapshot: HeapSnapshot,
499
+ scopes: Map<ScopeInfoNodeIndex, ScopeAccumulator>,
500
+ liveFunctionsByScript: ByScope<LiveFunction>,
501
+ ): HeapSnapshotModel.HeapSnapshotModel.ScopeAnalysis[] {
502
+ const scopeAnalyses: HeapSnapshotModel.HeapSnapshotModel.ScopeAnalysis[] = [];
503
+ const node = snapshot.createNode();
504
+
505
+ for (const scope of scopes.values()) {
506
+ const scopeContexts: HeapSnapshotModel.HeapSnapshotModel.ContextAnalysis[] = [];
507
+ for (const context of scope.contexts) {
508
+ let contextDeadFieldsRetainedSizeSum = 0;
509
+ const deadFields: HeapSnapshotModel.HeapSnapshotModel.ContextField[] = [];
510
+ for (let fieldIndex = 0; fieldIndex < context.fieldValueNodeIndexes.length; ++fieldIndex) {
511
+ const fieldName = scope.fieldNames[fieldIndex];
512
+ const variable = scope.scope.variables.find(v => v.name === fieldName);
513
+ if (!variable) {
514
+ // Missing variable definition. Nothing to check in this case.
515
+ continue;
516
+ }
517
+ if (isVariableUsedInContext(liveFunctionsByScript, scope.scriptNodeIndex, context.contextNodeIndex, variable)) {
518
+ // Only report dead fields.
519
+ continue;
520
+ }
521
+ const fieldValueNodeIndex = context.fieldValueNodeIndexes[fieldIndex];
522
+ node.nodeIndex = fieldValueNodeIndex;
523
+ const retainedSize = node.retainedSize();
524
+ deadFields.push({
525
+ name: fieldName,
526
+ valueNodeIndex: fieldValueNodeIndex,
527
+ valueNodeId: node.id(),
528
+ valueName: node.name(),
529
+ valueType: node.type(),
530
+ selfSize: node.selfSize(),
531
+ retainedSize,
532
+ });
533
+ contextDeadFieldsRetainedSizeSum += retainedSize;
534
+ }
535
+ if (deadFields.length === 0) {
536
+ continue;
537
+ }
538
+ node.nodeIndex = context.contextNodeIndex;
539
+ scopeContexts.push({
540
+ contextNodeIndex: context.contextNodeIndex,
541
+ contextNodeId: node.id(),
542
+ retainedSize: node.retainedSize(),
543
+ deadFieldsRetainedSizeSum: contextDeadFieldsRetainedSizeSum,
544
+ deadFields,
545
+ });
546
+ }
547
+ if (scopeContexts.length === 0) {
548
+ continue;
549
+ }
550
+ scopeAnalyses.push({
551
+ scopeInfoNodeIndex: scope.scopeInfoNodeIndex,
552
+ scopeInfoNodeId: scope.scopeInfoNodeId,
553
+ scriptNodeIndex: scope.scriptNodeIndex,
554
+ scriptNodeId: scope.scriptNodeId,
555
+ scriptName: scope.scriptName,
556
+ scopeName: scope.scopeName,
557
+ scopeStart: scope.scopeStart,
558
+ scopeEnd: scope.scopeEnd,
559
+ contextFieldCount: scope.fieldNames.length,
560
+ contexts: scopeContexts,
561
+ });
562
+ }
563
+
564
+ return scopeAnalyses;
565
+ }
566
+
567
+ // Determines whether a specific allocated Context instance (`contextNodeIndex`)
568
+ // is reachable by any live closure (or inner function instantiable by a live closure)
569
+ // that accesses this variable.
570
+ function isVariableUsedInContext(
571
+ liveFunctionsByScript: ByScope<LiveFunction>,
572
+ scriptNodeIndex: ScriptNodeIndex,
573
+ contextNodeIndex: ContextNodeIndex,
574
+ variable: VariableDefinition,
575
+ ): boolean {
576
+ const liveFunctionsByScopeId = liveFunctionsByScript.get(scriptNodeIndex);
577
+ if (!liveFunctionsByScopeId) {
578
+ return false;
579
+ }
580
+ for (const usingScope of variable.uses) {
581
+ // Walk up the lexical scope chain from the variable's use site.
582
+ // This checks if the use site is inside a live closure itself (or one
583
+ // of its nested block scopes) OR inside an uninstantiated function
584
+ // enclosed by a live closure.
585
+ //
586
+ // Executing a live outer closure can instantiate any function nested
587
+ // inside it, so those inner functions can access the outer closure's
588
+ // context chain even when no closure exists for them yet on the heap.
589
+ //
590
+ // Example:
591
+ // function outer() {
592
+ // const x = 1;
593
+ // return function middle() { // <- liveFunction (on heap)
594
+ // return function inner() {
595
+ // return x; // <- use site
596
+ // };
597
+ // };
598
+ // }
599
+ // No closure object exists on the heap for `inner` yet (so `inner` is
600
+ // not in `liveFunctionsByScript`). However, walking up from `inner`'s
601
+ // scope reaches `middle`. Since `middle` is a live closure on the heap
602
+ // whose context chain includes `outer`'s Context containing `x`,
603
+ // executing `middle()` in the future will instantiate `inner`, giving it
604
+ // access to `outer`'s Context containing `x`.
605
+ for (let curr: EmbeddedScope|undefined = usingScope; curr; curr = curr.parent) {
606
+ const liveFunction = liveFunctionsByScopeId.get(curr.scopeId);
607
+ if (liveFunction?.contextNodeIndexes.has(contextNodeIndex)) {
608
+ return true;
609
+ }
610
+ }
611
+ }
612
+ return false;
613
+ }
614
+
615
+ function sortAndBuildResult(
616
+ scopeAnalyses: HeapSnapshotModel.HeapSnapshotModel.ScopeAnalysis[],
617
+ scriptsWithoutScopes: HeapSnapshotModel.HeapSnapshotModel.ScriptWithoutScopes[],
618
+ ): HeapSnapshotModel.HeapSnapshotModel.ContextAnalysisResult {
619
+ const compareContexts = (
620
+ left: HeapSnapshotModel.HeapSnapshotModel.ContextAnalysis,
621
+ right: HeapSnapshotModel.HeapSnapshotModel.ContextAnalysis,
622
+ ): number => {
623
+ return right.deadFieldsRetainedSizeSum - left.deadFieldsRetainedSizeSum || right.retainedSize - left.retainedSize ||
624
+ left.contextNodeIndex - right.contextNodeIndex;
625
+ };
626
+ for (const scope of scopeAnalyses) {
627
+ for (const context of scope.contexts) {
628
+ context.deadFields.sort((left, right) => right.retainedSize - left.retainedSize);
629
+ }
630
+ scope.contexts.sort(compareContexts);
631
+ }
632
+ scopeAnalyses.sort((left, right) => compareContexts(left.contexts[0], right.contexts[0]));
633
+ scriptsWithoutScopes.sort((left, right) => left.scriptNodeIndex - right.scriptNodeIndex);
634
+ return {scopes: scopeAnalyses, scriptsWithoutScopes};
635
+ }