profview: View source code of functions with samples inline.

If profiling is done with --log-source-code profview will now display
a "View source" link for each function in the tree view. Clicking this
will show a new source viewer, with sampled lines highlighted. See the
associated bug for screenshots.

This patch also fixes a bug in the profiler where the source info of
only the first code object for each function would be logged, and
includes some refactoring.

Bug: v8:6240
Change-Id: Ib96a9cfc54543d0dc9bef4657cdeb96ce28b223c
Reviewed-on: https://chromium-review.googlesource.com/1194231
Commit-Queue: Bret Sepulveda <bsep@chromium.org>
Reviewed-by: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#55542}
This commit is contained in:
Bret Sepulveda 2018-08-31 11:50:07 +02:00 committed by Commit Bot
parent 33f2012efd
commit b9cb78a705
6 changed files with 363 additions and 45 deletions

View File

@ -1323,7 +1323,7 @@ void Logger::CodeCreateEvent(CodeEventListener::LogEventsAndTags tag,
// <script-offset> is the position within the script
// <inlining-id> is the offset in the <inlining> table
// <inlining> table is a sequence of strings of the form
// F<function-id>O<script-offset>[I<inlining-id>
// F<function-id>O<script-offset>[I<inlining-id>]
// where
// <function-id> is an index into the <fns> function table
// <fns> is the function table encoded as a sequence of strings
@ -1335,12 +1335,8 @@ void Logger::CodeCreateEvent(CodeEventListener::LogEventsAndTags tag,
<< shared->EndPosition() << kNext;
SourcePositionTableIterator iterator(code->source_position_table());
bool is_first = true;
bool hasInlined = false;
for (; !iterator.done(); iterator.Advance()) {
if (is_first) {
is_first = false;
}
SourcePosition pos = iterator.source_position();
msg << "C" << iterator.code_offset() << "O" << pos.ScriptOffset();
if (pos.isInlined()) {
@ -1604,7 +1600,7 @@ bool Logger::EnsureLogScriptSource(Script* script) {
// Make sure the script is written to the log file.
int script_id = script->id();
if (logged_source_code_.find(script_id) != logged_source_code_.end()) {
return false;
return true;
}
// This script has not been logged yet.
logged_source_code_.insert(script_id);

View File

@ -975,7 +975,7 @@ JsonProfile.prototype.addSourcePositions = function(
if (!entry) return;
var codeId = entry.codeId;
// Resolve the inlined fucntions list.
// Resolve the inlined functions list.
if (inlinedFunctions.length > 0) {
inlinedFunctions = inlinedFunctions.substring(1).split("S");
for (var i = 0; i < inlinedFunctions.length; i++) {

View File

@ -22,7 +22,7 @@ found in the LICENSE file. -->
Chrome V8 profiling log processor
</h3>
<input type="file" id="fileinput" />
<input type="file" id="fileinput" /><div id="source-status"></div>
<br>
<hr>
@ -59,6 +59,10 @@ found in the LICENSE file. -->
</table>
<div>
Current code object: <span id="timeline-currentCode"></span>
<button id="source-viewer-hide-button">Hide source</button>
</div>
<div>
<table id="source-viewer"> </table>
</div>
</div>

View File

@ -93,9 +93,10 @@ function codeEquals(code1, code2, allowDifferentKinds = false) {
function createNodeFromStackEntry(code, codeId, vmState) {
let name = code ? code.name : "UNKNOWN";
return { name, codeId, type : resolveCodeKindAndVmState(code, vmState),
children : [], ownTicks : 0, ticks : 0 };
let node = createEmptyNode(name);
node.codeId = codeId;
node.type = resolveCodeKindAndVmState(code, vmState);
return node;
}
function childIdFromCode(codeId, code) {
@ -148,29 +149,30 @@ function findNextFrame(file, stack, stackPos, step, filter) {
}
function addOrUpdateChildNode(parent, file, stackIndex, stackPos, ascending) {
let stack = file.ticks[stackIndex].s;
let vmState = file.ticks[stackIndex].vm;
let codeId = stack[stackPos];
let code = codeId >= 0 ? file.code[codeId] : undefined;
if (stackPos === -1) {
// We reached the end without finding the next step.
// If we are doing top-down call tree, update own ticks.
if (!ascending) {
parent.ownTicks++;
}
} else {
console.assert(stackPos >= 0 && stackPos < stack.length);
// We found a child node.
let childId = childIdFromCode(codeId, code);
let child = parent.children[childId];
if (!child) {
child = createNodeFromStackEntry(code, codeId, vmState);
child.delayedExpansion = { frameList : [], ascending };
parent.children[childId] = child;
}
child.ticks++;
addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos);
return;
}
let stack = file.ticks[stackIndex].s;
console.assert(stackPos >= 0 && stackPos < stack.length);
let codeId = stack[stackPos];
let code = codeId >= 0 ? file.code[codeId] : undefined;
// We found a child node.
let childId = childIdFromCode(codeId, code);
let child = parent.children[childId];
if (!child) {
let vmState = file.ticks[stackIndex].vm;
child = createNodeFromStackEntry(code, codeId, vmState);
child.delayedExpansion = { frameList : [], ascending };
parent.children[childId] = child;
}
child.ticks++;
addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos);
}
// This expands a tree node (direct children only).
@ -314,13 +316,7 @@ class FunctionListTree {
this.tree = root;
this.categories = categories;
} else {
this.tree = {
name : "root",
codeId: -1,
children : [],
ownTicks : 0,
ticks : 0
};
this.tree = createEmptyNode("root");
this.categories = null;
}
@ -339,7 +335,7 @@ class FunctionListTree {
let codeId = stack[i];
if (codeId < 0 || this.codeVisited[codeId]) continue;
let code = codeId >= 0 ? file.code[codeId] : undefined;
let code = file.code[codeId];
if (this.filter) {
let type = code ? code.type : undefined;
let kind = code ? code.kind : undefined;
@ -601,3 +597,15 @@ function computeOptimizationStats(file,
softDeoptimizations,
};
}
function normalizeLeadingWhitespace(lines) {
let regex = /^\s*/;
let minimumLeadingWhitespaceChars = Infinity;
for (let line of lines) {
minimumLeadingWhitespaceChars =
Math.min(minimumLeadingWhitespaceChars, regex.exec(line)[0].length);
}
for (let i = 0; i < lines.length; i++) {
lines[i] = lines[i].substring(minimumLeadingWhitespaceChars);
}
}

View File

@ -19,6 +19,10 @@ body {
font-family: 'Roboto', sans-serif;
}
#source-status {
display: inline-block;
}
.tree-row-arrow {
margin-right: 0.2em;
text-align: right;
@ -35,6 +39,7 @@ body {
.tree-row-name {
margin-left: 0.2em;
margin-right: 0.2em;
}
.codeid-link {
@ -42,6 +47,54 @@ body {
cursor: pointer;
}
.view-source-link {
text-decoration: underline;
cursor: pointer;
font-size: 10pt;
margin-left: 0.6em;
color: #555555;
}
#source-viewer {
border: 1px solid black;
padding: 0.2em;
font-family: 'Roboto Mono', monospace;
white-space: pre;
margin-top: 1em;
margin-bottom: 1em;
}
#source-viewer td.line-none {
background-color: white;
}
#source-viewer td.line-cold {
background-color: #e1f5fe;
}
#source-viewer td.line-mediumcold {
background-color: #b2ebf2;
}
#source-viewer td.line-mediumhot {
background-color: #c5e1a5;
}
#source-viewer td.line-hot {
background-color: #dce775;
}
#source-viewer td.line-superhot {
background-color: #ffee58;
}
#source-viewer .source-line-number {
padding-left: 0.2em;
padding-right: 0.2em;
color: #003c8f;
background-color: #eceff1;
}
div.mode-button {
padding: 1em 3em;
display: inline-block;

View File

@ -8,6 +8,12 @@ function $(id) {
return document.getElementById(id);
}
function removeAllChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
let components;
function createViews() {
components = [
@ -16,6 +22,7 @@ function createViews() {
new HelpView(),
new SummaryView(),
new ModeBarView(),
new ScriptSourceView(),
];
}
@ -24,6 +31,7 @@ function emptyState() {
file : null,
mode : null,
currentCodeId : null,
viewingSource: false,
start : 0,
end : Infinity,
timelineSize : {
@ -34,7 +42,8 @@ function emptyState() {
attribution : "js-exclude-bc",
categories : "code-type",
sort : "time"
}
},
sourceData: null
};
}
@ -119,11 +128,27 @@ let main = {
}
},
updateSources(file) {
let statusDiv = $("source-status");
if (!file) {
statusDiv.textContent = "";
return;
}
if (!file.scripts || file.scripts.length === 0) {
statusDiv.textContent =
"Script source not available. Run profiler with --log-source-code.";
return;
}
statusDiv.textContent = "Script source is available.";
main.currentState.sourceData = new SourceData(file);
},
setFile(file) {
if (file !== main.currentState.file) {
let lastMode = main.currentState.mode || "summary";
main.currentState = emptyState();
main.currentState.file = file;
main.updateSources(file);
main.setMode(lastMode);
main.delayRender();
}
@ -137,6 +162,14 @@ let main = {
}
},
setViewingSource(value) {
if (main.currentState.viewingSource !== value) {
main.currentState = Object.assign({}, main.currentState);
main.currentState.viewingSource = value;
main.delayRender();
}
},
onResize() {
main.delayRender();
},
@ -328,6 +361,20 @@ function createFunctionNode(name, codeId) {
return nameElement;
}
function createViewSourceNode(codeId) {
let linkElement = document.createElement("span");
linkElement.appendChild(document.createTextNode("View source"));
linkElement.classList.add("view-source-link");
linkElement.onclick = (event) => {
main.setCurrentCode(codeId);
main.setViewingSource(true);
// Prevent the click from bubbling to the row and causing it to
// collapse/expand.
event.stopPropagation();
};
return linkElement;
}
const COLLAPSED_ARROW = "\u25B6";
const EXPANDED_ARROW = "\u25BC";
@ -448,6 +495,10 @@ class CallTreeView {
nameCell.appendChild(arrow);
nameCell.appendChild(createTypeNode(node.type));
nameCell.appendChild(createFunctionNode(node.name, node.codeId));
if (main.currentState.sourceData &&
main.currentState.sourceData.hasSource(node.name)) {
nameCell.appendChild(createViewSourceNode(node.codeId));
}
// Inclusive ticks cell.
c = row.insertCell();
@ -793,8 +844,8 @@ class TimelineView {
return;
}
let width = Math.round(window.innerWidth - 20);
let height = Math.round(window.innerHeight / 5);
let width = Math.round(document.documentElement.clientWidth - 20);
let height = Math.round(document.documentElement.clientHeight / 5);
if (oldState) {
if (width === oldState.timelineSize.width &&
@ -1010,9 +1061,7 @@ class TimelineView {
cell.appendChild(document.createTextNode(" " + desc.text));
}
while (this.currentCode.firstChild) {
this.currentCode.removeChild(this.currentCode.firstChild);
}
removeAllChildren(this.currentCode);
if (currentCodeId) {
let currentCode = file.code[currentCodeId];
this.currentCode.appendChild(document.createTextNode(currentCode.name));
@ -1083,10 +1132,7 @@ class SummaryView {
}
this.element.style.display = "inherit";
while (this.element.firstChild) {
this.element.removeChild(this.element.firstChild);
}
removeAllChildren(this.element);
let stats = computeOptimizationStats(
this.currentState.file, newState.start, newState.end);
@ -1237,6 +1283,217 @@ class SummaryView {
}
}
class ScriptSourceView {
constructor() {
this.table = $("source-viewer");
this.hideButton = $("source-viewer-hide-button");
this.hideButton.onclick = () => {
main.setViewingSource(false);
};
}
render(newState) {
let oldState = this.currentState;
if (!newState.file || !newState.viewingSource) {
this.table.style.display = "none";
this.hideButton.style.display = "none";
this.currentState = null;
return;
}
if (oldState) {
if (newState.file === oldState.file &&
newState.currentCodeId === oldState.currentCodeId &&
newState.viewingSource === oldState.viewingSource) {
// No change, nothing to do.
return;
}
}
this.currentState = newState;
this.table.style.display = "inline-block";
this.hideButton.style.display = "inline";
removeAllChildren(this.table);
let functionName =
this.currentState.file.code[this.currentState.currentCodeId].name;
let sourceView =
this.currentState.sourceData.generateSourceView(functionName);
for (let i = 0; i < sourceView.source.length; i++) {
let sampleCount = sourceView.lineSampleCounts[i] || 0;
let sampleProportion = sourceView.samplesTotal > 0 ?
sampleCount / sourceView.samplesTotal : 0;
let heatBucket;
if (sampleProportion === 0) {
heatBucket = "line-none";
} else if (sampleProportion < 0.2) {
heatBucket = "line-cold";
} else if (sampleProportion < 0.4) {
heatBucket = "line-mediumcold";
} else if (sampleProportion < 0.6) {
heatBucket = "line-mediumhot";
} else if (sampleProportion < 0.8) {
heatBucket = "line-hot";
} else {
heatBucket = "line-superhot";
}
let row = this.table.insertRow(-1);
let lineNumberCell = row.insertCell(-1);
lineNumberCell.classList.add("source-line-number");
lineNumberCell.textContent = i + sourceView.firstLineNumber;
let sampleCountCell = row.insertCell(-1);
sampleCountCell.classList.add(heatBucket);
sampleCountCell.textContent = sampleCount;
let sourceLineCell = row.insertCell(-1);
sourceLineCell.classList.add(heatBucket);
sourceLineCell.textContent = sourceView.source[i];
}
$("timeline-currentCode").scrollIntoView();
}
}
class SourceData {
constructor(file) {
this.scripts = new Map();
for (let scriptBlock of file.scripts) {
if (scriptBlock === null) continue; // Array may be sparse.
let source = scriptBlock.source.split("\n");
this.scripts.set(scriptBlock.name, source);
}
this.functions = new Map();
for (let codeId = 0; codeId < file.code.length; ++codeId) {
let codeBlock = file.code[codeId];
if (codeBlock.source) {
let data = this.functions.get(codeBlock.name);
if (!data) {
data = new FunctionSourceData(codeBlock.source.start,
codeBlock.source.end);
this.functions.set(codeBlock.name, data);
}
data.addSourceBlock(codeId, codeBlock.source);
}
}
for (let tick of file.ticks) {
let stack = tick.s;
for (let i = 0; i < stack.length; i += 2) {
let codeId = stack[i];
if (codeId < 0) continue;
let name = file.code[codeId].name;
if (this.functions.has(name)) {
let codeOffset = stack[i + 1];
this.functions.get(name).addOffsetSample(codeId, codeOffset);
}
}
}
}
getScript(name) {
let nameAndSource = name.split(" ")
console.assert(nameAndSource.length >= 2);
let sourceAndLine = nameAndSource[1].split(":");
return this.scripts.get(sourceAndLine[0]);
}
getLineForScriptOffset(name, scriptOffset) {
let script = this.getScript(name);
let line = 0;
let charsConsumed = 0;
for (; line < script.length; ++line) {
charsConsumed += script[line].length + 1; // Add 1 for newline.
if (charsConsumed > scriptOffset) break;
}
return line;
}
hasSource(name) {
return this.functions.has(name);
}
generateSourceView(name) {
console.assert(this.hasSource(name));
let data = this.functions.get(name);
let firstLineNumber =
this.getLineForScriptOffset(name, data.startScriptOffset);
let lastLineNumber =
this.getLineForScriptOffset(name, data.endScriptOffset);
let script = this.getScript(name);
let lines = script.slice(firstLineNumber, lastLineNumber + 1);
normalizeLeadingWhitespace(lines);
let samplesTotal = 0;
let lineSampleCounts = [];
for (let [codeId, block] of data.codes) {
block.offsets.forEach((sampleCount, codeOffset) => {
let sourceOffset = block.positionTable.getScriptOffset(codeOffset);
let lineNumber =
this.getLineForScriptOffset(name, sourceOffset) - firstLineNumber;
samplesTotal += sampleCount;
lineSampleCounts[lineNumber] =
(lineSampleCounts[lineNumber] || 0) + sampleCount;
});
}
return {
source: lines,
lineSampleCounts: lineSampleCounts,
samplesTotal: samplesTotal,
firstLineNumber: firstLineNumber + 1 // Source code is 1-indexed.
};
}
}
class FunctionSourceData {
constructor(startScriptOffset, endScriptOffset) {
this.startScriptOffset = startScriptOffset;
this.endScriptOffset = endScriptOffset;
this.codes = new Map();
}
addSourceBlock(codeId, source) {
this.codes.set(codeId, {
positionTable: new SourcePositionTable(source.positions),
offsets: []
});
}
addOffsetSample(codeId, codeOffset) {
let codeIdOffsets = this.codes.get(codeId).offsets;
codeIdOffsets[codeOffset] = (codeIdOffsets[codeOffset] || 0) + 1;
}
}
class SourcePositionTable {
constructor(encodedTable) {
this.offsetTable = [];
let offsetPairRegex = /C([0-9]+)O([0-9]+)/g;
while (true) {
let regexResult = offsetPairRegex.exec(encodedTable);
if (!regexResult) break;
let codeOffset = parseInt(regexResult[1]);
let scriptOffset = parseInt(regexResult[2]);
if (isNaN(codeOffset) || isNaN(scriptOffset)) continue;
this.offsetTable.push(codeOffset, scriptOffset);
}
}
getScriptOffset(codeOffset) {
console.assert(codeOffset >= 0);
for (let i = this.offsetTable.length - 2; i >= 0; i -= 2) {
if (this.offsetTable[i] <= codeOffset) {
return this.offsetTable[i + 1];
}
}
return this.offsetTable[1];
}
}
class HelpView {
constructor() {
this.element = $("help");