diff --git a/tools/profile.mjs b/tools/profile.mjs index b8eeb2fa2c..d426e4456c 100644 --- a/tools/profile.mjs +++ b/tools/profile.mjs @@ -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> 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"; + } + })(); + } } diff --git a/tools/sourcemap.mjs b/tools/sourcemap.mjs index 8ddab13cb7..ca0f8b98bb 100644 --- a/tools/sourcemap.mjs +++ b/tools/sourcemap.mjs @@ -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; diff --git a/tools/system-analyzer/helper.mjs b/tools/system-analyzer/helper.mjs index 1eccfbdc93..359bbcdd98 100644 --- a/tools/system-analyzer/helper.mjs +++ b/tools/system-analyzer/helper.mjs @@ -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; diff --git a/tools/system-analyzer/index.mjs b/tools/system-analyzer/index.mjs index 97fc4a2e26..030c72b690 100644 --- a/tools/system-analyzer/index.mjs +++ b/tools/system-analyzer/index.mjs @@ -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) { diff --git a/tools/system-analyzer/view/helper.mjs b/tools/system-analyzer/view/helper.mjs index 92a8ff930e..f18eb6b135 100644 --- a/tools/system-analyzer/view/helper.mjs +++ b/tools/system-analyzer/view/helper.mjs @@ -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) { diff --git a/tools/system-analyzer/view/script-panel.mjs b/tools/system-analyzer/view/script-panel.mjs index d90091bf5a..d9de7d4e52 100644 --- a/tools/system-analyzer/view/script-panel.mjs +++ b/tools/system-analyzer/view/script-panel.mjs @@ -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; } diff --git a/tools/system-analyzer/view/tool-tip.mjs b/tools/system-analyzer/view/tool-tip.mjs index 84ee99a44a..5be98ae214 100644 --- a/tools/system-analyzer/view/tool-tip.mjs +++ b/tools/system-analyzer/view/tool-tip.mjs @@ -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') {