mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 13:18:29 +08:00
We re-create the whole breakpoints side panel every time we step, including the display of all the breakpoints even if they don't change. One expensive part of doing this is calculating a preview of the source line for which the breakpoint is set. To do this we need to search the source file for all line endings, which on very large files can be very expensive. Line endings are currently cached on the TextUtils.Text.Text object. We never get a chance to use this cached information though, because the whole _breakpoints ListModel is re-created from a new data model on each update, which we trigger on every Step. Ideally we would only regenerate the UI that actually changed which would avoid this recalculation on every step, but that is a bigger change that I'm not confident we can make right now without potentially breaking something. Instead, when (re)creating the breakpoint items for the ListModel, create and share the Text item between multiple breakpoints within the same source file. Now we do O(files with breakpoints set) line-ending calculations instead of O(breakpoints set). This would still be very slow if you had hundreds of large files with one breakpoint set in each, but I assume that is a way less common scenario. Line ending calculation is only about 50% of the cost of stepping at the moment, so this speeds up by a factor of 2x but still leaves more work to do (see screenshots of performance trace). Before: https://imgur.com/a/qOYHi3u After: https://imgur.com/a/8ydiuLO Bug: 1069694 Change-Id: I8d8baf955ef71ab12dc85d5e8103a8bf598ba7da Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2156351 Commit-Queue: Peter Marshall <petermarshall@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org>
395 lines
15 KiB
JavaScript
395 lines
15 KiB
JavaScript
// Copyright (c) 2015 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 Bindings from '../bindings/bindings.js';
|
|
import * as Common from '../common/common.js';
|
|
import * as SDK from '../sdk/sdk.js';
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
import * as Workspace from '../workspace/workspace.js';
|
|
|
|
/**
|
|
* @implements {UI.ContextFlavorListener.ContextFlavorListener}
|
|
* @implements {UI.ListControl.ListDelegate}
|
|
* @unrestricted
|
|
*/
|
|
export class JavaScriptBreakpointsSidebarPane extends UI.ThrottledWidget.ThrottledWidget {
|
|
constructor() {
|
|
super(true);
|
|
this.registerRequiredCSS('sources/javaScriptBreakpointsSidebarPane.css');
|
|
|
|
this._breakpointManager = Bindings.BreakpointManager.BreakpointManager.instance();
|
|
this._breakpointManager.addEventListener(Bindings.BreakpointManager.Events.BreakpointAdded, this.update, this);
|
|
this._breakpointManager.addEventListener(Bindings.BreakpointManager.Events.BreakpointRemoved, this.update, this);
|
|
Common.Settings.Settings.instance().moduleSetting('breakpointsActive').addChangeListener(this.update, this);
|
|
|
|
/** @type {!UI.ListModel.ListModel.<!BreakpointItem>} */
|
|
this._breakpoints = new UI.ListModel.ListModel();
|
|
/** @type {!UI.ListControl.ListControl.<!BreakpointItem>} */
|
|
this._list = new UI.ListControl.ListControl(this._breakpoints, this, UI.ListControl.ListMode.NonViewport);
|
|
UI.ARIAUtils.markAsList(this._list.element);
|
|
this.contentElement.appendChild(this._list.element);
|
|
|
|
this._emptyElement = this.contentElement.createChild('div', 'gray-info-message');
|
|
this._emptyElement.textContent = ls`No breakpoints`;
|
|
this._emptyElement.tabIndex = -1;
|
|
|
|
this.update();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Promise<?>}
|
|
*/
|
|
async doUpdate() {
|
|
const hadFocus = this.hasFocus();
|
|
const breakpointLocations = this._breakpointManager.allBreakpointLocations().filter(
|
|
breakpointLocation =>
|
|
breakpointLocation.uiLocation.uiSourceCode.project().type() !== Workspace.Workspace.projectTypes.Debugger);
|
|
if (!breakpointLocations.length) {
|
|
this._list.element.classList.add('hidden');
|
|
this._emptyElement.classList.remove('hidden');
|
|
this._breakpoints.replaceAll([]);
|
|
this._didUpdateForTest();
|
|
return Promise.resolve();
|
|
}
|
|
|
|
this._list.element.classList.remove('hidden');
|
|
this._emptyElement.classList.add('hidden');
|
|
|
|
breakpointLocations.sort((item1, item2) => item1.uiLocation.compareTo(item2.uiLocation));
|
|
|
|
/** @type {!Platform.Multimap<string, string>} */
|
|
const breakpointEntriesForLine = new Platform.Multimap();
|
|
|
|
/** @type {!Platform.Multimap<string, !{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UISourceCode.UILocation}>} */
|
|
const locationForEntry = new Platform.Multimap();
|
|
for (const breakpointLocation of breakpointLocations) {
|
|
const uiLocation = breakpointLocation.uiLocation;
|
|
const entryDescriptor = `${uiLocation.uiSourceCode.url()}:${uiLocation.lineNumber}:${uiLocation.columnNumber}`;
|
|
locationForEntry.set(entryDescriptor, breakpointLocation);
|
|
const lineDescriptor = `${uiLocation.uiSourceCode.url()}:${uiLocation.lineNumber}`;
|
|
breakpointEntriesForLine.set(lineDescriptor, entryDescriptor);
|
|
}
|
|
|
|
const details = self.UI.context.flavor(SDK.DebuggerModel.DebuggerPausedDetails);
|
|
const selectedUILocation = details && details.callFrames.length ?
|
|
await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(
|
|
details.callFrames[0].location()) :
|
|
null;
|
|
|
|
let shouldShowView = false;
|
|
const promises = [];
|
|
const breakpoints = [];
|
|
// Use a cache to share the Text objects between all breakpoints. This way
|
|
// we share the cached line ending information that Text calculates. This
|
|
// was very slow to calculate with a lot of breakpoints in the same very
|
|
// large source file.
|
|
/** @type {!Map<string, !TextUtils.Text.Text>} */
|
|
const contentToTextMap = new Map();
|
|
let itemToSelect;
|
|
for (const descriptor of locationForEntry.keysArray()) {
|
|
const locations = Array.from(locationForEntry.get(descriptor));
|
|
const breakpointLocation = locations[0];
|
|
const uiLocation = breakpointLocation.uiLocation;
|
|
const isSelected =
|
|
!!selectedUILocation && locations.some(location => location.uiLocation.id() === selectedUILocation.id());
|
|
const showColumn =
|
|
breakpointEntriesForLine.get(`${uiLocation.uiSourceCode.url()}:${uiLocation.lineNumber}`).size > 1;
|
|
|
|
const content = uiLocation.uiSourceCode.requestContent();
|
|
const contentAsText = content.then(content => {
|
|
if (contentToTextMap.has(content.content)) {
|
|
return contentToTextMap.get(content.content);
|
|
}
|
|
const text = new TextUtils.Text.Text(content.content || '');
|
|
contentToTextMap.set(content.content, text);
|
|
return text;
|
|
});
|
|
promises.push(contentAsText);
|
|
const item = {breakpointLocation, locations, isSelected, showColumn, contentAsText};
|
|
breakpoints.push(item);
|
|
if (this._list.selectedItem() &&
|
|
this._list.selectedItem().breakpointLocation.breakpoint === breakpointLocation.breakpoint) {
|
|
itemToSelect = item;
|
|
}
|
|
if (isSelected) {
|
|
shouldShowView = true;
|
|
}
|
|
}
|
|
if (shouldShowView) {
|
|
UI.ViewManager.ViewManager.instance().showView('sources.jsBreakpoints');
|
|
}
|
|
this._list.element.classList.toggle(
|
|
'breakpoints-list-deactivated', !Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get());
|
|
this._breakpoints.replaceAll(breakpoints);
|
|
this._list.selectItem(itemToSelect || this._breakpoints.at(0));
|
|
if (hadFocus) {
|
|
this.focus();
|
|
}
|
|
return Promise.all(promises).then(() => this._didUpdateForTest());
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!BreakpointItem} item
|
|
* @return {!Element}
|
|
*/
|
|
createElementForItem(item) {
|
|
const element = createElementWithClass('div', 'breakpoint-entry');
|
|
UI.ARIAUtils.markAsListitem(element);
|
|
element.tabIndex = this._list.selectedItem() === item ? 0 : -1;
|
|
element.addEventListener('contextmenu', this._breakpointContextMenu.bind(this), true);
|
|
element.addEventListener('click', this._revealLocation.bind(this, element), false);
|
|
const checkboxLabel = UI.UIUtils.CheckboxLabel.create('');
|
|
|
|
const uiLocation = item.locations[0].uiLocation;
|
|
const hasEnabled = item.locations.some(location => location.breakpoint.enabled());
|
|
const hasDisabled = item.locations.some(location => !location.breakpoint.enabled());
|
|
checkboxLabel.textElement.textContent =
|
|
uiLocation.linkText() + (item.showColumn ? ':' + (uiLocation.columnNumber + 1) : '');
|
|
checkboxLabel.checkboxElement.checked = hasEnabled;
|
|
checkboxLabel.checkboxElement.indeterminate = hasEnabled && hasDisabled;
|
|
checkboxLabel.checkboxElement.tabIndex = -1;
|
|
checkboxLabel.addEventListener('click', this._breakpointCheckboxClicked.bind(this), false);
|
|
element.appendChild(checkboxLabel);
|
|
let checkedDescription = hasEnabled ? ls`checked` : ls`unchecked`;
|
|
if (hasEnabled && hasDisabled) {
|
|
checkedDescription = ls`mixed`;
|
|
}
|
|
if (item.isSelected) {
|
|
UI.ARIAUtils.setDescription(element, ls`${checkedDescription} breakpoint hit`);
|
|
element.classList.add('breakpoint-hit');
|
|
this.setDefaultFocusedElement(element);
|
|
} else {
|
|
UI.ARIAUtils.setDescription(element, checkedDescription);
|
|
}
|
|
|
|
element.addEventListener('keydown', event => {
|
|
if (event.key === ' ') {
|
|
checkboxLabel.checkboxElement.click();
|
|
event.consume(true);
|
|
}
|
|
});
|
|
|
|
const snippetElement = element.createChild('div', 'source-text monospace');
|
|
item.contentAsText.then(text => {
|
|
const lineNumber = uiLocation.lineNumber;
|
|
if (lineNumber < text.lineCount()) {
|
|
const lineText = text.lineAt(lineNumber);
|
|
const maxSnippetLength = 200;
|
|
snippetElement.textContent =
|
|
lineText.substring(item.showColumn ? uiLocation.columnNumber : 0).trimEndWithMaxLength(maxSnippetLength);
|
|
}
|
|
});
|
|
|
|
element[breakpointLocationsSymbol] = item.locations;
|
|
element[locationSymbol] = uiLocation;
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!BreakpointItem} item
|
|
* @return {number}
|
|
*/
|
|
heightForItem(item) {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!BreakpointItem} item
|
|
* @return {boolean}
|
|
*/
|
|
isItemSelectable(item) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?BreakpointItem} from
|
|
* @param {?BreakpointItem} to
|
|
* @param {?Element} fromElement
|
|
* @param {?Element} toElement
|
|
*/
|
|
selectedItemChanged(from, to, fromElement, toElement) {
|
|
if (fromElement) {
|
|
fromElement.tabIndex = -1;
|
|
}
|
|
if (toElement) {
|
|
toElement.tabIndex = 0;
|
|
this.setDefaultFocusedElement(toElement);
|
|
if (this.hasFocus()) {
|
|
toElement.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?Element} fromElement
|
|
* @param {?Element} toElement
|
|
* @return {boolean}
|
|
*/
|
|
updateSelectedItemARIA(fromElement, toElement) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @return {!Array<!Bindings.BreakpointManager.BreakpointLocation>}
|
|
*/
|
|
_breakpointLocations(event) {
|
|
if (event.target instanceof Element) {
|
|
return this._breakpointLocationsForElement(event.target);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @return {!Array<!Bindings.BreakpointManager.BreakpointLocation>}
|
|
*/
|
|
_breakpointLocationsForElement(element) {
|
|
const node = element.enclosingNodeOrSelfWithClass('breakpoint-entry');
|
|
if (!node) {
|
|
return [];
|
|
}
|
|
return node[breakpointLocationsSymbol] || [];
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_breakpointCheckboxClicked(event) {
|
|
const hadFocus = this.hasFocus();
|
|
const breakpoints = this._breakpointLocations(event).map(breakpointLocation => breakpointLocation.breakpoint);
|
|
const newState = event.target.checkboxElement.checked;
|
|
for (const breakpoint of breakpoints) {
|
|
breakpoint.setEnabled(newState);
|
|
const item =
|
|
this._breakpoints.find(breakpointItem => breakpointItem.breakpointLocation.breakpoint === breakpoint);
|
|
if (item) {
|
|
this._list.refreshItem(item);
|
|
}
|
|
}
|
|
if (hadFocus) {
|
|
this.focus();
|
|
}
|
|
event.consume();
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
*/
|
|
_revealLocation(element) {
|
|
const uiLocations =
|
|
this._breakpointLocationsForElement(element).map(breakpointLocation => breakpointLocation.uiLocation);
|
|
let uiLocation = null;
|
|
for (const uiLocationCandidate of uiLocations) {
|
|
if (!uiLocation || uiLocationCandidate.columnNumber < uiLocation.columnNumber) {
|
|
uiLocation = uiLocationCandidate;
|
|
}
|
|
}
|
|
if (uiLocation) {
|
|
Common.Revealer.reveal(uiLocation);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_breakpointContextMenu(event) {
|
|
const breakpoints = this._breakpointLocations(event).map(breakpointLocation => breakpointLocation.breakpoint);
|
|
|
|
const contextMenu = new UI.ContextMenu.ContextMenu(event);
|
|
const removeEntryTitle = breakpoints.length > 1 ? Common.UIString.UIString('Remove all breakpoints in line') :
|
|
Common.UIString.UIString('Remove breakpoint');
|
|
contextMenu.defaultSection().appendItem(
|
|
removeEntryTitle, () => breakpoints.map(breakpoint => breakpoint.remove(false /* keepInStorage */)));
|
|
if (event.target instanceof Element) {
|
|
contextMenu.defaultSection().appendItem(ls`Reveal location`, this._revealLocation.bind(this, event.target));
|
|
}
|
|
|
|
const breakpointActive = Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get();
|
|
const breakpointActiveTitle = breakpointActive ? Common.UIString.UIString('Deactivate breakpoints') :
|
|
Common.UIString.UIString('Activate breakpoints');
|
|
contextMenu.defaultSection().appendItem(
|
|
breakpointActiveTitle,
|
|
() => Common.Settings.Settings.instance().moduleSetting('breakpointsActive').set(!breakpointActive));
|
|
|
|
if (breakpoints.some(breakpoint => !breakpoint.enabled())) {
|
|
const enableTitle = Common.UIString.UIString('Enable all breakpoints');
|
|
contextMenu.defaultSection().appendItem(enableTitle, this._toggleAllBreakpoints.bind(this, true));
|
|
}
|
|
if (breakpoints.some(breakpoint => breakpoint.enabled())) {
|
|
const disableTitle = Common.UIString.UIString('Disable all breakpoints');
|
|
contextMenu.defaultSection().appendItem(disableTitle, this._toggleAllBreakpoints.bind(this, false));
|
|
}
|
|
const removeAllTitle = Common.UIString.UIString('Remove all breakpoints');
|
|
contextMenu.defaultSection().appendItem(removeAllTitle, this._removeAllBreakpoints.bind(this));
|
|
const removeOtherTitle = Common.UIString.UIString('Remove other breakpoints');
|
|
contextMenu.defaultSection().appendItem(
|
|
removeOtherTitle, this._removeOtherBreakpoints.bind(this, new Set(breakpoints)));
|
|
contextMenu.show();
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} toggleState
|
|
*/
|
|
_toggleAllBreakpoints(toggleState) {
|
|
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations()) {
|
|
breakpointLocation.breakpoint.setEnabled(toggleState);
|
|
}
|
|
}
|
|
|
|
_removeAllBreakpoints() {
|
|
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations()) {
|
|
breakpointLocation.breakpoint.remove(false /* keepInStorage */);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Set<!Bindings.BreakpointManager.Breakpoint>} selectedBreakpoints
|
|
*/
|
|
_removeOtherBreakpoints(selectedBreakpoints) {
|
|
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations()) {
|
|
if (!selectedBreakpoints.has(breakpointLocation.breakpoint)) {
|
|
breakpointLocation.breakpoint.remove(false /* keepInStorage */);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?Object} object
|
|
*/
|
|
flavorChanged(object) {
|
|
this.update();
|
|
}
|
|
|
|
_didUpdateForTest() {
|
|
}
|
|
}
|
|
|
|
export const locationSymbol = Symbol('location');
|
|
export const checkboxLabelSymbol = Symbol('checkbox-label');
|
|
export const snippetElementSymbol = Symbol('snippet-element');
|
|
export const breakpointLocationsSymbol = Symbol('locations');
|
|
|
|
/** @typedef {{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UISourceCode.UILocation}} */
|
|
export let Breakpoint;
|
|
|
|
/**
|
|
* @typedef {{
|
|
* breakpointLocation: !Breakpoint,
|
|
* locations: !Array.<!Breakpoint>,
|
|
* showColumn: boolean,
|
|
* isSelected: boolean,
|
|
* contentAsText: !Promise.<TextUtils.Text.Text>
|
|
* }}
|
|
*/
|
|
export let BreakpointItem;
|