UNPKG

chrome-devtools-frontend

Version:
304 lines (263 loc) • 12.7 kB
// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import type * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import {augmentRawFramesWithScriptIds, parseRawFramesFromErrorStack} from './DetailedErrorStackParser.js'; // eslint-disable-next-line @devtools/es-modules-import import * as StackTrace from './stack_trace.js'; import { type AnyStackTraceImpl, AsyncFragmentImpl, DebuggableFragmentImpl, FragmentImpl, FrameImpl, ParsedErrorStackFragmentImpl, StackTraceImpl } from './StackTraceImpl.js'; import {EvalOrigin, type FrameNode, type RawFrame, Trie} from './Trie.js'; /** * A stack trace translation function. * * Any implementation must return an array with the same length as `frames`. */ export type TranslateRawFrames = (frames: readonly RawFrame[], target: SDK.Target.Target) => Promise< Array<Array<Pick<StackTrace.StackTrace.Frame, 'url'|'uiSourceCode'|'name'|'line'|'column'|'missingDebugInfo'>>>>; /** * The {@link StackTraceModel} is a thin wrapper around a fragment trie. * * We want to store stack trace fragments per target so a SDKModel is the natural choice. */ export class StackTraceModel extends SDK.SDKModel.SDKModel<unknown> { readonly #trie = new Trie(); readonly #mutex = new Common.Mutex.Mutex(); /** @returns the {@link StackTraceModel} for the target, or the model for the primaryPageTarget when passing null/undefined */ static #modelForTarget(target: SDK.Target.Target|null|undefined): StackTraceModel { const model = (target ?? SDK.TargetManager.TargetManager.instance().primaryPageTarget())?.model(StackTraceModel); if (!model) { throw new Error('Unable to find StackTraceModel'); } return model; } async createFromProtocolRuntime(stackTrace: Protocol.Runtime.StackTrace, rawFramesToUIFrames: TranslateRawFrames): Promise<StackTrace.StackTrace.StackTrace> { const debuggerModel = this.target().model(SDK.DebuggerModel.DebuggerModel); const syncFrames = stackTrace.callFrames.map((frame): RawFrame => { const isWasm = debuggerModel?.isWasm(frame.scriptId) ?? false; return {...frame, isWasm}; }); const [syncFragment, asyncFragments] = await Promise.all([ this.#createFragment(syncFrames, rawFramesToUIFrames), this.#createAsyncFragments(stackTrace, rawFramesToUIFrames), ]); return new StackTraceImpl(syncFragment, asyncFragments); } async createFromErrorStackLikeString( stack: string, rawFramesToUIFrames: TranslateRawFrames, exceptionDetails?: Protocol.Runtime.ExceptionDetails): Promise<StackTrace.StackTrace.ParsedErrorStackTrace|null> { const debuggerModel = this.target().model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel; const baseURL = this.target().inspectedURL(); const resolveURL = (url: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString|null => { let urlWithScheme = parseOrScriptMatch(debuggerModel, url); if (!urlWithScheme && Common.ParsedURL.ParsedURL.isRelativeURL(url)) { urlWithScheme = parseOrScriptMatch(debuggerModel, Common.ParsedURL.ParsedURL.completeURL(baseURL, url)); } return urlWithScheme; }; const rawFrames = parseRawFramesFromErrorStack(stack, resolveURL); if (!rawFrames) { return null; } if (exceptionDetails?.stackTrace) { augmentRawFramesWithScriptIds(rawFrames, exceptionDetails.stackTrace); } const [syncFragment, asyncFragments] = await Promise.all([ this.#createFragment(rawFrames, rawFramesToUIFrames), exceptionDetails?.stackTrace ? this.#createAsyncFragments(exceptionDetails.stackTrace, rawFramesToUIFrames) : Promise.resolve([]), ]); return new StackTraceImpl(new ParsedErrorStackFragmentImpl(syncFragment), asyncFragments); } async createFromDebuggerPaused( pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails, rawFramesToUIFrames: TranslateRawFrames): Promise<StackTrace.StackTrace.DebuggableStackTrace> { const [syncFragment, asyncFragments] = await Promise.all([ this.#createDebuggableFragment(pausedDetails, rawFramesToUIFrames), this.#createAsyncFragments(pausedDetails, rawFramesToUIFrames), ]); return new StackTraceImpl(syncFragment, asyncFragments); } /** Trigger re-translation of all fragments with the provide script in their call stack */ async scriptInfoChanged(script: SDK.Script.Script, translateRawFrames: TranslateRawFrames): Promise<void> { const release = await this.#mutex.acquire(); try { const translatePromises: Array<Promise<unknown>> = []; let stackTracesToUpdate = new Set<AnyStackTraceImpl>(); for (const fragment of this.#affectedFragments(script)) { // We trigger re-translation only for fragments of leaf-nodes. Any fragment along the ancestor-chain // is re-translated as a side-effect. // We just need to remember the stack traces of the skipped over fragments, so we can send the // UPDATED event also to them. if (fragment.node?.children.length === 0) { translatePromises.push(this.#translateFragment(fragment, translateRawFrames)); } stackTracesToUpdate = stackTracesToUpdate.union(fragment.stackTraces); } await Promise.all(translatePromises); for (const stackTrace of stackTracesToUpdate) { stackTrace.dispatchEventToListeners(StackTrace.StackTrace.Events.UPDATED); } } finally { release(); } } async #createDebuggableFragment( pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails, rawFramesToUIFrames: TranslateRawFrames): Promise<DebuggableFragmentImpl> { const fragment = await this.#createFragment( pausedDetails.callFrames.map(frame => ({ scriptId: frame.script.scriptId, url: frame.script.sourceURL, functionName: frame.functionName, lineNumber: frame.location().lineNumber, columnNumber: frame.location().columnNumber, isWasm: frame.script.isWasm(), })), rawFramesToUIFrames); return new DebuggableFragmentImpl(fragment, pausedDetails.callFrames); } async #createAsyncFragments( stackTraceOrPausedEvent: Protocol.Runtime.StackTrace|SDK.DebuggerModel.DebuggerPausedDetails, rawFramesToUIFrames: TranslateRawFrames): Promise<AsyncFragmentImpl[]> { const asyncFragments: Array<Promise<AsyncFragmentImpl>> = []; const debuggerModel = this.target().model(SDK.DebuggerModel.DebuggerModel); if (debuggerModel) { for await ( const {stackTrace: asyncStackTrace, target} of debuggerModel.iterateAsyncParents(stackTraceOrPausedEvent)) { if (asyncStackTrace.callFrames.length === 0) { // Skip empty async fragments, they don't add value. continue; } const model = StackTraceModel.#modelForTarget(target); const targetDebuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); const asyncFrames = asyncStackTrace.callFrames.map((frame): RawFrame => { const isWasm = targetDebuggerModel?.isWasm(frame.scriptId) ?? false; return {...frame, isWasm}; }); const asyncFragmentPromise = model.#createFragment(asyncFrames, rawFramesToUIFrames) .then(fragment => new AsyncFragmentImpl(asyncStackTrace.description ?? '', fragment)); asyncFragments.push(asyncFragmentPromise); } } return await Promise.all(asyncFragments); } async #createFragment(frames: RawFrame[], rawFramesToUIFrames: TranslateRawFrames): Promise<FragmentImpl> { if (frames.length === 0) { return FragmentImpl.EMPTY_FRAGMENT; } const release = await this.#mutex.acquire(); try { const node = this.#trie.insert(frames); const requiresTranslation = !Boolean(node.fragment); const fragment = FragmentImpl.getOrCreate(node); if (requiresTranslation) { await this.#translateFragment(fragment, rawFramesToUIFrames); } return fragment; } finally { release(); } } async #translateFragment(fragment: FragmentImpl, rawFramesToUIFrames: TranslateRawFrames): Promise<void> { if (!fragment.node) { return; } const rawFrames = fragment.node.getCallStack().map(node => node.rawFrame).toArray(); const uiFrames = await rawFramesToUIFrames(rawFrames, this.target()); console.assert(rawFrames.length === uiFrames.length, 'Broken rawFramesToUIFrames implementation'); const evalOriginPromises: Array<Promise<EvalOrigin|undefined>> = []; for (const node of fragment.node.getCallStack()) { if (node.parsedFrameInfo?.evalOrigin) { // Evaluate each eval origin individually, as they are not a contiguous stack trace. evalOriginPromises.push( translateEvalOrigin(node.parsedFrameInfo.evalOrigin, rawFramesToUIFrames, this.target())); } } const evalOrigins = await Promise.all(evalOriginPromises); let i = 0; let evalI = 0; for (const node of fragment.node.getCallStack()) { const group = uiFrames[i++]; node.frames = group.map((frame, index) => new FrameImpl(frame.url, frame.uiSourceCode, frame.name, frame.line, frame.column, frame.missingDebugInfo, node.rawFrame.functionName, node.rawFrame.isWasm, index < group.length - 1)); if (node.parsedFrameInfo?.evalOrigin) { node.evalOrigin = evalOrigins[evalI++]; } } } #affectedFragments(script: SDK.Script.Script): Set<FragmentImpl> { // 1. Collect branches with the matching script. const affectedBranches = new Set<FrameNode>(); this.#trie.walk(null, node => { // scriptId has precedence, but if the frame does not have one, check the URL. if (node.rawFrame.scriptId === script.scriptId || (!node.rawFrame.scriptId && node.rawFrame.url === script.sourceURL)) { affectedBranches.add(node); return false; } return true; }); // 2. For each branch collect all the fragments. const fragments = new Set<FragmentImpl>(); for (const branch of affectedBranches) { this.#trie.walk(branch, node => { if (node.fragment) { fragments.add(node.fragment); } return true; }); } return fragments; } } async function translateEvalOrigin( rawFrame: RawFrame, rawFramesToUIFrames: TranslateRawFrames, target: SDK.Target.Target): Promise<EvalOrigin|undefined> { const uiFrames = await rawFramesToUIFrames([rawFrame], target); const group = uiFrames[0]; const frames = group.map((frame, index) => new FrameImpl(frame.url, frame.uiSourceCode, frame.name, frame.line, frame.column, frame.missingDebugInfo, rawFrame.functionName, rawFrame.isWasm, index < group.length - 1)); let parentEvalOrigin: EvalOrigin|undefined; if (rawFrame.parsedFrameInfo?.evalOrigin) { parentEvalOrigin = await translateEvalOrigin(rawFrame.parsedFrameInfo.evalOrigin, rawFramesToUIFrames, target); } return new EvalOrigin(frames, parentEvalOrigin); } function parseOrScriptMatch(debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString|null): Platform.DevToolsPath.UrlString|null { if (!url) { return null; } if (Common.ParsedURL.ParsedURL.isValidUrlString(url)) { return url; } if (debuggerModel.scriptsForSourceURL(url).length) { return url; } // nodejs stack traces contain (absolute) file paths, but v8 reports them as file: urls. try { const fileUrl = new URL(url, 'file://'); if (debuggerModel.scriptsForSourceURL(fileUrl.href as Platform.DevToolsPath.UrlString).length) { return fileUrl.href as Platform.DevToolsPath.UrlString; } } catch { } return null; } SDK.SDKModel.SDKModel.register(StackTraceModel, {capabilities: SDK.Target.Capability.NONE, autostart: false});