chrome-devtools-frontend
Version:
Chrome DevTools UI
600 lines (526 loc) • 23.5 kB
text/typescript
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Workspace from '../workspace/workspace.js';
import {CompilerScriptMapping} from './CompilerScriptMapping.js';
import {DebuggerLanguagePluginManager} from './DebuggerLanguagePlugins.js';
import {DefaultScriptMapping} from './DefaultScriptMapping.js';
import {IgnoreListManager} from './IgnoreListManager.js';
import {LiveLocationWithPool, type LiveLocation, type LiveLocationPool} from './LiveLocation.js';
import {ResourceMapping} from './ResourceMapping.js';
import {ResourceScriptMapping, type ResourceScriptFile} from './ResourceScriptMapping.js';
let debuggerWorkspaceBindingInstance: DebuggerWorkspaceBinding|undefined;
export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
readonly workspace: Workspace.Workspace.WorkspaceImpl;
readonly #sourceMappings: DebuggerSourceMapping[];
readonly #debuggerModelToData: Map<SDK.DebuggerModel.DebuggerModel, ModelData>;
readonly #liveLocationPromises: Set<Promise<void|Location|StackTraceTopFrameLocation|null>>;
pluginManager: DebuggerLanguagePluginManager|null;
#targetManager: SDK.TargetManager.TargetManager;
private constructor(targetManager: SDK.TargetManager.TargetManager, workspace: Workspace.Workspace.WorkspaceImpl) {
this.workspace = workspace;
this.#sourceMappings = [];
this.#debuggerModelToData = new Map();
targetManager.addModelListener(
SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this);
targetManager.addModelListener(
SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this);
targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this);
this.#targetManager = targetManager;
this.#liveLocationPromises = new Set();
this.pluginManager = Root.Runtime.experiments.isEnabled('wasmDWARFDebugging') ?
new DebuggerLanguagePluginManager(targetManager, workspace, this) :
null;
}
initPluginManagerForTest(): DebuggerLanguagePluginManager|null {
if (Root.Runtime.experiments.isEnabled('wasmDWARFDebugging')) {
if (!this.pluginManager) {
this.pluginManager = new DebuggerLanguagePluginManager(this.#targetManager, this.workspace, this);
}
} else {
this.pluginManager = null;
}
return this.pluginManager;
}
static instance(opts: {
forceNew: boolean|null,
targetManager: SDK.TargetManager.TargetManager|null,
workspace: Workspace.Workspace.WorkspaceImpl|null,
} = {forceNew: null, targetManager: null, workspace: null}): DebuggerWorkspaceBinding {
const {forceNew, targetManager, workspace} = opts;
if (!debuggerWorkspaceBindingInstance || forceNew) {
if (!targetManager || !workspace) {
throw new Error(`Unable to create DebuggerWorkspaceBinding: targetManager and workspace must be provided: ${
new Error().stack}`);
}
debuggerWorkspaceBindingInstance = new DebuggerWorkspaceBinding(targetManager, workspace);
}
return debuggerWorkspaceBindingInstance;
}
static removeInstance(): void {
debuggerWorkspaceBindingInstance = undefined;
}
addSourceMapping(sourceMapping: DebuggerSourceMapping): void {
this.#sourceMappings.push(sourceMapping);
}
removeSourceMapping(sourceMapping: DebuggerSourceMapping): void {
const index = this.#sourceMappings.indexOf(sourceMapping);
if (index !== -1) {
this.#sourceMappings.splice(index, 1);
}
}
private async computeAutoStepRanges(mode: SDK.DebuggerModel.StepMode, callFrame: SDK.DebuggerModel.CallFrame):
Promise<RawLocationRange[]> {
function contained(location: SDK.DebuggerModel.Location, range: RawLocationRange): boolean {
const {start, end} = range;
if (start.scriptId !== location.scriptId) {
return false;
}
if (location.lineNumber < start.lineNumber || location.lineNumber > end.lineNumber) {
return false;
}
if (location.lineNumber === start.lineNumber && location.columnNumber < start.columnNumber) {
return false;
}
if (location.lineNumber === end.lineNumber && location.columnNumber >= end.columnNumber) {
return false;
}
return true;
}
const rawLocation = callFrame.location();
if (!rawLocation) {
return [];
}
const pluginManager = this.pluginManager;
let ranges: RawLocationRange[] = [];
if (pluginManager) {
if (mode === SDK.DebuggerModel.StepMode.StepOut) {
// Step out of inline function.
return await pluginManager.getInlinedFunctionRanges(rawLocation);
}
const uiLocation = await pluginManager.rawLocationToUILocation(rawLocation);
if (uiLocation) {
ranges = await pluginManager.uiLocationToRawLocationRanges(
uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber) ||
[];
// TODO(bmeurer): Remove the {rawLocation} from the {ranges}?
ranges = ranges.filter(range => contained(rawLocation, range));
if (mode === SDK.DebuggerModel.StepMode.StepOver) {
// Step over an inlined function.
ranges = ranges.concat(await pluginManager.getInlinedCalleesRanges(rawLocation));
}
return ranges;
}
}
const compilerMapping = this.#debuggerModelToData.get(rawLocation.debuggerModel)?.compilerMapping;
if (!compilerMapping) {
return [];
}
if (mode === SDK.DebuggerModel.StepMode.StepOut) {
// We should actually return the source range for the entire function
// to skip over. Since we don't have that, we return an empty range
// instead, to signal that we should perform a regular step-out.
return [];
}
ranges = compilerMapping.getLocationRangesForSameSourceLocation(rawLocation);
ranges = ranges.filter(range => contained(rawLocation, range));
return ranges;
}
modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
this.#debuggerModelToData.set(debuggerModel, new ModelData(debuggerModel, this));
debuggerModel.setComputeAutoStepRangesCallback(this.computeAutoStepRanges.bind(this));
}
modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
debuggerModel.setComputeAutoStepRangesCallback(null);
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (modelData) {
modelData.dispose();
this.#debuggerModelToData.delete(debuggerModel);
}
}
/**
* The promise returned by this function is resolved once all *currently*
* pending LiveLocations are processed.
*/
async pendingLiveLocationChangesPromise(): Promise<void|Location|StackTraceTopFrameLocation|null> {
await Promise.all(this.#liveLocationPromises);
}
private recordLiveLocationChange(promise: Promise<void|Location|StackTraceTopFrameLocation|null>): void {
void promise.then(() => {
this.#liveLocationPromises.delete(promise);
});
this.#liveLocationPromises.add(promise);
}
async updateLocations(script: SDK.Script.Script): Promise<void> {
const modelData = this.#debuggerModelToData.get(script.debuggerModel);
if (modelData) {
const updatePromise = modelData.updateLocations(script);
this.recordLiveLocationChange(updatePromise);
await updatePromise;
}
}
async createLiveLocation(
rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>,
locationPool: LiveLocationPool): Promise<Location|null> {
const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel);
if (!modelData) {
return null;
}
const liveLocationPromise = modelData.createLiveLocation(rawLocation, updateDelegate, locationPool);
this.recordLiveLocationChange(liveLocationPromise);
return liveLocationPromise;
}
async createStackTraceTopFrameLiveLocation(
rawLocations: SDK.DebuggerModel.Location[], updateDelegate: (arg0: LiveLocation) => Promise<void>,
locationPool: LiveLocationPool): Promise<LiveLocation> {
console.assert(rawLocations.length > 0);
const locationPromise =
StackTraceTopFrameLocation.createStackTraceTopFrameLocation(rawLocations, this, updateDelegate, locationPool);
this.recordLiveLocationChange(locationPromise);
return locationPromise;
}
async createCallFrameLiveLocation(
location: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>,
locationPool: LiveLocationPool): Promise<Location|null> {
const script = location.script();
if (!script) {
return null;
}
const debuggerModel = location.debuggerModel;
const liveLocationPromise = this.createLiveLocation(location, updateDelegate, locationPool);
this.recordLiveLocationChange(liveLocationPromise);
const liveLocation = await liveLocationPromise;
if (!liveLocation) {
return null;
}
this.registerCallFrameLiveLocation(debuggerModel, liveLocation);
return liveLocation;
}
async rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location):
Promise<Workspace.UISourceCode.UILocation|null> {
for (const sourceMapping of this.#sourceMappings) {
const uiLocation = sourceMapping.rawLocationToUILocation(rawLocation);
if (uiLocation) {
return uiLocation;
}
}
if (this.pluginManager) {
const uiLocation = await this.pluginManager.rawLocationToUILocation(rawLocation);
if (uiLocation) {
return uiLocation;
}
}
const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel);
return modelData ? modelData.rawLocationToUILocation(rawLocation) : null;
}
uiSourceCodeForSourceMapSourceURL(
debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString,
isContentScript: boolean): Workspace.UISourceCode.UISourceCode|null {
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (!modelData) {
return null;
}
return modelData.compilerMapping.uiSourceCodeForURL(url, isContentScript);
}
async uiLocationToRawLocations(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber?: number): Promise<SDK.DebuggerModel.Location[]> {
for (const sourceMapping of this.#sourceMappings) {
const locations = sourceMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
if (locations.length) {
return locations;
}
}
const locations = await this.pluginManager?.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
if (locations) {
return locations;
}
for (const modelData of this.#debuggerModelToData.values()) {
const locations = modelData.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
if (locations.length) {
return locations;
}
}
return [];
}
uiLocationToRawLocationsForUnformattedJavaScript(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber: number): SDK.DebuggerModel.Location[] {
console.assert(uiSourceCode.contentType().isScript());
const locations = [];
for (const modelData of this.#debuggerModelToData.values()) {
locations.push(...modelData.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber));
}
return locations;
}
async normalizeUILocation(uiLocation: Workspace.UISourceCode.UILocation): Promise<Workspace.UISourceCode.UILocation> {
const rawLocations =
await this.uiLocationToRawLocations(uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber);
for (const location of rawLocations) {
const uiLocationCandidate = await this.rawLocationToUILocation(location);
if (uiLocationCandidate) {
return uiLocationCandidate;
}
}
return uiLocation;
}
scriptFile(uiSourceCode: Workspace.UISourceCode.UISourceCode, debuggerModel: SDK.DebuggerModel.DebuggerModel):
ResourceScriptFile|null {
const modelData = this.#debuggerModelToData.get(debuggerModel);
return modelData ? modelData.getResourceMapping().scriptFile(uiSourceCode) : null;
}
scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] {
const scripts = new Set<SDK.Script.Script>();
if (this.pluginManager) {
this.pluginManager.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script));
}
for (const modelData of this.#debuggerModelToData.values()) {
const resourceScriptFile = modelData.getResourceMapping().scriptFile(uiSourceCode);
if (resourceScriptFile && resourceScriptFile.script) {
scripts.add(resourceScriptFile.script);
}
modelData.compilerMapping.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script));
}
return [...scripts];
}
scriptsForResource(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] {
const scripts = new Set<SDK.Script.Script>();
for (const modelData of this.#debuggerModelToData.values()) {
const resourceScriptFile = modelData.getResourceMapping().scriptFile(uiSourceCode);
if (resourceScriptFile && resourceScriptFile.script) {
scripts.add(resourceScriptFile.script);
}
}
return [...scripts];
}
supportsConditionalBreakpoints(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
// DevTools traditionally supported (JavaScript) conditions
// for breakpoints everywhere, so we keep that behavior...
if (!this.pluginManager) {
return true;
}
const scripts = this.pluginManager.scriptsForUISourceCode(uiSourceCode);
return scripts.every(script => script.isJavaScript());
}
sourceMapForScript(script: SDK.Script.Script): SDK.SourceMap.SourceMap|null {
const modelData = this.#debuggerModelToData.get(script.debuggerModel);
if (!modelData) {
return null;
}
return modelData.compilerMapping.sourceMapForScript(script);
}
private globalObjectCleared(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void {
this.reset(event.data);
}
private reset(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (!modelData) {
return;
}
for (const location of modelData.callFrameLocations.values()) {
this.removeLiveLocation(location);
}
modelData.callFrameLocations.clear();
}
resetForTest(target: SDK.Target.Target): void {
const debuggerModel = (target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel);
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (modelData) {
modelData.getResourceMapping().resetForTest();
}
}
private registerCallFrameLiveLocation(debuggerModel: SDK.DebuggerModel.DebuggerModel, location: Location): void {
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (modelData) {
const locations = modelData.callFrameLocations;
locations.add(location);
}
}
removeLiveLocation(location: Location): void {
const modelData = this.#debuggerModelToData.get(location.rawLocation.debuggerModel);
if (modelData) {
modelData.disposeLocation(location);
}
}
private debuggerResumed(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void {
this.reset(event.data);
}
}
class ModelData {
readonly #debuggerModel: SDK.DebuggerModel.DebuggerModel;
readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
callFrameLocations: Set<Location>;
#defaultMapping: DefaultScriptMapping;
resourceMapping: ResourceScriptMapping;
readonly compilerMapping: CompilerScriptMapping;
readonly #locations: Platform.MapUtilities.Multimap<string, Location>;
constructor(debuggerModel: SDK.DebuggerModel.DebuggerModel, debuggerWorkspaceBinding: DebuggerWorkspaceBinding) {
this.#debuggerModel = debuggerModel;
this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
this.callFrameLocations = new Set();
const workspace = debuggerWorkspaceBinding.workspace;
this.#defaultMapping = new DefaultScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding);
this.resourceMapping = new ResourceScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding);
this.compilerMapping = new CompilerScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding);
this.#locations = new Platform.MapUtilities.Multimap();
debuggerModel.setBeforePausedCallback(this.beforePaused.bind(this));
}
async createLiveLocation(
rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>,
locationPool: LiveLocationPool): Promise<Location> {
console.assert(rawLocation.scriptId !== '');
const scriptId = rawLocation.scriptId;
const location = new Location(scriptId, rawLocation, this.#debuggerWorkspaceBinding, updateDelegate, locationPool);
this.#locations.set(scriptId, location);
await location.update();
return location;
}
disposeLocation(location: Location): void {
this.#locations.delete(location.scriptId, location);
}
async updateLocations(script: SDK.Script.Script): Promise<void> {
const promises = [];
for (const location of this.#locations.get(script.scriptId)) {
promises.push(location.update());
}
await Promise.all(promises);
}
rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null {
let uiLocation = this.compilerMapping.rawLocationToUILocation(rawLocation);
uiLocation = uiLocation || this.resourceMapping.rawLocationToUILocation(rawLocation);
uiLocation = uiLocation || ResourceMapping.instance().jsLocationToUILocation(rawLocation);
uiLocation = uiLocation || this.#defaultMapping.rawLocationToUILocation(rawLocation);
return uiLocation;
}
uiLocationToRawLocations(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber: number|undefined = 0): SDK.DebuggerModel.Location[] {
// TODO(crbug.com/1153123): Revisit the `#columnNumber = 0` and also preserve `undefined` for source maps?
let locations = this.compilerMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
locations = locations.length ?
locations :
this.resourceMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
locations = locations.length ?
locations :
ResourceMapping.instance().uiLocationToJSLocations(uiSourceCode, lineNumber, columnNumber);
locations = locations.length ?
locations :
this.#defaultMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
return locations;
}
private beforePaused(debuggerPausedDetails: SDK.DebuggerModel.DebuggerPausedDetails): boolean {
return Boolean(debuggerPausedDetails.callFrames[0]);
}
dispose(): void {
this.#debuggerModel.setBeforePausedCallback(null);
this.compilerMapping.dispose();
this.resourceMapping.dispose();
this.#defaultMapping.dispose();
}
getResourceMapping(): ResourceScriptMapping {
return this.resourceMapping;
}
}
export class Location extends LiveLocationWithPool {
readonly scriptId: string;
readonly rawLocation: SDK.DebuggerModel.Location;
readonly #binding: DebuggerWorkspaceBinding;
constructor(
scriptId: string, rawLocation: SDK.DebuggerModel.Location, binding: DebuggerWorkspaceBinding,
updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) {
super(updateDelegate, locationPool);
this.scriptId = scriptId;
this.rawLocation = rawLocation;
this.#binding = binding;
}
async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> {
const debuggerModelLocation = this.rawLocation;
return this.#binding.rawLocationToUILocation(debuggerModelLocation);
}
dispose(): void {
super.dispose();
this.#binding.removeLiveLocation(this);
}
async isIgnoreListed(): Promise<boolean> {
const uiLocation = await this.uiLocation();
if (!uiLocation) {
return false;
}
return IgnoreListManager.instance().isUserOrSourceMapIgnoreListedUISourceCode(uiLocation.uiSourceCode);
}
}
class StackTraceTopFrameLocation extends LiveLocationWithPool {
#updateScheduled: boolean;
#current: LiveLocation|null;
#locations: LiveLocation[]|null;
constructor(updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) {
super(updateDelegate, locationPool);
this.#updateScheduled = true;
this.#current = null;
this.#locations = null;
}
static async createStackTraceTopFrameLocation(
rawLocations: SDK.DebuggerModel.Location[], binding: DebuggerWorkspaceBinding,
updateDelegate: (arg0: LiveLocation) => Promise<void>,
locationPool: LiveLocationPool): Promise<StackTraceTopFrameLocation> {
const location = new StackTraceTopFrameLocation(updateDelegate, locationPool);
const locationsPromises = rawLocations.map(
rawLocation => binding.createLiveLocation(rawLocation, location.scheduleUpdate.bind(location), locationPool));
location.#locations = ((await Promise.all(locationsPromises)).filter(l => Boolean(l)) as Location[]);
await location.updateLocation();
return location;
}
async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> {
return this.#current ? this.#current.uiLocation() : null;
}
async isIgnoreListed(): Promise<boolean> {
return this.#current ? this.#current.isIgnoreListed() : false;
}
dispose(): void {
super.dispose();
if (this.#locations) {
for (const location of this.#locations) {
location.dispose();
}
}
this.#locations = null;
this.#current = null;
}
private async scheduleUpdate(): Promise<void> {
if (this.#updateScheduled) {
return;
}
this.#updateScheduled = true;
queueMicrotask(() => {
void this.updateLocation();
});
}
private async updateLocation(): Promise<void> {
this.#updateScheduled = false;
if (!this.#locations || this.#locations.length === 0) {
return;
}
this.#current = this.#locations[0];
for (const location of this.#locations) {
if (!(await location.isIgnoreListed())) {
this.#current = location;
break;
}
}
void this.update();
}
}
export interface RawLocationRange {
start: SDK.DebuggerModel.Location;
end: SDK.DebuggerModel.Location;
}
export interface DebuggerSourceMapping {
rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null;
uiLocationToRawLocations(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber?: number): SDK.DebuggerModel.Location[];
}