[system-analyzer] Add source map support
- Asynchronously load source map from sourceMappingURL - Once loaded, annotate source positions with their original position - Update script panel tooltip to include link to original source - For the above, make DOM.element a slightly more flexible API, allowing defining attributes and children - Also fix ToolTipEvent handling to support nodes. - Shuffle around some code to make createScriptNode async, in case we want to load the source map when building the script node itself. - Drive-by: make source markers a simple backgroundColor when there is only one group. Change-Id: I0926807761cbfe8b6dd8ff5154815a7e5ccb39bf Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2972827 Commit-Queue: Leszek Swirski <leszeks@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Auto-Submit: Leszek Swirski <leszeks@chromium.org> Reviewed-by: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#75302}
This commit is contained in:
parent
329ee4d423
commit
18bcc9a6f2
@ -27,16 +27,22 @@
|
||||
|
||||
import { CodeMap, CodeEntry } from "./codemap.mjs";
|
||||
import { ConsArray } from "./consarray.mjs";
|
||||
import { WebInspector } from "./sourcemap.mjs";
|
||||
|
||||
// Used to associate log entries with source positions in scripts.
|
||||
// TODO: move to separate modules
|
||||
export class SourcePosition {
|
||||
script = null;
|
||||
line = -1;
|
||||
column = -1;
|
||||
entries = [];
|
||||
isFunction = false;
|
||||
originalPosition = undefined;
|
||||
|
||||
constructor(script, line, column) {
|
||||
this.script = script;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
this.entries = [];
|
||||
this.isFunction = false;
|
||||
}
|
||||
|
||||
addEntry(entry) {
|
||||
@ -66,9 +72,11 @@ export class Script {
|
||||
url;
|
||||
source;
|
||||
name;
|
||||
sourcePosition = undefined;
|
||||
// Map<line, Map<column, SourcePosition>>
|
||||
lineToColumn = new Map();
|
||||
_entries = [];
|
||||
_sourceMapState = "unknown";
|
||||
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
@ -89,6 +97,14 @@ export class Script {
|
||||
return this._entries;
|
||||
}
|
||||
|
||||
get startLine() {
|
||||
return this.sourcePosition?.line ?? 1;
|
||||
}
|
||||
|
||||
get sourceMapState() {
|
||||
return this._sourceMapState;
|
||||
}
|
||||
|
||||
findFunctionSourcePosition(sourcePosition) {
|
||||
// TODO(cbruni) implmenent
|
||||
return undefined;
|
||||
@ -100,6 +116,10 @@ export class Script {
|
||||
sourcePosition = new SourcePosition(this, line, column,)
|
||||
this._addSourcePosition(line, column, sourcePosition);
|
||||
}
|
||||
if (entry.entry?.type == "Script") {
|
||||
// Mark the source position of scripts, for inline scripts which
|
||||
this.sourcePosition = sourcePosition;
|
||||
}
|
||||
sourcePosition.addEntry(entry);
|
||||
this._entries.push(entry);
|
||||
return sourcePosition;
|
||||
@ -149,6 +169,67 @@ export class Script {
|
||||
matchingScripts.push(script);
|
||||
return url;
|
||||
}
|
||||
|
||||
ensureSourceMapCalculated(sourceMapFetchPrefix=undefined) {
|
||||
if (this._sourceMapState !== "unknown") return;
|
||||
|
||||
const sourceMapURLMatch =
|
||||
this.source.match(/\/\/# sourceMappingURL=(.*)\n/);
|
||||
if (!sourceMapURLMatch) {
|
||||
this._sourceMapState = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
this._sourceMapState = "loading";
|
||||
let sourceMapURL = sourceMapURLMatch[1];
|
||||
(async () => {
|
||||
try {
|
||||
let sourceMapPayload;
|
||||
try {
|
||||
sourceMapPayload = await fetch(sourceMapURL);
|
||||
} catch (e) {
|
||||
if (e instanceof TypeError && sourceMapFetchPrefix) {
|
||||
// Try again with fetch prefix.
|
||||
// TODO(leszeks): Remove the retry once the prefix is
|
||||
// configurable.
|
||||
sourceMapPayload =
|
||||
await fetch(sourceMapFetchPrefix + sourceMapURL);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
sourceMapPayload = await sourceMapPayload.text();
|
||||
|
||||
if (sourceMapPayload.startsWith(')]}')) {
|
||||
sourceMapPayload =
|
||||
sourceMapPayload.substring(sourceMapPayload.indexOf('\n'));
|
||||
}
|
||||
sourceMapPayload = JSON.parse(sourceMapPayload);
|
||||
const sourceMap =
|
||||
new WebInspector.SourceMap(sourceMapURL, sourceMapPayload);
|
||||
|
||||
const startLine = this.startLine;
|
||||
for (const sourcePosition of this.sourcePositions) {
|
||||
const line = sourcePosition.line - startLine;
|
||||
const column = sourcePosition.column - 1;
|
||||
const mapping = sourceMap.findEntry(line, column);
|
||||
if (mapping) {
|
||||
sourcePosition.originalPosition = {
|
||||
source: new URL(mapping[2], sourceMapURL).href,
|
||||
line: mapping[3] + 1,
|
||||
column: mapping[4] + 1
|
||||
};
|
||||
} else {
|
||||
sourcePosition.originalPosition = {source: null, line:0, column:0};
|
||||
}
|
||||
}
|
||||
this._sourceMapState = "loaded";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this._sourceMapState = "failed";
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -326,7 +326,7 @@ WebInspector.SourceMap.prototype = {
|
||||
} while (digit & this._VLQ_CONTINUATION_MASK);
|
||||
|
||||
// Fix the sign.
|
||||
const negative = result & 1;
|
||||
const negate = result & 1;
|
||||
// Use unsigned right shift, so that the 32nd bit is properly shifted
|
||||
// to the 31st, and the 32nd becomes unset.
|
||||
result >>>= 1;
|
||||
|
@ -26,6 +26,17 @@ export function delay(time) {
|
||||
return new Promise(resolver => setTimeout(resolver, time));
|
||||
}
|
||||
|
||||
export function defer() {
|
||||
let resolve_func, reject_func;
|
||||
const p = new Promise((resolve, reject) => {
|
||||
resolve_func = resolve;
|
||||
reject_func = resolve;
|
||||
});
|
||||
p.resolve = resolve_func;
|
||||
p.reject = reject_func;
|
||||
return p;
|
||||
}
|
||||
|
||||
export class Group {
|
||||
constructor(key, id, parentTotal, entries) {
|
||||
this.key = key;
|
||||
|
@ -312,7 +312,8 @@ class App {
|
||||
|
||||
handleToolTip(event) {
|
||||
let content = event.content;
|
||||
if (typeof content !== 'string') {
|
||||
if (typeof content !== 'string' &&
|
||||
!(content?.nodeType && content?.nodeName)) {
|
||||
content = content?.toolTipDict;
|
||||
}
|
||||
if (!content) {
|
||||
|
@ -123,13 +123,32 @@ export class CSSColor {
|
||||
}
|
||||
|
||||
export class DOM {
|
||||
static element(type, classes) {
|
||||
static element(type, options) {
|
||||
const node = document.createElement(type);
|
||||
if (classes !== undefined) {
|
||||
if (typeof classes === 'string') {
|
||||
node.className = classes;
|
||||
if (options !== undefined) {
|
||||
if (typeof options === 'string') {
|
||||
// Old behaviour: options = class string
|
||||
node.className = options;
|
||||
} else if (Array.isArray(options)) {
|
||||
// Old behaviour: options = class array
|
||||
DOM.addClasses(node, options);
|
||||
} else {
|
||||
DOM.addClasses(node, classes);
|
||||
// New behaviour: options = attribute dict
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (key == 'className') {
|
||||
node.className = value;
|
||||
} else if (key == 'classList') {
|
||||
node.classList = value;
|
||||
} else if (key == 'textContent') {
|
||||
node.textContent = value;
|
||||
} else if (key == 'children') {
|
||||
for (const child of value) {
|
||||
node.appendChild(child);
|
||||
}
|
||||
} else {
|
||||
node.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return node;
|
||||
@ -158,20 +177,20 @@ export class DOM {
|
||||
return button;
|
||||
}
|
||||
|
||||
static div(classes) {
|
||||
return this.element('div', classes);
|
||||
static div(options) {
|
||||
return this.element('div', options);
|
||||
}
|
||||
|
||||
static span(classes) {
|
||||
return this.element('span', classes);
|
||||
static span(options) {
|
||||
return this.element('span', options);
|
||||
}
|
||||
|
||||
static table(classes) {
|
||||
return this.element('table', classes);
|
||||
static table(options) {
|
||||
return this.element('table', options);
|
||||
}
|
||||
|
||||
static tbody(classes) {
|
||||
return this.element('tbody', classes);
|
||||
static tbody(options) {
|
||||
return this.element('tbody', options);
|
||||
}
|
||||
|
||||
static td(textOrNode, className) {
|
||||
|
@ -1,17 +1,21 @@
|
||||
// Copyright 2020 the V8 project 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 {groupBy} from '../helper.mjs';
|
||||
import {defer, groupBy} from '../helper.mjs';
|
||||
import {App} from '../index.mjs'
|
||||
|
||||
import {SelectRelatedEvent, ToolTipEvent} from './events.mjs';
|
||||
import {CollapsableElement, CSSColor, delay, DOM, formatBytes, gradientStopsFromGroups} from './helper.mjs';
|
||||
|
||||
// A source mapping proxy for source maps that don't have CORS headers.
|
||||
// TODO(leszeks): Make this configurable.
|
||||
const sourceMapFetchPrefix = 'http://localhost:8080/';
|
||||
|
||||
DOM.defineCustomElement('view/script-panel',
|
||||
(templateText) =>
|
||||
class SourcePanel extends CollapsableElement {
|
||||
_selectedSourcePositions = [];
|
||||
_sourcePositionsToMarkNodes = [];
|
||||
_sourcePositionsToMarkNodesPromise = Promise.resolve([]);
|
||||
_scripts = [];
|
||||
_script;
|
||||
|
||||
@ -34,6 +38,8 @@ DOM.defineCustomElement('view/script-panel',
|
||||
set script(script) {
|
||||
if (this._script === script) return;
|
||||
this._script = script;
|
||||
script.ensureSourceMapCalculated(sourceMapFetchPrefix);
|
||||
this._sourcePositionsToMarkNodesPromise = defer();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@ -82,15 +88,19 @@ DOM.defineCustomElement('view/script-panel',
|
||||
|
||||
async _renderSourcePanel() {
|
||||
let scriptNode;
|
||||
if (this._script) {
|
||||
const script = this._script;
|
||||
if (script) {
|
||||
await delay(1);
|
||||
if (script != this._script) return;
|
||||
const builder = new LineBuilder(this, this._script);
|
||||
scriptNode = builder.createScriptNode();
|
||||
this._sourcePositionsToMarkNodes = builder.sourcePositionToMarkers;
|
||||
scriptNode = await builder.createScriptNode(this._script.startLine);
|
||||
if (script != this._script) return;
|
||||
this._sourcePositionsToMarkNodesPromise.resolve(
|
||||
builder.sourcePositionToMarkers);
|
||||
} else {
|
||||
scriptNode = DOM.div();
|
||||
this._selectedMarkNodes = undefined;
|
||||
this._sourcePositionsToMarkNodes = new Map();
|
||||
this._sourcePositionsToMarkNodesPromise.resolve(new Map());
|
||||
}
|
||||
const oldScriptNode = this.script.childNodes[1];
|
||||
this.script.replaceChild(scriptNode, oldScriptNode);
|
||||
@ -98,22 +108,24 @@ DOM.defineCustomElement('view/script-panel',
|
||||
|
||||
async _focusSelectedMarkers() {
|
||||
await delay(100);
|
||||
const sourcePositionsToMarkNodes =
|
||||
await this._sourcePositionsToMarkNodesPromise;
|
||||
// Remove all marked nodes.
|
||||
for (let markNode of this._sourcePositionsToMarkNodes.values()) {
|
||||
for (let markNode of sourcePositionsToMarkNodes.values()) {
|
||||
markNode.className = '';
|
||||
}
|
||||
for (let sourcePosition of this._selectedSourcePositions) {
|
||||
if (sourcePosition.script !== this._script) continue;
|
||||
this._sourcePositionsToMarkNodes.get(sourcePosition).className = 'marked';
|
||||
sourcePositionsToMarkNodes.get(sourcePosition).className = 'marked';
|
||||
}
|
||||
this._scrollToFirstSourcePosition()
|
||||
this._scrollToFirstSourcePosition(sourcePositionsToMarkNodes)
|
||||
}
|
||||
|
||||
_scrollToFirstSourcePosition() {
|
||||
_scrollToFirstSourcePosition(sourcePositionsToMarkNodes) {
|
||||
const sourcePosition = this._selectedSourcePositions.find(
|
||||
each => each.script === this._script);
|
||||
if (!sourcePosition) return;
|
||||
const markNode = this._sourcePositionsToMarkNodes.get(sourcePosition);
|
||||
const markNode = sourcePositionsToMarkNodes.get(sourcePosition);
|
||||
markNode.scrollIntoView(
|
||||
{behavior: 'auto', block: 'center', inline: 'center'});
|
||||
}
|
||||
@ -135,7 +147,8 @@ DOM.defineCustomElement('view/script-panel',
|
||||
}
|
||||
|
||||
handleSourcePositionMouseOver(e) {
|
||||
const entries = e.target.sourcePosition.entries;
|
||||
const sourcePosition = e.target.sourcePosition;
|
||||
const entries = sourcePosition.entries;
|
||||
let text = groupBy(entries, each => each.constructor, true)
|
||||
.map(group => {
|
||||
let text = `${group.key.name}: ${group.count}\n`
|
||||
@ -147,7 +160,46 @@ DOM.defineCustomElement('view/script-panel',
|
||||
return text;
|
||||
})
|
||||
.join('\n');
|
||||
this.dispatchEvent(new ToolTipEvent(text, e.target));
|
||||
|
||||
let sourceMapContent;
|
||||
switch (this._script.sourceMapState) {
|
||||
case 'loaded': {
|
||||
const originalPosition = sourcePosition.originalPosition;
|
||||
if (originalPosition.source === null) {
|
||||
sourceMapContent =
|
||||
DOM.element('i', {textContent: 'no source mapping for location'});
|
||||
} else {
|
||||
sourceMapContent = DOM.element('a', {
|
||||
href: `${originalPosition.source}`,
|
||||
target: '_blank',
|
||||
textContent: `${originalPosition.source}:${originalPosition.line}:${
|
||||
originalPosition.column}`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'loading':
|
||||
sourceMapContent =
|
||||
DOM.element('i', {textContent: 'source map still loading...'});
|
||||
break;
|
||||
case 'failed':
|
||||
sourceMapContent =
|
||||
DOM.element('i', {textContent: 'source map failed to load'});
|
||||
break;
|
||||
case 'none':
|
||||
sourceMapContent = DOM.element('i', {textContent: 'no source map'});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const toolTipContent = DOM.div({
|
||||
children: [
|
||||
DOM.element('pre', {className: 'textContent', textContent: text}),
|
||||
sourceMapContent
|
||||
]
|
||||
});
|
||||
this.dispatchEvent(new ToolTipEvent(toolTipContent, e.target));
|
||||
}
|
||||
});
|
||||
|
||||
@ -214,49 +266,42 @@ class LineBuilder {
|
||||
_script;
|
||||
_clickHandler;
|
||||
_mouseoverHandler;
|
||||
_sourcePositions;
|
||||
_sourcePositionToMarkers = new Map();
|
||||
|
||||
constructor(panel, script) {
|
||||
this._script = script;
|
||||
this._clickHandler = panel.handleSourcePositionClick.bind(panel);
|
||||
this._mouseoverHandler = panel.handleSourcePositionMouseOver.bind(panel);
|
||||
// TODO: sort on script finalization.
|
||||
script.sourcePositions.sort((a, b) => {
|
||||
if (a.line === b.line) return a.column - b.column;
|
||||
return a.line - b.line;
|
||||
});
|
||||
this._sourcePositions = new SourcePositionIterator(script.sourcePositions);
|
||||
}
|
||||
|
||||
get sourcePositionToMarkers() {
|
||||
return this._sourcePositionToMarkers;
|
||||
}
|
||||
|
||||
createScriptNode() {
|
||||
async createScriptNode(startLine) {
|
||||
const scriptNode = DOM.div('scriptNode');
|
||||
let startLine = 1;
|
||||
// Try to infer the start line of the script from its code entries.
|
||||
for (const entry of this._script.entries) {
|
||||
if (entry.type.startsWith('Script')) {
|
||||
if (entry.sourcePosition) {
|
||||
startLine = entry.sourcePosition.line;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: sort on script finalization.
|
||||
this._script.sourcePositions.sort((a, b) => {
|
||||
if (a.line === b.line) return a.column - b.column;
|
||||
return a.line - b.line;
|
||||
});
|
||||
|
||||
const sourcePositionsIterator =
|
||||
new SourcePositionIterator(this._script.sourcePositions);
|
||||
scriptNode.style.counterReset = `sourceLineCounter ${startLine - 1}`;
|
||||
for (let [lineIndex, line] of lineIterator(
|
||||
this._script.source, startLine)) {
|
||||
scriptNode.appendChild(this._createLineNode(lineIndex, line));
|
||||
scriptNode.appendChild(
|
||||
this._createLineNode(sourcePositionsIterator, lineIndex, line));
|
||||
}
|
||||
return scriptNode;
|
||||
}
|
||||
|
||||
_createLineNode(lineIndex, line) {
|
||||
_createLineNode(sourcePositionsIterator, lineIndex, line) {
|
||||
const lineNode = DOM.span();
|
||||
let columnIndex = 0;
|
||||
for (const sourcePosition of this._sourcePositions.forLine(lineIndex)) {
|
||||
for (const sourcePosition of sourcePositionsIterator.forLine(lineIndex)) {
|
||||
const nextColumnIndex = sourcePosition.column - 1;
|
||||
lineNode.appendChild(document.createTextNode(
|
||||
line.substring(columnIndex, nextColumnIndex)));
|
||||
@ -280,10 +325,14 @@ class LineBuilder {
|
||||
marker.onmouseover = this._mouseoverHandler;
|
||||
|
||||
const entries = sourcePosition.entries;
|
||||
const stops = gradientStopsFromGroups(
|
||||
entries.length, '%', groupBy(entries, entry => entry.constructor),
|
||||
type => LineBuilder.colorMap.get(type));
|
||||
marker.style.backgroundImage = `linear-gradient(0deg,${stops.join(',')})`
|
||||
const groups = groupBy(entries, entry => entry.constructor);
|
||||
if (groups.length > 1) {
|
||||
const stops = gradientStopsFromGroups(
|
||||
entries.length, '%', groups, type => LineBuilder.colorMap.get(type));
|
||||
marker.style.backgroundImage = `linear-gradient(0deg,${stops.join(',')})`
|
||||
} else {
|
||||
marker.style.backgroundColor = LineBuilder.colorMap.get(groups[0].key)
|
||||
}
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ DOM.defineCustomElement(
|
||||
if (typeof content === 'string') {
|
||||
this.contentNode.innerHTML = content;
|
||||
this.contentNode.className = 'textContent';
|
||||
} else if (content?.nodeType && nodeType?.nodeName) {
|
||||
} else if (content?.nodeType && content?.nodeName) {
|
||||
this._setContentNode(content);
|
||||
} else {
|
||||
if (this.contentNode.firstChild?.localName == 'property-link-table') {
|
||||
|
Loading…
Reference in New Issue
Block a user