[tools][system-analyzer] Add Source Code Panel

This CL adds a source code panel to display source code positions of
Map/IC log events.

* Clicking file positions on the Ic Panel emits FocusEvent with
SourcePositionLogEvent as entry to highlight code related with the
selected icLogEvent.

* Clicking map details on the Map Panel emits FocusEvent with
SourcePositionLogEvent as entry to highlight code related with the
selected mapLogEvent.

Bug: v8:10644
Change-Id: Icaf3e9e3f7fae485c50ad685f9ec5dc8ac28b3dc
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2358734
Commit-Queue: Zeynep Cankara <zcankara@google.com>
Reviewed-by: Sathya Gunasekaran  <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#69610}
This commit is contained in:
Zeynep Cankara 2020-08-28 15:41:56 +01:00 committed by Commit Bot
parent 1512f89328
commit 0f6afbe125
13 changed files with 257 additions and 34 deletions

View File

@ -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
};

View File

@ -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;
}

View File

@ -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 };

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);

View File

@ -115,9 +115,9 @@ found in the LICENSE file. -->
<script type="module">
import { App } from './index.mjs';
globalThis.app =
new App("#log-file-reader", "#map-panel", "#timeline-panel",
"#ic-panel", "#map-track", "#ic-track");
globalThis.app = new App("#log-file-reader", "#map-panel",
"#timeline-panel", "#ic-panel", "#map-track", "#ic-track",
"#source-panel");
</script>
</head>
@ -141,6 +141,7 @@ found in the LICENSE file. -->
</timeline-panel>
<map-panel id="map-panel"></map-panel>
<ic-panel id="ic-panel" onchange="app.handleSelectIc(event)"></ic-panel>
<source-panel id="source-panel"></source-panel>
</div>
</div>
<div id="instructions">

View File

@ -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";

View File

@ -6,7 +6,8 @@ found in the LICENSE file. -->
<link href="./index.css" rel="stylesheet">
</head>
<style>
#mapDetails {
#mapDetails,
#filePositionNode {
overflow-x: scroll;
}
@ -17,5 +18,6 @@ found in the LICENSE file. -->
</style>
<div class="panel">
<h4>Map Details</h4>
<section id="filePositionNode"></section>
<section id="mapDetails"></section>
</div>

View File

@ -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
}
}
);

View File

@ -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');

View File

@ -0,0 +1,34 @@
<!-- 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. -->
<style>
@import "./index.css";
pre.scriptNode {
white-space: pre-wrap;
}
pre.scriptNode:before {
counter-reset: listing;
}
pre.scriptNode code {
counter-increment: listing;
}
pre.scriptNode code::before {
content: counter(listing) ". ";
display: inline-block;
width: 4em;
padding-left: auto;
margin-left: auto;
text-align: left;
}
</style>
<div class="panel">
<h2>Source Panel</h2>
<div id="script">
<pre class="scripNode"></pre>
</div>
</div>

View File

@ -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) +
"<span class='highlight'> </span>" +
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);
}
}
}
);