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.
- package/README.md +6 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Settings.js +3 -32
- package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +35 -8
- package/build/node_modules/chrome-devtools-frontend/front_end/core/root/Runtime.js +4 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +4 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +12 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +366 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +366 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIQueries.js +105 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CSSWorkspaceBinding.js +243 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +407 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +128 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +992 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +574 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DefaultScriptMapping.js +112 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/FileUtils.js +186 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/LiveLocation.js +60 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/NetworkProject.js +107 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/PresentationConsoleMessageHelper.js +244 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceMapping.js +473 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceScriptMapping.js +399 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceUtils.js +87 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/SASSSourceMapping.js +181 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/StylesSourceMapping.js +268 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/TempFile.js +55 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/bindings.js +20 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js +283 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/crux-manager.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +775 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +1706 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/emulation.js +6 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +131 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/ScriptFormatter.js +77 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/formatter.js +6 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/GeometryImpl.js +347 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/geometry.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +626 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeChainModel.js +59 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeTreeCache.js +32 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/source_map_scopes.js +7 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +67 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +97 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/Trie.js +113 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace.js +5 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace_impl.js +7 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/text_utils/TextUtils.js +23 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +5 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +199 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/FileManager.js +64 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/IgnoreListManager.js +511 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/SearchConfig.js +113 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/UISourceCode.js +563 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/WorkspaceImpl.js +204 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/workspace.js +9 -0
- package/build/src/McpContext.js +24 -9
- package/build/src/McpResponse.js +3 -3
- package/build/src/browser.js +3 -1
- package/build/src/index.js +1 -1
- package/build/src/tools/input.js +7 -7
- package/build/src/tools/performance.js +29 -2
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +40 -14
- package/build/src/trace-processing/parse.js +26 -22
- 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, };
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js
ADDED
|
@@ -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
|
+
}
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js
ADDED
|
@@ -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
|
+
}
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace_impl.js
ADDED
|
@@ -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, };
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/text_utils/TextUtils.js
CHANGED
|
@@ -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
|
|
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:
|
|
195
|
+
PH2: millisToString(redirectDuration),
|
|
195
196
|
}),
|
|
196
197
|
value: noRedirects
|
|
197
198
|
},
|
|
198
199
|
serverResponseIsFast: {
|
|
199
200
|
label: serverResponseIsFast ?
|
|
200
|
-
i18nString(UIStrings.passingServerResponseTime, { PH1:
|
|
201
|
-
i18nString(UIStrings.failedServerResponseTime, { PH1:
|
|
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
|
+
}
|