UNPKG

chrome-devtools-frontend

Version:
420 lines (374 loc) • 18.3 kB
// Copyright 2020 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 * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Bindings from '../../models/bindings/bindings.js'; import type * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as LinearMemoryInspectorComponents from './components/components.js'; import {Events as LmiEvents, LinearMemoryInspectorPane} from './LinearMemoryInspectorPane.js'; const UIStrings = { /** *@description Error message that shows up in the console if a buffer to be opened in the linear memory inspector cannot be found. */ couldNotOpenLinearMemory: 'Could not open linear memory inspector: failed locating buffer.', /** *@description A context menu item in the Scope View of the Sources Panel */ openInMemoryInspectorPanel: 'Open in Memory inspector panel', } as const; const str_ = i18n.i18n.registerUIStrings('panels/linear_memory_inspector/LinearMemoryInspectorController.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const LINEAR_MEMORY_INSPECTOR_OBJECT_GROUP = 'linear-memory-inspector'; const MEMORY_TRANSFER_MIN_CHUNK_SIZE = 1000; let controllerInstance: LinearMemoryInspectorController; export interface LazyUint8Array { getRange(start: number, end: number): Promise<Uint8Array<ArrayBuffer>>; length(): number; } export class RemoteArrayBufferWrapper implements LazyUint8Array { #remoteArrayBuffer: SDK.RemoteObject.RemoteArrayBuffer; constructor(arrayBuffer: SDK.RemoteObject.RemoteArrayBuffer) { this.#remoteArrayBuffer = arrayBuffer; } length(): number { return this.#remoteArrayBuffer.byteLength(); } async getRange(start: number, end: number): Promise<Uint8Array<ArrayBuffer>> { const newEnd = Math.min(end, this.length()); if (start < 0 || start > newEnd) { console.error(`Requesting invalid range of memory: (${start}, ${end})`); return new Uint8Array(0); } const array = await this.#remoteArrayBuffer.bytes(start, newEnd); return new Uint8Array(array ?? []); } } async function getBufferFromObject(obj: SDK.RemoteObject.RemoteObject): Promise<SDK.RemoteObject.RemoteArrayBuffer> { const response = await obj.runtimeModel().agent.invoke_callFunctionOn({ objectId: obj.objectId, functionDeclaration: 'function() { return this instanceof ArrayBuffer || (typeof SharedArrayBuffer !== \'undefined\' && this instanceof SharedArrayBuffer) ? this : this.buffer; }', silent: true, // Set object group in order to bind the object lifetime to the linear memory inspector. objectGroup: LINEAR_MEMORY_INSPECTOR_OBJECT_GROUP, }); const error = response.getError(); if (error) { throw new Error(`Remote object representing ArrayBuffer could not be retrieved: ${error}`); } obj = obj.runtimeModel().createRemoteObject(response.result); return new SDK.RemoteObject.RemoteArrayBuffer(obj); } interface SerializableSettings { valueTypes: LinearMemoryInspectorComponents.ValueInterpreterDisplayUtils.ValueType[]; valueTypeModes: Array<[ LinearMemoryInspectorComponents.ValueInterpreterDisplayUtils.ValueType, LinearMemoryInspectorComponents.ValueInterpreterDisplayUtils.ValueTypeMode, ]>; endianness: LinearMemoryInspectorComponents.ValueInterpreterDisplayUtils.Endianness; } export class LinearMemoryInspectorController extends SDK.TargetManager.SDKModelObserver<SDK.RuntimeModel.RuntimeModel> implements Common.Revealer.Revealer<SDK.RemoteObject.LinearMemoryInspectable>, UI.ContextMenu.Provider<ObjectUI.ObjectPropertiesSection.ObjectPropertyTreeElement> { #paneInstance = LinearMemoryInspectorPane.instance(); #bufferIdToRemoteObject = new Map<string, SDK.RemoteObject.RemoteObject>(); #bufferIdToHighlightInfo = new Map<string, LinearMemoryInspectorComponents.LinearMemoryViewerUtils.HighlightInfo>(); #settings: Common.Settings.Setting<SerializableSettings>; private constructor() { super(); SDK.TargetManager.TargetManager.instance().observeModels(SDK.RuntimeModel.RuntimeModel, this); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, this.#onGlobalObjectClear, this); this.#paneInstance.addEventListener(LmiEvents.VIEW_CLOSED, this.#viewClosed.bind(this)); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, this.#onDebuggerPause, this); const defaultValueTypeModes = LinearMemoryInspectorComponents.ValueInterpreterDisplayUtils.getDefaultValueTypeMapping(); const defaultSettings: SerializableSettings = { valueTypes: Array.from(defaultValueTypeModes.keys()), valueTypeModes: Array.from(defaultValueTypeModes), endianness: LinearMemoryInspectorComponents.ValueInterpreterDisplayUtils.Endianness.LITTLE, }; this.#settings = Common.Settings.Settings.instance().createSetting('lmi-interpreter-settings', defaultSettings); } static instance(): LinearMemoryInspectorController { if (controllerInstance) { return controllerInstance; } controllerInstance = new LinearMemoryInspectorController(); return controllerInstance; } static async getMemoryForAddress(memoryWrapper: LazyUint8Array, address: number): Promise<{memory: Uint8Array<ArrayBuffer>, offset: number}> { // Provide a chunk of memory that covers the address to show and some before and after // as 1. the address shown is not necessarily at the beginning of a page and // 2. to allow for fewer memory requests. const memoryChunkStart = Math.max(0, address - MEMORY_TRANSFER_MIN_CHUNK_SIZE / 2); const memoryChunkEnd = memoryChunkStart + MEMORY_TRANSFER_MIN_CHUNK_SIZE; const memory = await memoryWrapper.getRange(memoryChunkStart, memoryChunkEnd); return {memory, offset: memoryChunkStart}; } static async getMemoryRange(memoryWrapper: LazyUint8Array, start: number, end: number): Promise<Uint8Array<ArrayBuffer>> { // Check that the requested start is within bounds. // If the requested end is larger than the actual // memory, it will be automatically capped when // requesting the range. if (start < 0 || start > end || start >= memoryWrapper.length()) { throw new Error('Requested range is out of bounds.'); } const chunkEnd = Math.max(end, start + MEMORY_TRANSFER_MIN_CHUNK_SIZE); return await memoryWrapper.getRange(start, chunkEnd); } async evaluateExpression(callFrame: SDK.DebuggerModel.CallFrame, expressionName: string): Promise<SDK.RemoteObject.RemoteObject|undefined> { const result = await callFrame.evaluate({expression: expressionName}); if ('error' in result) { console.error(`Tried to evaluate the expression '${expressionName}' but got an error: ${result.error}`); return undefined; } if ('exceptionDetails' in result && result?.exceptionDetails?.text) { console.error( `Tried to evaluate the expression '${expressionName}' but got an exception: ${result.exceptionDetails.text}`); return undefined; } return result.object; } saveSettings(data: LinearMemoryInspectorComponents.LinearMemoryInspector.Settings): void { const valueTypes = Array.from(data.valueTypes); const modes = [...data.modes]; this.#settings.set({valueTypes, valueTypeModes: modes, endianness: data.endianness}); } loadSettings(): LinearMemoryInspectorComponents.LinearMemoryInspector.Settings { const settings = this.#settings.get(); return { valueTypes: new Set(settings.valueTypes), modes: new Map(settings.valueTypeModes), endianness: settings.endianness, }; } getHighlightInfo(bufferId: string): LinearMemoryInspectorComponents.LinearMemoryViewerUtils.HighlightInfo|undefined { return this.#bufferIdToHighlightInfo.get(bufferId); } removeHighlight( bufferId: string, highlightInfo: LinearMemoryInspectorComponents.LinearMemoryViewerUtils.HighlightInfo): void { const currentHighlight = this.getHighlightInfo(bufferId); if (currentHighlight === highlightInfo) { this.#bufferIdToHighlightInfo.delete(bufferId); } } setHighlightInfo( bufferId: string, highlightInfo: LinearMemoryInspectorComponents.LinearMemoryViewerUtils.HighlightInfo): void { this.#bufferIdToHighlightInfo.set(bufferId, highlightInfo); } #resetHighlightInfo(bufferId: string): void { this.#bufferIdToHighlightInfo.delete(bufferId); } static async retrieveDWARFMemoryObjectAndAddress(obj: SDK.RemoteObject.RemoteObject): Promise<{obj: SDK.RemoteObject.RemoteObject, address: number}|undefined> { if (!(obj instanceof Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject)) { return undefined; } const valueNode = obj; const address = obj.linearMemoryAddress; if (address === undefined) { return undefined; } const callFrame = valueNode.callFrame; const response = await obj.debuggerModel().agent.invoke_evaluateOnCallFrame({ callFrameId: callFrame.id, expression: 'memories[0]', }); const error = response.getError(); if (error) { console.error(error); Common.Console.Console.instance().error(i18nString(UIStrings.couldNotOpenLinearMemory)); } const runtimeModel = obj.debuggerModel().runtimeModel(); return {obj: runtimeModel.createRemoteObject(response.result), address}; } // This function returns the size of the source language value represented by the ValueNode or ExtensionRemoteObject. // If the value is a pointer, the function returns the size of the pointed-to value. If the pointed-to value is also a // pointer, it returns the size of the pointer (usually 4 bytes). This is the convention taken by the DWARF extension. // > double x = 42.0; // > double *ptr = &x; // > double **dblptr = &ptr; // // retrieveObjectSize(ptr_ValueNode) -> 8, the size of a double // retrieveObjectSize(dblptr_ValueNode) -> 4, the size of a pointer static extractObjectSize(obj: Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject): number { return obj.linearMemorySize ?? 0; } // The object type description corresponds to the type of the highlighted memory // that the user sees in the memory inspector. For pointers, we highlight the pointed to object. // // Example: The variable `x` has the type `int *`. Assume that `x` points to the value 3. // -> The memory inspector will jump to the address where 3 is stored. // -> The memory inspector will highlight the bytes that represent the 3. // -> The object type description of what we show will thus be `int` and not `int *`. static extractObjectTypeDescription(obj: SDK.RemoteObject.RemoteObject): string { const objType = obj.description; if (!objType) { return ''; } const lastChar = objType.charAt(objType.length - 1); const secondToLastChar = objType.charAt(objType.length - 2); const isPointerType = lastChar === '*' || lastChar === '&'; const isOneLevelPointer = secondToLastChar === ' '; if (!isPointerType) { return objType; } if (isOneLevelPointer) { // For example, 'int *'and 'int &' become 'int'. return objType.slice(0, objType.length - 2); } // For example, 'int **' becomes 'int *'. return objType.slice(0, objType.length - 1); } // When inspecting a pointer variable, we indicate that we display the pointed-to object in the viewer // by prepending an asterisk to the pointer expression's name (mimicking C++ dereferencing). // If the object isn't a pointer, we return the expression unchanged. // // Examples: // (int *) myNumber -> (int) *myNumber // (int[]) numbers -> (int[]) numbers static extractObjectName(obj: SDK.RemoteObject.RemoteObject, expression: string): string { const lastChar = obj.description?.charAt(obj.description.length - 1); const isPointerType = lastChar === '*'; if (isPointerType) { return '*' + expression; } return expression; } async reveal({object, expression}: SDK.RemoteObject.LinearMemoryInspectable, omitFocus?: boolean|undefined): Promise<void> { const response = await LinearMemoryInspectorController.retrieveDWARFMemoryObjectAndAddress(object); let memoryObject = object; let memoryAddress = undefined; if (response !== undefined) { memoryAddress = response.address; memoryObject = response.obj; } const buffer = await getBufferFromObject(memoryObject); const {internalProperties} = await buffer.object().getOwnProperties(false); const idProperty = internalProperties?.find(({name}) => name === '[[ArrayBufferData]]'); const id = idProperty?.value?.value; if (!id) { throw new Error('Unable to find backing store id for array buffer'); } const memoryProperty = internalProperties?.find(({name}) => name === '[[WebAssemblyMemory]]'); const memory = memoryProperty?.value; const highlightInfo = LinearMemoryInspectorController.extractHighlightInfo(object, expression); if (highlightInfo) { this.setHighlightInfo(id, highlightInfo); } else { this.#resetHighlightInfo(id); } if (this.#bufferIdToRemoteObject.has(id)) { this.#paneInstance.reveal(id, memoryAddress); void UI.ViewManager.ViewManager.instance().showView('linear-memory-inspector', omitFocus); return; } const title = String(memory ? memory.description : buffer.object().description); this.#bufferIdToRemoteObject.set(id, buffer.object()); const arrayBufferWrapper = new RemoteArrayBufferWrapper(buffer); this.#paneInstance.create(id, title, arrayBufferWrapper, memoryAddress); void UI.ViewManager.ViewManager.instance().showView('linear-memory-inspector', omitFocus); } appendApplicableItems( _event: Event, contextMenu: UI.ContextMenu.ContextMenu, target: ObjectUI.ObjectPropertiesSection.ObjectPropertyTreeElement): void { if (target.property.value?.isLinearMemoryInspectable()) { const expression = target.path(); const object = target.property.value; contextMenu.debugSection().appendItem( i18nString(UIStrings.openInMemoryInspectorPanel), this.reveal.bind(this, new SDK.RemoteObject.LinearMemoryInspectable(object, expression)), {jslogContext: 'reveal-in-memory-inspector'}); } } static extractHighlightInfo(obj: SDK.RemoteObject.RemoteObject, expression?: string): LinearMemoryInspectorComponents.LinearMemoryViewerUtils.HighlightInfo|undefined { if (!(obj instanceof Bindings.DebuggerLanguagePlugins.ExtensionRemoteObject)) { return undefined; } const startAddress = obj.linearMemoryAddress ?? 0; let highlightInfo; try { highlightInfo = { startAddress, size: LinearMemoryInspectorController.extractObjectSize(obj), name: expression ? LinearMemoryInspectorController.extractObjectName(obj, expression) : expression, type: LinearMemoryInspectorController.extractObjectTypeDescription(obj), }; } catch { highlightInfo = undefined; } return highlightInfo; } override modelRemoved(model: SDK.RuntimeModel.RuntimeModel): void { for (const [bufferId, remoteObject] of this.#bufferIdToRemoteObject) { if (model === remoteObject.runtimeModel()) { this.#bufferIdToRemoteObject.delete(bufferId); this.#resetHighlightInfo(bufferId); this.#paneInstance.close(bufferId); } } } #onDebuggerPause(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { const debuggerModel = event.data; for (const [bufferId, remoteObject] of this.#bufferIdToRemoteObject) { if (debuggerModel.runtimeModel() === remoteObject.runtimeModel()) { const topCallFrame = debuggerModel.debuggerPausedDetails()?.callFrames[0]; if (topCallFrame) { void this .updateHighlightedMemory(bufferId, topCallFrame) // Need to call refreshView in the callback to trigger re-render. .then(() => this.#paneInstance.refreshView(bufferId)); } else { this.#resetHighlightInfo(bufferId); this.#paneInstance.refreshView(bufferId); } } } } #onGlobalObjectClear(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { this.modelRemoved(event.data.runtimeModel()); } #viewClosed({data: bufferId}: Common.EventTarget.EventTargetEvent<string>): void { const remoteObj = this.#bufferIdToRemoteObject.get(bufferId); if (remoteObj) { remoteObj.release(); } this.#bufferIdToRemoteObject.delete(bufferId); this.#resetHighlightInfo(bufferId); } async updateHighlightedMemory(bufferId: string, callFrame: SDK.DebuggerModel.CallFrame): Promise<void> { const oldHighlightInfo = this.getHighlightInfo(bufferId); const expressionName = oldHighlightInfo?.name; if (!oldHighlightInfo || !expressionName) { this.#resetHighlightInfo(bufferId); return; } const obj = await this.evaluateExpression(callFrame, expressionName); if (!obj) { this.#resetHighlightInfo(bufferId); return; } const newHighlightInfo = LinearMemoryInspectorController.extractHighlightInfo(obj, expressionName); if (!newHighlightInfo || !this.#pointToSameMemoryObject(newHighlightInfo, oldHighlightInfo)) { this.#resetHighlightInfo(bufferId); } else { this.setHighlightInfo(bufferId, newHighlightInfo); } } #pointToSameMemoryObject( highlightInfoA: LinearMemoryInspectorComponents.LinearMemoryViewerUtils.HighlightInfo, highlightInfoB: LinearMemoryInspectorComponents.LinearMemoryViewerUtils.HighlightInfo): boolean { return highlightInfoA.type === highlightInfoB.type && highlightInfoA.startAddress === highlightInfoB.startAddress; } }