chrome-devtools-mcp 0.0.2 → 0.2.0

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 (69) hide show
  1. package/README.md +6 -3
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Settings.js +3 -32
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +35 -8
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/root/Runtime.js +4 -1
  5. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +4 -4
  6. package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +12 -0
  7. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +366 -0
  8. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +366 -0
  9. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -0
  10. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIQueries.js +105 -0
  11. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CSSWorkspaceBinding.js +243 -0
  12. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +407 -0
  13. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +128 -0
  14. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +992 -0
  15. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +574 -0
  16. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DefaultScriptMapping.js +112 -0
  17. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/FileUtils.js +186 -0
  18. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/LiveLocation.js +60 -0
  19. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/NetworkProject.js +107 -0
  20. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/PresentationConsoleMessageHelper.js +244 -0
  21. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceMapping.js +473 -0
  22. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceScriptMapping.js +399 -0
  23. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceUtils.js +87 -0
  24. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/SASSSourceMapping.js +181 -0
  25. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/StylesSourceMapping.js +268 -0
  26. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/TempFile.js +55 -0
  27. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/bindings.js +20 -0
  28. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js +283 -0
  29. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/crux-manager.js +4 -0
  30. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +775 -0
  31. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +1706 -0
  32. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/emulation.js +6 -0
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +131 -0
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/ScriptFormatter.js +77 -0
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/formatter.js +6 -0
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/GeometryImpl.js +347 -0
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/geometry.js +4 -0
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +626 -0
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeChainModel.js +59 -0
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeTreeCache.js +32 -0
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/source_map_scopes.js +7 -0
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +4 -0
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +67 -0
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +97 -0
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/Trie.js +113 -0
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace.js +5 -0
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace_impl.js +7 -0
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/text_utils/TextUtils.js +23 -0
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +1 -1
  50. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +1 -1
  51. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +5 -4
  52. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +199 -0
  53. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +4 -0
  54. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/FileManager.js +64 -0
  55. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/IgnoreListManager.js +511 -0
  56. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/SearchConfig.js +113 -0
  57. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/UISourceCode.js +563 -0
  58. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/WorkspaceImpl.js +204 -0
  59. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/workspace.js +9 -0
  60. package/build/src/McpContext.js +24 -9
  61. package/build/src/McpResponse.js +3 -3
  62. package/build/src/browser.js +3 -1
  63. package/build/src/index.js +1 -1
  64. package/build/src/tools/input.js +7 -7
  65. package/build/src/tools/performance.js +29 -2
  66. package/build/src/tools/screenshot.js +1 -1
  67. package/build/src/tools/script.js +40 -14
  68. package/build/src/trace-processing/parse.js +26 -22
  69. package/package.json +9 -7
@@ -0,0 +1,32 @@
1
+ // Copyright 2023 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
+ import * as Formatter from '../formatter/formatter.js';
5
+ import * as TextUtils from '../text_utils/text_utils.js';
6
+ /** If a script failed to parse, we stash null in order to prevent unnecessary re-parsing */
7
+ const scopeTrees = new WeakMap();
8
+ /**
9
+ * Computes and caches the scope tree for `script`.
10
+ *
11
+ * We use {@link SDK.Script.Script} as a key to uniquely identify scripts.
12
+ * {@link SDK.Script.Script} boils down to "target" + "script ID". This
13
+ * duplicates work in case of identitical script running on multiple targets
14
+ * (e.g. workers).
15
+ */
16
+ export function scopeTreeForScript(script) {
17
+ let promise = scopeTrees.get(script);
18
+ if (promise === undefined) {
19
+ promise = script.requestContentData().then(content => {
20
+ if (TextUtils.ContentData.ContentData.isError(content)) {
21
+ return null;
22
+ }
23
+ const sourceType = script.isModule ? 'module' : 'script';
24
+ return Formatter.FormatterWorkerPool.formatterWorkerPool()
25
+ .javaScriptScopeTree(content.text, sourceType)
26
+ .catch(() => null);
27
+ });
28
+ scopeTrees.set(script, promise);
29
+ }
30
+ // We intentionally return `null` here if the script already failed to parse once.
31
+ return promise;
32
+ }
@@ -0,0 +1,7 @@
1
+ // Copyright 2022 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
+ import * as NamesResolver from './NamesResolver.js';
5
+ import * as ScopeChainModel from './ScopeChainModel.js';
6
+ import * as ScopeTreeCache from './ScopeTreeCache.js';
7
+ export { NamesResolver, ScopeChainModel, ScopeTreeCache, };
@@ -0,0 +1,4 @@
1
+ // Copyright 2025 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
+ export {};
@@ -0,0 +1,67 @@
1
+ // Copyright 2025 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
+ import * as Common from '../../core/common/common.js';
5
+ export class StackTraceImpl extends Common.ObjectWrapper.ObjectWrapper {
6
+ syncFragment;
7
+ asyncFragments;
8
+ constructor(syncFragment, asyncFragments) {
9
+ super();
10
+ this.syncFragment = syncFragment;
11
+ this.asyncFragments = asyncFragments;
12
+ syncFragment.stackTraces.add(this);
13
+ this.asyncFragments.forEach(asyncFragment => asyncFragment.fragment.stackTraces.add(this));
14
+ }
15
+ }
16
+ export class FragmentImpl {
17
+ node;
18
+ stackTraces = new Set();
19
+ /**
20
+ * Fragments are deduplicated based on the node.
21
+ *
22
+ * In turn, each fragment can be part of multiple stack traces.
23
+ */
24
+ static getOrCreate(node) {
25
+ if (!node.fragment) {
26
+ node.fragment = new FragmentImpl(node);
27
+ }
28
+ return node.fragment;
29
+ }
30
+ constructor(node) {
31
+ this.node = node;
32
+ }
33
+ get frames() {
34
+ const frames = [];
35
+ for (const node of this.node.getCallStack()) {
36
+ frames.push(...node.frames);
37
+ }
38
+ return frames;
39
+ }
40
+ }
41
+ export class AsyncFragmentImpl {
42
+ description;
43
+ fragment;
44
+ constructor(description, fragment) {
45
+ this.description = description;
46
+ this.fragment = fragment;
47
+ }
48
+ get frames() {
49
+ return this.fragment.frames;
50
+ }
51
+ }
52
+ export class FrameImpl {
53
+ url;
54
+ uiSourceCode;
55
+ name;
56
+ line;
57
+ column;
58
+ missingDebugInfo;
59
+ constructor(url, uiSourceCode, name, line, column, missingDebugInfo) {
60
+ this.url = url;
61
+ this.uiSourceCode = uiSourceCode;
62
+ this.name = name;
63
+ this.line = line;
64
+ this.column = column;
65
+ this.missingDebugInfo = missingDebugInfo;
66
+ }
67
+ }
@@ -0,0 +1,97 @@
1
+ // Copyright 2025 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
+ import * as SDK from '../../core/sdk/sdk.js';
5
+ // eslint-disable-next-line rulesdir/es-modules-import
6
+ import * as StackTrace from './stack_trace.js';
7
+ import { AsyncFragmentImpl, FragmentImpl, FrameImpl, StackTraceImpl } from './StackTraceImpl.js';
8
+ import { Trie } from './Trie.js';
9
+ /**
10
+ * The {@link StackTraceModel} is a thin wrapper around a fragment trie.
11
+ *
12
+ * We want to store stack trace fragments per target so a SDKModel is the natural choice.
13
+ */
14
+ export class StackTraceModel extends SDK.SDKModel.SDKModel {
15
+ #trie = new Trie();
16
+ /** @returns the {@link StackTraceModel} for the target, or the model for the primaryPageTarget when passing null/undefined */
17
+ static #modelForTarget(target) {
18
+ const model = (target ?? SDK.TargetManager.TargetManager.instance().primaryPageTarget())?.model(StackTraceModel);
19
+ if (!model) {
20
+ throw new Error('Unable to find StackTraceModel');
21
+ }
22
+ return model;
23
+ }
24
+ async createFromProtocolRuntime(stackTrace, rawFramesToUIFrames) {
25
+ const translatePromises = [];
26
+ const fragment = this.#createFragment(stackTrace.callFrames);
27
+ translatePromises.push(this.#translateFragment(fragment, rawFramesToUIFrames));
28
+ const asyncFragments = [];
29
+ const debuggerModel = this.target().model(SDK.DebuggerModel.DebuggerModel);
30
+ if (debuggerModel) {
31
+ for await (const { stackTrace: asyncStackTrace, target } of debuggerModel.iterateAsyncParents(stackTrace)) {
32
+ const model = StackTraceModel.#modelForTarget(target);
33
+ const asyncFragment = model.#createFragment(asyncStackTrace.callFrames);
34
+ translatePromises.push(model.#translateFragment(asyncFragment, rawFramesToUIFrames));
35
+ asyncFragments.push(new AsyncFragmentImpl(asyncStackTrace.description ?? '', asyncFragment));
36
+ }
37
+ }
38
+ await Promise.all(translatePromises);
39
+ return new StackTraceImpl(fragment, asyncFragments);
40
+ }
41
+ /** Trigger re-translation of all fragments with the provide script in their call stack */
42
+ async scriptInfoChanged(script, translateRawFrames) {
43
+ const translatePromises = [];
44
+ let stackTracesToUpdate = new Set();
45
+ for (const fragment of this.#affectedFragments(script)) {
46
+ // We trigger re-translation only for fragments of leaf-nodes. Any fragment along the ancestor-chain
47
+ // is re-translated as a side-effect.
48
+ // We just need to remember the stack traces of the skipped over fragments, so we can send the
49
+ // UPDATED event also to them.
50
+ if (fragment.node.children.length === 0) {
51
+ translatePromises.push(this.#translateFragment(fragment, translateRawFrames));
52
+ }
53
+ stackTracesToUpdate = stackTracesToUpdate.union(fragment.stackTraces);
54
+ }
55
+ await Promise.all(translatePromises);
56
+ for (const stackTrace of stackTracesToUpdate) {
57
+ stackTrace.dispatchEventToListeners("UPDATED" /* StackTrace.StackTrace.Events.UPDATED */);
58
+ }
59
+ }
60
+ #createFragment(frames) {
61
+ return FragmentImpl.getOrCreate(this.#trie.insert(frames));
62
+ }
63
+ async #translateFragment(fragment, rawFramesToUIFrames) {
64
+ const rawFrames = fragment.node.getCallStack().map(node => node.rawFrame).toArray();
65
+ const uiFrames = await rawFramesToUIFrames(rawFrames, this.target());
66
+ console.assert(rawFrames.length === uiFrames.length, 'Broken rawFramesToUIFrames implementation');
67
+ let i = 0;
68
+ for (const node of fragment.node.getCallStack()) {
69
+ node.frames = uiFrames[i++].map(frame => new FrameImpl(frame.url, frame.uiSourceCode, frame.name, frame.line, frame.column, frame.missingDebugInfo));
70
+ }
71
+ }
72
+ #affectedFragments(script) {
73
+ // 1. Collect branches with the matching script.
74
+ const affectedBranches = new Set();
75
+ this.#trie.walk(null, node => {
76
+ // scriptId has precedence, but if the frame does not have one, check the URL.
77
+ if (node.rawFrame.scriptId === script.scriptId ||
78
+ (!node.rawFrame.scriptId && node.rawFrame.url === script.sourceURL)) {
79
+ affectedBranches.add(node);
80
+ return false;
81
+ }
82
+ return true;
83
+ });
84
+ // 2. For each branch collect all the fragments.
85
+ const fragments = new Set();
86
+ for (const branch of affectedBranches) {
87
+ this.#trie.walk(branch, node => {
88
+ if (node.fragment) {
89
+ fragments.add(node.fragment);
90
+ }
91
+ return true;
92
+ });
93
+ }
94
+ return fragments;
95
+ }
96
+ }
97
+ SDK.SDKModel.SDKModel.register(StackTraceModel, { capabilities: 0 /* SDK.Target.Capability.NONE */, autostart: false });
@@ -0,0 +1,113 @@
1
+ // Copyright 2025 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
+ export class FrameNode {
5
+ parent;
6
+ children = [];
7
+ rawFrame;
8
+ frames = [];
9
+ fragment;
10
+ constructor(rawFrame, parent) {
11
+ this.rawFrame = rawFrame;
12
+ this.parent = parent;
13
+ }
14
+ /**
15
+ * Produces the ancestor chain. Including `this` but excluding the `RootFrameNode`.
16
+ */
17
+ *getCallStack() {
18
+ // The `RootFrameNode` doesn't have an actual frame attached, that's why we check for `node.parent` instead of `node`.
19
+ for (let node = this; node.parent; node = node.parent) {
20
+ yield node;
21
+ }
22
+ }
23
+ }
24
+ /**
25
+ * Stores stack trace fragments in a trie, but does not own them/keep them alive.
26
+ */
27
+ export class Trie {
28
+ #root = { parent: null, children: [] };
29
+ /**
30
+ * Most sources produce stack traces in "top-to-bottom" order, so that is what this method expects.
31
+ *
32
+ * @returns The {@link FrameNode} corresponding to the top-most stack frame.
33
+ */
34
+ insert(frames) {
35
+ if (frames.length === 0) {
36
+ throw new Error('Trie.insert called with an empty frames array.');
37
+ }
38
+ let currentNode = this.#root;
39
+ for (let i = frames.length - 1; i >= 0; --i) {
40
+ currentNode = this.#insert(currentNode, frames[i]);
41
+ }
42
+ return currentNode;
43
+ }
44
+ /**
45
+ * Inserts `rawFrame` into the children of the provided node if not already there.
46
+ *
47
+ * @returns the child node corresponding to `rawFrame`.
48
+ */
49
+ #insert(node, rawFrame) {
50
+ let i = 0;
51
+ for (; i < node.children.length; ++i) {
52
+ const maybeChild = node.children[i];
53
+ const child = maybeChild instanceof WeakRef ? maybeChild.deref() : maybeChild;
54
+ if (!child) {
55
+ continue;
56
+ }
57
+ const compareResult = compareRawFrames(child.rawFrame, rawFrame);
58
+ if (compareResult === 0) {
59
+ return child;
60
+ }
61
+ if (compareResult > 0) {
62
+ break;
63
+ }
64
+ }
65
+ const newNode = new FrameNode(rawFrame, node);
66
+ if (node.parent) {
67
+ node.children.splice(i, 0, newNode);
68
+ }
69
+ else {
70
+ node.children.splice(i, 0, new WeakRef(newNode));
71
+ }
72
+ return newNode;
73
+ }
74
+ /**
75
+ * Traverses the trie in pre-order.
76
+ *
77
+ * @param node Start at `node` or `null` to start with the children of the root.
78
+ * @param visit Called on each node in the trie. Return `true` if the visitor should descend into child nodes of the provided node.
79
+ */
80
+ walk(node, visit) {
81
+ const stack = node ? [node] : [...this.#root.children].map(ref => ref.deref()).filter(node => Boolean(node));
82
+ for (let node = stack.pop(); node; node = stack.pop()) {
83
+ const visitChildren = visit(node);
84
+ if (visitChildren) {
85
+ // Pushing the children in reverse means the "left-most" child is visited first (i.e. pre-order).
86
+ for (let i = node.children.length - 1; i >= 0; --i) {
87
+ stack.push(node.children[i]);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ /**
94
+ * @returns a number < 0, 0 or > 0, if the `a` is smaller then, equal or greater then `b`.
95
+ */
96
+ export function compareRawFrames(a, b) {
97
+ const scriptIdCompare = (a.scriptId ?? '').localeCompare(b.scriptId ?? '');
98
+ if (scriptIdCompare !== 0) {
99
+ return scriptIdCompare;
100
+ }
101
+ const urlCompare = (a.url ?? '').localeCompare(b.url ?? '');
102
+ if (urlCompare !== 0) {
103
+ return urlCompare;
104
+ }
105
+ const nameCompare = (a.functionName ?? '').localeCompare(b.functionName ?? '');
106
+ if (nameCompare !== 0) {
107
+ return nameCompare;
108
+ }
109
+ if (a.lineNumber !== b.lineNumber) {
110
+ return a.lineNumber - b.lineNumber;
111
+ }
112
+ return a.columnNumber - b.columnNumber;
113
+ }
@@ -0,0 +1,5 @@
1
+ // Copyright 2025 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
+ import * as StackTrace from './StackTrace.js';
5
+ export { StackTrace, };
@@ -0,0 +1,7 @@
1
+ // Copyright 2025 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
+ import * as StackTraceImpl from './StackTraceImpl.js';
5
+ import * as StackTraceModel from './StackTraceModel.js';
6
+ import * as Trie from './Trie.js';
7
+ export { StackTraceImpl, StackTraceModel, Trie, };
@@ -330,3 +330,26 @@ export const performSearchInSearchMatches = function (matches, query, caseSensit
330
330
  }
331
331
  return result;
332
332
  };
333
+ /**
334
+ * Finds the longest overlapping string segment between the end of the first
335
+ * string and the beginning of the second string.
336
+ *
337
+ * @param s1 The first string (whose suffix will be checked).
338
+ * @param s2 The second string (whose prefix will be checked).
339
+ * @returns The overlapping string segment, or an empty string ("")
340
+ * if no overlap is found.
341
+ */
342
+ export const getOverlap = function (s1, s2) {
343
+ const minLen = Math.min(s1.length, s2.length);
344
+ // Check from longest possible overlap down to 1
345
+ for (let n = minLen; n > 0; n--) {
346
+ // slice(-n) gets the last 'n' chars
347
+ const suffix = s1.slice(-n);
348
+ // substring(0, n) gets the first 'n' chars
349
+ const prefix = s2.substring(0, n);
350
+ if (suffix === prefix) {
351
+ return suffix;
352
+ }
353
+ }
354
+ return null;
355
+ };
@@ -361,7 +361,7 @@ export class TraceProcessor extends EventTarget {
361
361
  let model;
362
362
  try {
363
363
  options.logger?.start(`insights:${name}`);
364
- model = insight.generateInsight(data, context);
364
+ model = insight.generateInsight(data, context, options.insightTimeFormatters);
365
365
  model.frameId = context.frameId;
366
366
  const navId = context.navigation?.args.data?.navigationId;
367
367
  if (navId) {
@@ -642,7 +642,7 @@ export function extractSampleTraceId(event) {
642
642
  }
643
643
  return event.args?.sampleTraceId ?? event.args?.data?.sampleTraceId ?? null;
644
644
  }
645
- // This exactly matches EntryStyles.visibleTypes. See the runtime verification in maybeInitSylesMap.
645
+ // This exactly matches Trace.Styles.visibleTypes. See the runtime verification in maybeInitStylesMap.
646
646
  // TODO(crbug.com/410884528)
647
647
  export const VISIBLE_TRACE_EVENT_TYPES = new Set([
648
648
  "AbortPostTaskCallback" /* Types.Events.Name.ABORT_POST_TASK_CALLBACK */,
@@ -153,10 +153,11 @@ function finalize(partialModel) {
153
153
  ...partialModel,
154
154
  };
155
155
  }
156
- export function generateInsight(data, context) {
156
+ export function generateInsight(data, context, timeFormatters) {
157
157
  if (!context.navigation) {
158
158
  return finalize({});
159
159
  }
160
+ const millisToString = timeFormatters?.milli ?? i18n.TimeUtilities.millisToString;
160
161
  const documentRequest = data.NetworkRequests.byId.get(context.navigationId);
161
162
  if (!documentRequest) {
162
163
  return finalize({ warnings: [InsightWarning.NO_DOCUMENT_REQUEST] });
@@ -191,14 +192,14 @@ export function generateInsight(data, context) {
191
192
  noRedirects: {
192
193
  label: noRedirects ? i18nString(UIStrings.passingRedirects) : i18nString(UIStrings.failedRedirects, {
193
194
  PH1: documentRequest.args.data.redirects.length,
194
- PH2: i18n.TimeUtilities.millisToString(redirectDuration),
195
+ PH2: millisToString(redirectDuration),
195
196
  }),
196
197
  value: noRedirects
197
198
  },
198
199
  serverResponseIsFast: {
199
200
  label: serverResponseIsFast ?
200
- i18nString(UIStrings.passingServerResponseTime, { PH1: i18n.TimeUtilities.millisToString(serverResponseTime) }) :
201
- i18nString(UIStrings.failedServerResponseTime, { PH1: i18n.TimeUtilities.millisToString(serverResponseTime) }),
201
+ i18nString(UIStrings.passingServerResponseTime, { PH1: millisToString(serverResponseTime) }) :
202
+ i18nString(UIStrings.failedServerResponseTime, { PH1: millisToString(serverResponseTime) }),
202
203
  value: serverResponseIsFast
203
204
  },
204
205
  usesCompression: {
@@ -0,0 +1,199 @@
1
+ // Copyright 2023 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
+ import * as SDK from '../../core/sdk/sdk.js';
5
+ import * as Bindings from '../bindings/bindings.js';
6
+ import * as SourceMapScopes from '../source_map_scopes/source_map_scopes.js';
7
+ import * as Trace from '../trace/trace.js';
8
+ import * as Workspace from '../workspace/workspace.js';
9
+ export class SourceMappingsUpdated extends Event {
10
+ static eventName = 'sourcemappingsupdated';
11
+ constructor() {
12
+ super(SourceMappingsUpdated.eventName, { composed: true, bubbles: true });
13
+ }
14
+ }
15
+ // The code location key is created as a concatenation of its fields.
16
+ export const resolvedCodeLocationDataNames = new Map();
17
+ export class SourceMapsResolver extends EventTarget {
18
+ executionContextNamesByOrigin = new Map();
19
+ #parsedTrace;
20
+ #entityMapper = null;
21
+ #isResolving = false;
22
+ // We need to gather up a list of all the DebuggerModels that we should
23
+ // listen to for source map attached events. For most pages this will be
24
+ // the debugger model for the primary page target, but if a trace has
25
+ // workers, we would also need to gather up the DebuggerModel instances for
26
+ // those workers too.
27
+ #debuggerModelsToListen = new Set();
28
+ constructor(parsedTrace, entityMapper) {
29
+ super();
30
+ this.#parsedTrace = parsedTrace;
31
+ this.#entityMapper = entityMapper ?? null;
32
+ }
33
+ static clearResolvedNodeNames() {
34
+ resolvedCodeLocationDataNames.clear();
35
+ }
36
+ static keyForCodeLocation(callFrame) {
37
+ return `${callFrame.url}$$$${callFrame.scriptId}$$$${callFrame.functionName}$$$${callFrame.lineNumber}$$$${callFrame.columnNumber}`;
38
+ }
39
+ /**
40
+ * For trace events containing a call frame / source location
41
+ * (f.e. a stack trace), attempts to obtain the resolved source
42
+ * location based on the those that have been resolved so far from
43
+ * listened source maps.
44
+ *
45
+ * Note that a single deployed URL can map to multiple authored URLs
46
+ * (f.e. if an app is bundled). Thus, beyond a URL we can use code
47
+ * location data like line and column numbers to obtain the specific
48
+ * authored code according to the source mappings.
49
+ *
50
+ * TODO(andoli): This can return incorrect scripts if the target page has been reloaded since the trace.
51
+ */
52
+ static resolvedCodeLocationForCallFrame(callFrame) {
53
+ const codeLocationKey = this.keyForCodeLocation(callFrame);
54
+ return resolvedCodeLocationDataNames.get(codeLocationKey) ?? null;
55
+ }
56
+ static resolvedCodeLocationForEntry(entry) {
57
+ let callFrame = null;
58
+ if (Trace.Types.Events.isProfileCall(entry)) {
59
+ callFrame = entry.callFrame;
60
+ }
61
+ else {
62
+ const topCallFrame = Trace.Helpers.Trace.getStackTraceTopCallFrameInEventPayload(entry);
63
+ if (!topCallFrame) {
64
+ return null;
65
+ }
66
+ callFrame = topCallFrame;
67
+ }
68
+ return SourceMapsResolver.resolvedCodeLocationForCallFrame(callFrame);
69
+ }
70
+ static resolvedURLForEntry(parsedTrace, entry) {
71
+ const resolvedCallFrameURL = SourceMapsResolver.resolvedCodeLocationForEntry(entry)?.devtoolsLocation?.uiSourceCode.url();
72
+ if (resolvedCallFrameURL) {
73
+ return resolvedCallFrameURL;
74
+ }
75
+ // If no source mapping was found for an entry's URL, then default
76
+ // to the URL value contained in the event itself, if any.
77
+ const url = Trace.Handlers.Helpers.getNonResolvedURL(entry, parsedTrace.data);
78
+ if (url) {
79
+ return Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url)?.url() ?? url;
80
+ }
81
+ return null;
82
+ }
83
+ static storeResolvedCodeDataForCallFrame(callFrame, resolvedCodeLocationData) {
84
+ const keyForCallFrame = this.keyForCodeLocation(callFrame);
85
+ resolvedCodeLocationDataNames.set(keyForCallFrame, resolvedCodeLocationData);
86
+ }
87
+ async install() {
88
+ for (const threadToProfileMap of this.#parsedTrace.data.Samples.profilesInProcess.values()) {
89
+ for (const [tid, profile] of threadToProfileMap) {
90
+ const nodes = profile.parsedProfile.nodes();
91
+ if (!nodes || nodes.length === 0) {
92
+ continue;
93
+ }
94
+ const target = this.#targetForThread(tid);
95
+ const debuggerModel = target?.model(SDK.DebuggerModel.DebuggerModel);
96
+ if (!debuggerModel) {
97
+ continue;
98
+ }
99
+ for (const node of nodes) {
100
+ const script = debuggerModel.scriptForId(String(node.callFrame.scriptId));
101
+ const shouldListenToSourceMap = !script || script.sourceMapURL;
102
+ if (!shouldListenToSourceMap) {
103
+ continue;
104
+ }
105
+ this.#debuggerModelsToListen.add(debuggerModel);
106
+ }
107
+ }
108
+ }
109
+ for (const debuggerModel of this.#debuggerModelsToListen) {
110
+ debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, this.#onAttachedSourceMap, this);
111
+ }
112
+ this.#updateExtensionNames();
113
+ // Although we have added listeners for SourceMapAttached events, we also
114
+ // immediately try to resolve function names. This ensures we use any
115
+ // sourcemaps that were attached before we bound our event listener.
116
+ await this.#resolveMappingsForProfileNodes();
117
+ }
118
+ /**
119
+ * Removes the event listeners and stops tracking newly added sourcemaps.
120
+ * Should be called before destroying an instance of this class to avoid leaks
121
+ * with listeners.
122
+ */
123
+ uninstall() {
124
+ for (const debuggerModel of this.#debuggerModelsToListen) {
125
+ debuggerModel.sourceMapManager().removeEventListener(SDK.SourceMapManager.Events.SourceMapAttached, this.#onAttachedSourceMap, this);
126
+ }
127
+ this.#debuggerModelsToListen.clear();
128
+ }
129
+ async #resolveMappingsForProfileNodes() {
130
+ // Used to track if source mappings were updated when a source map
131
+ // is attach. If not, we do not notify the flamechart that mappings
132
+ // were updated, since that would trigger a rerender.
133
+ let updatedMappings = false;
134
+ for (const [, threadsInProcess] of this.#parsedTrace.data.Samples.profilesInProcess) {
135
+ for (const [tid, threadProfile] of threadsInProcess) {
136
+ const nodes = threadProfile.parsedProfile.nodes() ?? [];
137
+ const target = this.#targetForThread(tid);
138
+ if (!target) {
139
+ continue;
140
+ }
141
+ for (const node of nodes) {
142
+ const resolvedFunctionName = await SourceMapScopes.NamesResolver.resolveProfileFrameFunctionName(node.callFrame, target);
143
+ updatedMappings ||= Boolean(resolvedFunctionName);
144
+ node.setFunctionName(resolvedFunctionName);
145
+ const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
146
+ const script = debuggerModel?.scriptForId(node.scriptId) || null;
147
+ const location = debuggerModel &&
148
+ new SDK.DebuggerModel.Location(debuggerModel, node.callFrame.scriptId, node.callFrame.lineNumber, node.callFrame.columnNumber);
149
+ const uiLocation = location &&
150
+ await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(location);
151
+ updatedMappings ||= Boolean(uiLocation);
152
+ if (uiLocation?.uiSourceCode.url() && this.#entityMapper) {
153
+ // Update mappings for the related events of the entity.
154
+ this.#entityMapper.updateSourceMapEntities(node.callFrame, uiLocation.uiSourceCode.url());
155
+ }
156
+ SourceMapsResolver.storeResolvedCodeDataForCallFrame(node.callFrame, { name: resolvedFunctionName, devtoolsLocation: uiLocation, script });
157
+ }
158
+ }
159
+ }
160
+ if (!updatedMappings) {
161
+ return;
162
+ }
163
+ this.dispatchEvent(new SourceMappingsUpdated());
164
+ }
165
+ #onAttachedSourceMap() {
166
+ // Exit if we are already resolving so that we batch requests; if pages
167
+ // have a lot of sourcemaps we can get a lot of events at once.
168
+ if (this.#isResolving) {
169
+ return;
170
+ }
171
+ this.#isResolving = true;
172
+ // Resolving names triggers a repaint of the flame chart. Instead of attempting to resolve
173
+ // names every time a source map is attached, wait for some time once the first source map is
174
+ // attached. This way we allow for other source maps to be parsed before attempting a name
175
+ // resolving using the available source maps. Otherwise the UI is blocked when the number
176
+ // of source maps is particularly large.
177
+ setTimeout(async () => {
178
+ this.#isResolving = false;
179
+ await this.#resolveMappingsForProfileNodes();
180
+ }, 500);
181
+ }
182
+ // Figure out the target for the node. If it is in a worker thread,
183
+ // that is the target, otherwise we use the primary page target.
184
+ #targetForThread(tid) {
185
+ const maybeWorkerId = this.#parsedTrace.data.Workers.workerIdByThread.get(tid);
186
+ if (maybeWorkerId) {
187
+ return SDK.TargetManager.TargetManager.instance().targetById(maybeWorkerId);
188
+ }
189
+ return SDK.TargetManager.TargetManager.instance().primaryPageTarget();
190
+ }
191
+ #updateExtensionNames() {
192
+ for (const runtimeModel of SDK.TargetManager.TargetManager.instance().models(SDK.RuntimeModel.RuntimeModel)) {
193
+ for (const context of runtimeModel.executionContexts()) {
194
+ this.executionContextNamesByOrigin.set(context.origin, context.name);
195
+ }
196
+ }
197
+ this.#entityMapper?.updateExtensionEntitiesWithName(this.executionContextNamesByOrigin);
198
+ }
199
+ }
@@ -0,0 +1,4 @@
1
+ // Copyright 2025 The Chromium Authors. All rights reserved.
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ export * from './SourceMapsResolver.js';