[tools] Add markers to system-analyzer source panel

- Create SourcePosition objects for Map and IC log entries
- Display source code with markers for SourcePositions
- Avoid some try-catches for a better debugging experience

Bug: v8:10644
Change-Id: I559b0eaeaa1442986a00d2ef720d19ba85178509
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2424258
Reviewed-by: Sathya Gunasekaran  <gsathya@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Auto-Submit: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#70091}
This commit is contained in:
Camillo Bruni 2020-09-23 12:30:17 +02:00 committed by Commit Bot
parent 607414e91c
commit 89e0d45c66
9 changed files with 275 additions and 141 deletions

View File

@ -25,6 +25,52 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// TODO: move to separate modules
class SourcePosition {
constructor(script, line, column) {
this.script = script;
this.line = line;
this.column = column;
this.entries = [];
}
addEntry(entry) {
this.entries.push(entry);
}
}
class Script {
constructor(id, name, source) {
this.id = id;
this.name = name;
this.source = source;
this.sourcePositions = [];
// Map<line, Map<column, SourcePosition>>
this.lineToColumn = new Map();
}
addSourcePosition(line, column, entry) {
let sourcePosition = this.lineToColumn.get(line)?.get(column);
if (sourcePosition === undefined) {
sourcePosition = new SourcePosition(this, line, column, )
this.#addSourcePosition(line, column, sourcePosition);
}
sourcePosition.addEntry(entry);
return sourcePosition;
}
#addSourcePosition(line, column, sourcePosition) {
let columnToSourcePosition;
if (this.lineToColumn.has(line)) {
columnToSourcePosition = this.lineToColumn.get(line);
} else {
columnToSourcePosition = new Map();
this.lineToColumn.set(line, columnToSourcePosition);
}
this.sourcePositions.push(sourcePosition);
columnToSourcePosition.set(column, sourcePosition);
}
}
/**
* Creates a profile object for processing profiling-related events
@ -228,13 +274,10 @@ Profile.prototype.addSourcePositions = function (
/**
* Adds script source code.
*/
Profile.prototype.addScriptSource = function (scriptId, url, source) {
this.scripts_[scriptId] = {
scriptId: scriptId,
name: url,
source: source
};
this.urlToScript_.set(url, source);
Profile.prototype.addScriptSource = function (id, url, source) {
const script = new Script(id, url, source);
this.scripts_[id] = script;
this.urlToScript_.set(url, script);
};
@ -1033,12 +1076,8 @@ JsonProfile.prototype.addSourcePositions = function (
};
};
JsonProfile.prototype.addScriptSource = function (scriptId, url, source) {
this.scripts_[scriptId] = {
scriptId: scriptId,
name: url,
source: source
};
JsonProfile.prototype.addScriptSource = function (id, url, source) {
this.scripts_[id] = new Script(id, url, source);
};

View File

@ -4,7 +4,6 @@
import { Group } from './ic-model.mjs';
import Processor from "./processor.mjs";
import { SourcePositionLogEvent } from "./log/sourcePosition.mjs";
import { MapLogEvent } from "./log/map.mjs";
import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs';
import { defineCustomElement, V8CustomElement } from './helper.mjs';
@ -122,18 +121,7 @@ defineCustomElement('ic-panel', (templateText) =>
//TODO(zcankara) Handle in the processor for events with source positions.
handleFilePositionClick(e) {
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
this.dispatchEvent(new FocusEvent(entry.filePosition));
}
render(entries, parent) {

View File

@ -140,7 +140,9 @@ button {
color: var(--on-primary-color);
}
.clickable:hover,
.clickable:active {
.mark:hover,
.clickable:active,
.mark:active {
background-color: var(--primary-color);
color: var(--on-primary-color);
cursor: pointer;

View File

@ -4,7 +4,6 @@
import { SelectionEvent, FocusEvent, SelectTimeEvent } from "./events.mjs";
import { State } from "./app-model.mjs";
import { SourcePositionLogEvent } from "./log/sourcePosition.mjs";
import { MapLogEvent } from "./log/map.mjs";
import { IcLogEvent } from "./log/ic.mjs";
import Processor from "./processor.mjs";
@ -55,10 +54,10 @@ class App {
this.showMapEntries(e.entries);
} else if (e.entries[0] instanceof IcLogEvent) {
this.showIcEntries(e.entries);
} else if (e.entries[0] instanceof SourcePositionLogEvent) {
} else if (e.entries[0] instanceof SourcePosition) {
this.showSourcePositionEntries(e.entries);
} else {
console.error("Undefined selection!");
throw new Error("Unknown selection type!");
}
}
showMapEntries(entries) {
@ -82,10 +81,10 @@ class App {
this.selectMapLogEvent(e.entry);
} else if (e.entry instanceof IcLogEvent) {
this.selectICLogEvent(e.entry);
} else if (e.entry instanceof SourcePositionLogEvent) {
} else if (e.entry instanceof SourcePosition) {
this.selectSourcePositionEvent(e.entry);
} else {
console.log("undefined");
throw new Error("Unknown selection type!");
}
}
selectTimeRange(start, end) {
@ -108,7 +107,6 @@ class App {
}
selectSourcePositionEvent(sourcePositions) {
if (!sourcePositions.script) return;
console.log("source positions: ", sourcePositions);
this.#view.sourcePanel.selectedSourcePositions = [sourcePositions];
}
@ -133,26 +131,22 @@ class App {
$("#container").className = "loaded";
// instantiate the app logic
let fileData = e.detail;
try {
const processor = this.handleLoadTextProcessor(fileData.chunk);
const mapTimeline = processor.mapTimeline;
const icTimeline = processor.icTimeline;
//TODO(zcankara) Make sure only one instance of src event map ic id match
// Load map log events timeline.
this.#state.mapTimeline = mapTimeline;
// Transitions must be set before timeline for stats panel.
this.#view.mapPanel.transitions = this.#state.mapTimeline.transitions;
this.#view.mapTrack.data = mapTimeline;
this.#state.chunks = this.#view.mapTrack.chunks;
this.#view.mapPanel.timeline = mapTimeline;
// Load ic log events timeline.
this.#state.icTimeline = icTimeline;
this.#view.icPanel.timeline = icTimeline;
this.#view.icTrack.data = icTimeline;
// TODO(zcankara) Load source position log events timeline.
} catch (error) {
console.log(error);
}
const processor = this.handleLoadTextProcessor(fileData.chunk);
const mapTimeline = processor.mapTimeline;
const icTimeline = processor.icTimeline;
//TODO(zcankara) Make sure only one instance of src event map ic id match
// Load map log events timeline.
this.#state.mapTimeline = mapTimeline;
// Transitions must be set before timeline for stats panel.
this.#view.mapPanel.transitions = this.#state.mapTimeline.transitions;
this.#view.mapTrack.data = mapTimeline;
this.#state.chunks = this.#view.mapTrack.chunks;
this.#view.mapPanel.timeline = mapTimeline;
// Load ic log events timeline.
this.#state.icTimeline = icTimeline;
this.#view.icPanel.timeline = icTimeline;
this.#view.icTrack.data = icTimeline;
this.#view.sourcePanel.data = processor.scripts
this.fileLoaded = true;
}

View File

@ -1,16 +0,0 @@
// 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 { Event } from './log.mjs';
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 { SourcePositionLogEvent };

View File

@ -3,7 +3,6 @@
// found in the LICENSE file.
import { V8CustomElement, defineCustomElement } from "../helper.mjs";
import { FocusEvent } from "../events.mjs";
import { SourcePositionLogEvent } from "../log/sourcePosition.mjs";
defineCustomElement(
"./map-panel/map-details",
@ -43,21 +42,7 @@ defineCustomElement(
}
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
this.dispatchEvent(new FocusEvent(this.selectedMap.sourcePosition));
}
}
);

View File

@ -52,7 +52,7 @@ class Processor extends LogReader {
'map': {
parsers: [
parseString, parseInt, parseString, parseString, parseInt, parseInt,
parseString, parseString, parseString
parseInt, parseString, parseString
],
processor: this.processMap
},
@ -154,10 +154,6 @@ class Processor extends LogReader {
});
}
addEntry(entry) {
this.entries.push(entry);
}
/**
* Parser for dynamic code optimization state.
*/
@ -227,10 +223,13 @@ class Processor extends LogReader {
let parts = fnName.split(' ');
let fileName = parts[1];
let script = this.getScript(fileName);
// TODO: Use SourcePosition here directly
let entry = new IcLogEvent(
type, fnName, time, line, column, key, old_state, new_state, map,
slow_reason, script);
//TODO(zcankara) Process sourcePosition
if (script) {
entry.sourcePosition = script.addSourcePosition(line, column, entry);
}
this.#icTimeline.push(entry);
}
@ -266,11 +265,14 @@ class Processor extends LogReader {
if (type === 'Deprecate') return this.deprecateMap(type, time_, from);
let from_ = this.getExistingMap(from, time_);
let to_ = this.getExistingMap(to, time_);
//TODO(zcankara) Process sourcePosition
// TODO: use SourcePosition directly.
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);
if (to_.script) {
to_.sourcePosition = to_.script.addSourcePosition(line, column, to_)
}
edge.finishSetup();
}
@ -292,7 +294,6 @@ class Processor extends LogReader {
createMap(id, time) {
let map = new MapLogEvent(id, time);
//TODO(zcankara) Process sourcePosition
this.#mapTimeline.push(map);
return map;
}
@ -309,7 +310,12 @@ class Processor extends LogReader {
}
getScript(url) {
return this.#profile.getScript(url);
const script = this.#profile.getScript(url);
// TODO create placeholder script for empty urls.
if (script === undefined) {
console.error(`Could not find script for url: '${url}'`)
}
return script;
}
get icTimeline() {
@ -320,7 +326,9 @@ class Processor extends LogReader {
return this.#mapTimeline;
}
get scripts() {
return this.#profile.scripts_.filter(script => script !== undefined);
}
}
Processor.kProperties = [

View File

@ -2,32 +2,50 @@
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
@import "./index.css";
pre.scriptNode {
white-space: pre-wrap;
}
pre.scriptNode:before {
counter-reset: listing;
counter-reset: sourceLineCounter;
}
pre.scriptNode code {
counter-increment: listing;
pre.scriptNode span {
counter-increment: sourceLineCounter;
}
pre.scriptNode code::before {
content: counter(listing) ". ";
pre.scriptNode span::before {
content: counter(sourceLineCounter) " ";
display: inline-block;
width: 4em;
padding-left: auto;
margin-left: auto;
text-align: left;
text-align: right;
}
mark {
width: 1ch;
height: 1lh;
border-radius: 0.1lh;
border: 0.5px var(--background-color) solid;
cursor: pointer;
}
.marked {
background-color: var(--primary-color);
color: var(--on-primary-color);
}
</style>
<div class="panel">
<h2>Source Panel</h2>
<div class="script-dropdown">
<label for="scripts-label">Scripts:</label>
<select id="script-dropdown"></select>
</div>
<div id="script">
<pre class="scripNode"></pre>
</div>

View File

@ -2,14 +2,21 @@
// 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";
import { SelectionEvent, FocusEvent } from "./events.mjs";
import { MapLogEvent } from "./log/map.mjs";
import { IcLogEvent } from "./log/ic.mjs";
defineCustomElement(
"source-panel",
(templateText) =>
class SourcePanel extends V8CustomElement {
#selectedSourcePositions;
#scripts = [];
#script;
constructor() {
super(templateText);
this.scriptDropdown.addEventListener(
'change', e => this.handleSelectScript(e));
}
get script() {
return this.$('#script');
@ -18,56 +25,165 @@ defineCustomElement(
return this.$('.scriptNode');
}
set script(script) {
this.renderSourcePanel(script);
this.#script = script;
this.renderSourcePanel();
}
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) +
"<span class='highlight'> </span>" +
lineText.slice(target, lineText.length);
}
set data(value) {
this.#scripts = value;
this.initializeScriptDropdown();
this.script = this.#scripts[0];
}
get scriptDropdown() {
return this.$("#script-dropdown");
}
initializeScriptDropdown() {
this.#scripts.sort((a, b) => a.name.localeCompare(b.name));
let select = this.scriptDropdown;
select.options.length = 0;
for (const script of this.#scripts) {
const option = document.createElement("option");
option.text = `${script.name} (id=${script.id})`;
option.script = script;
select.add(option);
}
}
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];
renderSourcePanel() {
const builder = new LineBuilder(this, this.#script);
const scriptNode = builder.createScriptNode();
const 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;
if (!(line && col && script)) continue;
this.highlightSourcePosition(line, col, script);
handleSelectScript(e) {
const option = this.scriptDropdown.options[this.scriptDropdown.selectedIndex];
this.script = option.script;
}
handleSourcePositionClick(e) {
let icLogEvents = [];
let mapLogEvents = [];
for (const entry of e.target.sourcePosition.entries) {
if (entry instanceof MapLogEvent) {
mapLogEvents.push(entry);
} else if (entry instanceof IcLogEvent) {
icLogEvents.push(entry);
}
}
if (icLogEvents.length > 0 ) {
this.dispatchEvent(new SelectionEvent(icLogEvents));
this.dispatchEvent(new FocusEvent(icLogEvents[0]));
}
if (mapLogEvents.length > 0) {
this.dispatchEvent(new SelectionEvent(mapLogEvents));
this.dispatchEvent(new FocusEvent(mapLogEvents[0]));
}
}
}
);
class SourcePositionIterator {
#entries;
#index = 0;
constructor(sourcePositions) {
this.#entries = sourcePositions;
}
*forLine(lineIndex) {
while(!this.#done() && this.#current().line === lineIndex) {
yield this.#current();
this.#next();
}
}
#current() {
return this.#entries[this.#index];
}
#done() {
return this.#index + 1 >= this.#entries.length;
}
#next() {
this.#index++;
}
}
function * lineIterator(source) {
let current = 0;
let line = 1;
while(current < source.length) {
const next = source.indexOf("\n", current);
if (next === -1) break;
yield [line, source.substring(current, next)];
line++;
current = next + 1;
}
if (current < source.length) yield [line, source.substring(current)];
}
class LineBuilder {
#script
#clickHandler
#sourcePositions
constructor(panel, script) {
this.#script = script;
this.#clickHandler = panel.handleSourcePositionClick.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);
}
createScriptNode() {
const scriptNode = document.createElement("pre");
scriptNode.classList.add('scriptNode');
for (let [lineIndex, line] of lineIterator(this.#script.source)) {
scriptNode.appendChild(this.#createLineNode(lineIndex, line));
}
return scriptNode;
}
#createLineNode(lineIndex, line) {
const lineNode = document.createElement("span");
let columnIndex = 0;
for (const sourcePosition of this.#sourcePositions.forLine(lineIndex)) {
const nextColumnIndex = sourcePosition.column - 1;
lineNode.appendChild(
document.createTextNode(
line.substring(columnIndex, nextColumnIndex)));
columnIndex = nextColumnIndex;
lineNode.appendChild(
this.#createMarkerNode(line[columnIndex], sourcePosition));
columnIndex++;
}
lineNode.appendChild(
document.createTextNode(line.substring(columnIndex) + "\n"));
return lineNode;
}
#createMarkerNode(text, sourcePosition) {
const marker = document.createElement("mark");
marker.classList.add('marked');
marker.textContent = text;
marker.sourcePosition = sourcePosition;
marker.onclick = this.#clickHandler;
return marker;
}
}