diff --git a/tools/profile.js b/tools/profile.js index 991349a348..0439bb9eed 100644 --- a/tools/profile.js +++ b/tools/profile.js @@ -38,6 +38,8 @@ function Profile() { this.bottomUpTree_ = new CallTree(); this.c_entries_ = {}; this.ticks_ = []; + this.scripts_ = []; + this.urlToScript_ = new Map(); }; @@ -226,8 +228,21 @@ Profile.prototype.addSourcePositions = function ( /** * Adds script source code. */ -Profile.prototype.addScriptSource = function (script, source) { - // CLI does not need source code => ignore. +Profile.prototype.addScriptSource = function (scriptId, url, source) { + this.scripts_[scriptId] = { + scriptId: scriptId, + name: url, + source: source + }; + this.urlToScript_.set(url, source); +}; + + +/** + * Adds script source code. + */ +Profile.prototype.getScript = function (url) { + return this.urlToScript_.get(url); }; /** @@ -1018,8 +1033,9 @@ JsonProfile.prototype.addSourcePositions = function ( }; }; -JsonProfile.prototype.addScriptSource = function (script, url, source) { - this.scripts_[script] = { +JsonProfile.prototype.addScriptSource = function (scriptId, url, source) { + this.scripts_[scriptId] = { + scriptId: scriptId, name: url, source: source }; diff --git a/tools/system-analyzer/app-model.mjs b/tools/system-analyzer/app-model.mjs index 821d3a2c17..37fa5ae2f3 100644 --- a/tools/system-analyzer/app-model.mjs +++ b/tools/system-analyzer/app-model.mjs @@ -8,6 +8,7 @@ class State { #ic; #selectedMapLogEvents; #selectedIcLogEvents; + #selectedSourcePositionLogEvents; #nofChunks; #chunks; #icTimeline; @@ -81,6 +82,12 @@ class State { if (!value) return; this.#selectedMapLogEvents = value; } + get selectedSourcePositionLogEvents() { + return this.#selectedSourcePositionLogEvents; + } + set selectedSourcePositionLogEvents(value) { + this.#selectedSourcePositionLogEvents = value; + } get selectedIcLogEvents() { return this.#selectedIcLogEvents; } diff --git a/tools/system-analyzer/event.mjs b/tools/system-analyzer/event.mjs index f3af966a23..438fc1e015 100644 --- a/tools/system-analyzer/event.mjs +++ b/tools/system-analyzer/event.mjs @@ -18,4 +18,14 @@ class Event { } } -export { Event }; +class SourcePositionLogEvent extends Event { + constructor(type, time, file, line, col, script) { + super(type, time); + this.file = file; + this.line = line; + this.col = col; + this.script = script; + } +} + +export { Event, SourcePositionLogEvent }; diff --git a/tools/system-analyzer/ic-panel.mjs b/tools/system-analyzer/ic-panel.mjs index 6f0e0cb558..d97f66022a 100644 --- a/tools/system-analyzer/ic-panel.mjs +++ b/tools/system-analyzer/ic-panel.mjs @@ -5,12 +5,12 @@ import { Group } from './ic-model.mjs'; import CustomIcProcessor from "./ic-processor.mjs"; import { MapLogEvent } from "./map-processor.mjs"; +import { SourcePositionLogEvent } from './event.mjs'; import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs'; import { defineCustomElement, V8CustomElement } from './helper.mjs'; defineCustomElement('ic-panel', (templateText) => class ICPanel extends V8CustomElement { - //TODO(zcankara) Entries never set #selectedLogEvents; #timeline; constructor() { @@ -29,8 +29,6 @@ defineCustomElement('ic-panel', (templateText) => this.selectedLogEvents = this.timeline.all; this.updateCount(); } - - get groupKey() { return this.$('#group-key'); } @@ -105,26 +103,38 @@ defineCustomElement('ic-panel', (templateText) => } handleMapClick(e) { - let entry = e.target.parentNode.entry; - let id = entry.key; - let selectedMapLofEvents = + const entry = e.target.parentNode.entry; + const id = entry.key; + const selectedMapLogEvents = this.searchIcLogEventToMapLogEvent(id, entry.entries); - this.dispatchEvent(new SelectionEvent(selectedMapLofEvents)); + this.dispatchEvent(new SelectionEvent(selectedMapLogEvents)); } searchIcLogEventToMapLogEvent(id, icLogEvents) { // searches for mapLogEvents using the id, time - let selectedMapLogEventsSet = new Set(); + const selectedMapLogEventsSet = new Set(); for (const icLogEvent of icLogEvents) { - let time = icLogEvent.time; - let selectedMap = MapLogEvent.get(id, time); + const time = icLogEvent.time; + const selectedMap = MapLogEvent.get(id, time); selectedMapLogEventsSet.add(selectedMap); } return Array.from(selectedMapLogEventsSet); } handleFilePositionClick(e) { - this.dispatchEvent(new FocusEvent(e.target.parentNode.entry.key)); + const entry = e.target.parentNode.entry; + const filePosition = + this.createSourcePositionLogEvent( + entry.entries[0].type, entry.entries[0].time, entry.key, + entry.entries[0].script); + this.dispatchEvent(new FocusEvent(filePosition)); + } + + createSourcePositionLogEvent(type, time, filePositionLine, script) { + const [file, line, col] = filePositionLine.split(':'); + const filePosition = new SourcePositionLogEvent(type, time, + file, line, col, script); + return filePosition } render(entries, parent) { diff --git a/tools/system-analyzer/ic-processor.mjs b/tools/system-analyzer/ic-processor.mjs index 4de67f5cb9..9b14f770ff 100644 --- a/tools/system-analyzer/ic-processor.mjs +++ b/tools/system-analyzer/ic-processor.mjs @@ -44,6 +44,10 @@ class IcProcessor extends LogReader { ], processor: this.processV8Version }, + 'script-source': { + parsers: [parseInt, parseString, parseString], + processor: this.processScriptSource + }, 'code-move': { parsers: [parseInt, parseInt], processor: this.processCodeMove }, 'code-delete': { parsers: [parseInt], processor: this.processCodeDelete }, @@ -122,6 +126,9 @@ class IcProcessor extends LogReader { `Please use the matching tool for given the V8 version.`); } } + processScriptSource(scriptId, url, script) { + this.#profile.addScriptSource(scriptId, url, script); + } processLogFile(fileName) { this.collectEntries = true; this.lastLogFileName_ = fileName; @@ -181,6 +188,11 @@ class IcProcessor extends LogReader { ' (map 0x' + map.toString(16) + ')' + (slow_reason ? ' ' + slow_reason : '') + 'time: ' + time); } + + getScript(url) { + return this.#profile.getScript(url); + } + } // ================ @@ -209,9 +221,12 @@ class CustomIcProcessor extends IcProcessor { type, pc, time, line, column, old_state, new_state, map, key, modifier, slow_reason) { let fnName = this.functionName(pc); + let parts = fnName.split(' '); + let fileName = parts[1]; + let script = this.getScript(fileName); let entry = new IcLogEvent( type, fnName, time, line, column, key, old_state, new_state, map, - slow_reason); + slow_reason, script); this.#timeline.push(entry); } @@ -229,7 +244,7 @@ class CustomIcProcessor extends IcProcessor { class IcLogEvent extends Event { constructor( type, fn_file, time, line, column, key, oldState, newState, map, reason, - additional) { + script, additional) { super(type, time); this.category = 'other'; if (this.type.indexOf('Store') !== -1) { @@ -249,6 +264,7 @@ class IcLogEvent extends Event { this.map = map; this.reason = reason; this.additional = additional; + this.script = script; } diff --git a/tools/system-analyzer/index.css b/tools/system-analyzer/index.css index d57727950a..38476c6d48 100644 --- a/tools/system-analyzer/index.css +++ b/tools/system-analyzer/index.css @@ -135,6 +135,10 @@ button { background-color: var(--error-color); } +.highlight { + background-color: var(--primary-color); + color: var(--on-primary-color); +} .clickable:hover, .clickable:active { background-color: var(--primary-color); diff --git a/tools/system-analyzer/index.html b/tools/system-analyzer/index.html index 6f5c483974..400e573f3f 100644 --- a/tools/system-analyzer/index.html +++ b/tools/system-analyzer/index.html @@ -115,9 +115,9 @@ found in the LICENSE file. --> @@ -141,6 +141,7 @@ found in the LICENSE file. --> +
diff --git a/tools/system-analyzer/index.mjs b/tools/system-analyzer/index.mjs index e339bc1d13..4ead408cfa 100644 --- a/tools/system-analyzer/index.mjs +++ b/tools/system-analyzer/index.mjs @@ -8,17 +8,19 @@ import { IcLogEvent } from "./ic-processor.mjs"; import { State } from "./app-model.mjs"; import { MapProcessor, MapLogEvent } from "./map-processor.mjs"; import { SelectTimeEvent } from "./events.mjs"; +import { SourcePositionLogEvent } from "./event.mjs"; import { $ } from "./helper.mjs"; import "./ic-panel.mjs"; import "./timeline-panel.mjs"; import "./map-panel.mjs"; import "./log-file-reader.mjs"; +import "./source-panel.mjs"; class App { #state; #view; #navigation; constructor(fileReaderId, mapPanelId, timelinePanelId, - icPanelId, mapTrackId, icTrackId) { + icPanelId, mapTrackId, icTrackId, sourcePanelId) { this.#view = { logFileReader: $(fileReaderId), icPanel: $(icPanelId), @@ -26,6 +28,7 @@ class App { timelinePanel: $(timelinePanelId), mapTrack: $(mapTrackId), icTrack: $(icTrackId), + sourcePanel: $(sourcePanelId) }; this.#state = new State(); this.#navigation = new Navigation(this.#state, this.#view); @@ -53,6 +56,8 @@ class App { this.showMapEntries(e.entries); } else if (e.entries[0] instanceof IcLogEvent) { this.showIcEntries(e.entries); + } else if (e.entries[0] instanceof SourcePositionLogEvent) { + this.showSourcePositionEntries(e.entries); } else { console.error("Undefined selection!"); } @@ -65,6 +70,10 @@ class App { this.#state.selectedIcLogEvents = entries; this.#view.icPanel.selectedLogEvents = this.#state.selectedIcLogEvents; } + showSourcePositionEntries(entries) { + //TODO(zcankara) Handle multiple source position selection events + this.#view.sourcePanel.selectedSourcePositions = entries; + } handleTimeRangeSelect(e) { this.selectTimeRange(e.start, e.end); @@ -74,16 +83,12 @@ class App { this.selectMapLogEvent(e.entry); } else if (e.entry instanceof IcLogEvent) { this.selectICLogEvent(e.entry); - } else if (typeof e.entry === 'string') { + } else if (e.entry instanceof SourcePositionLogEvent) { this.selectSourcePositionEvent(e.entry); } else { console.log("undefined"); } } - handleClickSourcePositions(e) { - //TODO(zcankara) Handle source position - console.log("Entry containing source position: ", e.entries); - } selectTimeRange(start, end) { this.#state.timeSelection.start = start; this.#state.timeSelection.end = end; @@ -103,8 +108,11 @@ class App { this.#view.icPanel.selectedLogEvents = [entry]; } selectSourcePositionEvent(sourcePositions) { + if (!sourcePositions.script) return; console.log("source positions: ", sourcePositions); + this.#view.sourcePanel.selectedSourcePositions = [sourcePositions]; } + handleFileUpload(e) { this.restartApp(); $("#container").className = "initial"; diff --git a/tools/system-analyzer/map-panel/map-details-template.html b/tools/system-analyzer/map-panel/map-details-template.html index b4b1541fd8..6d1b268c5d 100644 --- a/tools/system-analyzer/map-panel/map-details-template.html +++ b/tools/system-analyzer/map-panel/map-details-template.html @@ -6,7 +6,8 @@ found in the LICENSE file. -->

Map Details

+
diff --git a/tools/system-analyzer/map-panel/map-details.mjs b/tools/system-analyzer/map-panel/map-details.mjs index dddccf7d8b..35a3443d67 100644 --- a/tools/system-analyzer/map-panel/map-details.mjs +++ b/tools/system-analyzer/map-panel/map-details.mjs @@ -3,6 +3,7 @@ // found in the LICENSE file. import { V8CustomElement, defineCustomElement } from "../helper.mjs"; import { FocusEvent } from "../events.mjs"; +import { SourcePositionLogEvent } from '../event.mjs'; defineCustomElement( "./map-panel/map-details", @@ -10,8 +11,8 @@ defineCustomElement( class MapDetails extends V8CustomElement { constructor() { super(templateText); - this.mapDetails.addEventListener("click", () => - this.handleClickSourcePositions() + this.#filePositionNode.addEventListener("click", e => + this.handleFilePositionClick(e) ); this.selectedMap = undefined; } @@ -19,23 +20,44 @@ defineCustomElement( return this.$("#mapDetails"); } + get #filePositionNode() { + return this.$("#filePositionNode"); + } + setSelectedMap(value) { this.selectedMap = value; } set mapDetails(map) { let details = ""; + let clickableDetails = ""; if (map) { - details += "ID: " + map.id; - details += "\nSource location: " + map.filePosition; + clickableDetails += "ID: " + map.id; + clickableDetails += "\nSource location: " + map.filePosition; details += "\n" + map.description; this.setSelectedMap(map); } + this.#filePositionNode.innerText = clickableDetails; + this.#filePositionNode.classList.add("clickable"); this.mapDetails.innerText = details; } - handleClickSourcePositions() { - this.dispatchEvent(new FocusEvent(this.selectedMap.filePosition)); + handleFilePositionClick() { + let filePosition = + this.createSourcePositionLogEvent( + this.selectedMap.type, this.selectedMap.time, + this.selectedMap.filePosition, this.selectedMap.script); + this.dispatchEvent(new FocusEvent(filePosition)); + } + + createSourcePositionLogEvent(type, time, filePositionLine, script) { + // remove token + if (!(/\s/.test(filePositionLine))) return; + filePositionLine = filePositionLine.split(' '); + let [file, line, col] = filePositionLine[1].split(':'); + let filePosition = new SourcePositionLogEvent(type, time, + file, line, col, script); + return filePosition } } ); diff --git a/tools/system-analyzer/map-processor.mjs b/tools/system-analyzer/map-processor.mjs index 17a4b01112..04a47e4680 100644 --- a/tools/system-analyzer/map-processor.mjs +++ b/tools/system-analyzer/map-processor.mjs @@ -54,6 +54,10 @@ class MapProcessor extends LogReader { ], processor: this.processV8Version }, + 'script-source': { + parsers: [parseInt, parseString, parseString], + processor: this.processScriptSource + }, 'code-move': { parsers: [parseInt, parseInt], 'sfi-move': @@ -184,6 +188,10 @@ class MapProcessor extends LogReader { } } + processScriptSource(scriptId, url, source) { + this.#profile.addScriptSource(scriptId, url, source); + } + processCodeMove(from, to) { this.#profile.moveCode(from, to); } @@ -211,6 +219,12 @@ class MapProcessor extends LogReader { } return entry + ':' + line + ':' + column; } + processFileName(filePositionLine) { + if (!(/\s/.test(filePositionLine))) return; + filePositionLine = filePositionLine.split(' '); + let file = filePositionLine[1].split(':')[0]; + return file; + } processMap(type, time, from, to, pc, line, column, reason, name) { let time_ = parseInt(time); @@ -219,6 +233,8 @@ class MapProcessor extends LogReader { let to_ = this.getExistingMap(to, time_); let edge = new Edge(type, name, reason, time, from_, to_); to_.filePosition = this.formatPC(pc, line, column); + let fileName = this.processFileName(to_.filePosition); + to_.script = this.getScript(fileName); edge.finishSetup(); } @@ -254,6 +270,10 @@ class MapProcessor extends LogReader { }; return map; } + + getScript(url) { + return this.#profile.getScript(url); + } } // =========================================================================== @@ -268,6 +288,7 @@ class MapLogEvent extends Event { leftId = 0; rightId = 0; filePosition = ''; + script = ''; id = -1; constructor(id, time) { if (!time) throw new Error('Invalid time'); diff --git a/tools/system-analyzer/source-panel-template.html b/tools/system-analyzer/source-panel-template.html new file mode 100644 index 0000000000..d3b77b27bc --- /dev/null +++ b/tools/system-analyzer/source-panel-template.html @@ -0,0 +1,34 @@ + + + +
+

Source Panel

+
+

+  
+
diff --git a/tools/system-analyzer/source-panel.mjs b/tools/system-analyzer/source-panel.mjs new file mode 100644 index 0000000000..be0fa266eb --- /dev/null +++ b/tools/system-analyzer/source-panel.mjs @@ -0,0 +1,72 @@ +// 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 { V8CustomElement, defineCustomElement } from "./helper.mjs"; + +defineCustomElement( + "source-panel", + (templateText) => + class SourcePanel extends V8CustomElement { + #selectedSourcePositions; + constructor() { + super(templateText); + } + get script() { + return this.$('#script'); + } + get scriptNode() { + return this.$('.scriptNode'); + } + set script(script) { + this.renderSourcePanel(script); + } + set selectedSourcePositions(sourcePositions) { + this.#selectedSourcePositions = sourcePositions; + this.renderSourcePanelSelectedHighlight(); + } + get selectedSourcePositions() { + return this.#selectedSourcePositions; + } + highlightSourcePosition(line, col, script) { + //TODO(zcankara) change setting source to support multiple files + this.script = script; + let codeNodes = this.scriptNode.children; + for (let index = 1; index <= codeNodes.length; index++) { + if (index != line) continue; + let lineText = codeNodes[index - 1].innerHTML; + for (let char = 1; char <= lineText.length; char++) { + if (char != col) continue; + let target = char - 1; + codeNodes[line - 1].innerHTML = lineText.slice(0, target) + + " " + + lineText.slice(target, lineText.length); + } + } + } + createScriptNode() { + let scriptNode = document.createElement("pre"); + scriptNode.classList.add('scriptNode'); + return scriptNode; + } + renderSourcePanel(source) { + let scriptNode = this.createScriptNode(); + let sourceLines = source.split("\n"); + for (let line = 1; line <= sourceLines.length; line++) { + let codeNode = document.createElement("code"); + codeNode.classList.add("line" + line); + codeNode.innerHTML = sourceLines[line - 1] + "\n"; + scriptNode.appendChild(codeNode); + } + let oldScriptNode = this.script.childNodes[1]; + this.script.replaceChild(scriptNode, oldScriptNode); + } + renderSourcePanelSelectedHighlight() { + for (const sourcePosition of this.selectedSourcePositions) { + let line = sourcePosition.line; + let col = sourcePosition.col; + let script = sourcePosition.script; + this.highlightSourcePosition(line, col, script); + } + } + } +);