Commit b9cb78a7 authored by Bret Sepulveda's avatar Bret Sepulveda Committed by Commit Bot

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: 's avatarCamillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#55542}
parent 33f2012e
...@@ -1323,7 +1323,7 @@ void Logger::CodeCreateEvent(CodeEventListener::LogEventsAndTags tag, ...@@ -1323,7 +1323,7 @@ void Logger::CodeCreateEvent(CodeEventListener::LogEventsAndTags tag,
// <script-offset> is the position within the script // <script-offset> is the position within the script
// <inlining-id> is the offset in the <inlining> table // <inlining-id> is the offset in the <inlining> table
// <inlining> table is a sequence of strings of the form // <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 // where
// <function-id> is an index into the <fns> function table // <function-id> is an index into the <fns> function table
// <fns> is the function table encoded as a sequence of strings // <fns> is the function table encoded as a sequence of strings
...@@ -1335,12 +1335,8 @@ void Logger::CodeCreateEvent(CodeEventListener::LogEventsAndTags tag, ...@@ -1335,12 +1335,8 @@ void Logger::CodeCreateEvent(CodeEventListener::LogEventsAndTags tag,
<< shared->EndPosition() << kNext; << shared->EndPosition() << kNext;
SourcePositionTableIterator iterator(code->source_position_table()); SourcePositionTableIterator iterator(code->source_position_table());
bool is_first = true;
bool hasInlined = false; bool hasInlined = false;
for (; !iterator.done(); iterator.Advance()) { for (; !iterator.done(); iterator.Advance()) {
if (is_first) {
is_first = false;
}
SourcePosition pos = iterator.source_position(); SourcePosition pos = iterator.source_position();
msg << "C" << iterator.code_offset() << "O" << pos.ScriptOffset(); msg << "C" << iterator.code_offset() << "O" << pos.ScriptOffset();
if (pos.isInlined()) { if (pos.isInlined()) {
...@@ -1604,7 +1600,7 @@ bool Logger::EnsureLogScriptSource(Script* script) { ...@@ -1604,7 +1600,7 @@ bool Logger::EnsureLogScriptSource(Script* script) {
// Make sure the script is written to the log file. // Make sure the script is written to the log file.
int script_id = script->id(); int script_id = script->id();
if (logged_source_code_.find(script_id) != logged_source_code_.end()) { if (logged_source_code_.find(script_id) != logged_source_code_.end()) {
return false; return true;
} }
// This script has not been logged yet. // This script has not been logged yet.
logged_source_code_.insert(script_id); logged_source_code_.insert(script_id);
......
...@@ -975,7 +975,7 @@ JsonProfile.prototype.addSourcePositions = function( ...@@ -975,7 +975,7 @@ JsonProfile.prototype.addSourcePositions = function(
if (!entry) return; if (!entry) return;
var codeId = entry.codeId; var codeId = entry.codeId;
// Resolve the inlined fucntions list. // Resolve the inlined functions list.
if (inlinedFunctions.length > 0) { if (inlinedFunctions.length > 0) {
inlinedFunctions = inlinedFunctions.substring(1).split("S"); inlinedFunctions = inlinedFunctions.substring(1).split("S");
for (var i = 0; i < inlinedFunctions.length; i++) { for (var i = 0; i < inlinedFunctions.length; i++) {
......
...@@ -22,7 +22,7 @@ found in the LICENSE file. --> ...@@ -22,7 +22,7 @@ found in the LICENSE file. -->
Chrome V8 profiling log processor Chrome V8 profiling log processor
</h3> </h3>
<input type="file" id="fileinput" /> <input type="file" id="fileinput" /><div id="source-status"></div>
<br> <br>
<hr> <hr>
...@@ -59,6 +59,10 @@ found in the LICENSE file. --> ...@@ -59,6 +59,10 @@ found in the LICENSE file. -->
</table> </table>
<div> <div>
Current code object: <span id="timeline-currentCode"></span> 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>
</div> </div>
......
...@@ -93,9 +93,10 @@ function codeEquals(code1, code2, allowDifferentKinds = false) { ...@@ -93,9 +93,10 @@ function codeEquals(code1, code2, allowDifferentKinds = false) {
function createNodeFromStackEntry(code, codeId, vmState) { function createNodeFromStackEntry(code, codeId, vmState) {
let name = code ? code.name : "UNKNOWN"; let name = code ? code.name : "UNKNOWN";
let node = createEmptyNode(name);
return { name, codeId, type : resolveCodeKindAndVmState(code, vmState), node.codeId = codeId;
children : [], ownTicks : 0, ticks : 0 }; node.type = resolveCodeKindAndVmState(code, vmState);
return node;
} }
function childIdFromCode(codeId, code) { function childIdFromCode(codeId, code) {
...@@ -148,29 +149,30 @@ function findNextFrame(file, stack, stackPos, step, filter) { ...@@ -148,29 +149,30 @@ function findNextFrame(file, stack, stackPos, step, filter) {
} }
function addOrUpdateChildNode(parent, file, stackIndex, stackPos, ascending) { 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) { if (stackPos === -1) {
// We reached the end without finding the next step. // We reached the end without finding the next step.
// If we are doing top-down call tree, update own ticks. // If we are doing top-down call tree, update own ticks.
if (!ascending) { if (!ascending) {
parent.ownTicks++; parent.ownTicks++;
} }
} else { return;
}
let stack = file.ticks[stackIndex].s;
console.assert(stackPos >= 0 && stackPos < stack.length); console.assert(stackPos >= 0 && stackPos < stack.length);
let codeId = stack[stackPos];
let code = codeId >= 0 ? file.code[codeId] : undefined;
// We found a child node. // We found a child node.
let childId = childIdFromCode(codeId, code); let childId = childIdFromCode(codeId, code);
let child = parent.children[childId]; let child = parent.children[childId];
if (!child) { if (!child) {
let vmState = file.ticks[stackIndex].vm;
child = createNodeFromStackEntry(code, codeId, vmState); child = createNodeFromStackEntry(code, codeId, vmState);
child.delayedExpansion = { frameList : [], ascending }; child.delayedExpansion = { frameList : [], ascending };
parent.children[childId] = child; parent.children[childId] = child;
} }
child.ticks++; child.ticks++;
addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos); addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos);
}
} }
// This expands a tree node (direct children only). // This expands a tree node (direct children only).
...@@ -314,13 +316,7 @@ class FunctionListTree { ...@@ -314,13 +316,7 @@ class FunctionListTree {
this.tree = root; this.tree = root;
this.categories = categories; this.categories = categories;
} else { } else {
this.tree = { this.tree = createEmptyNode("root");
name : "root",
codeId: -1,
children : [],
ownTicks : 0,
ticks : 0
};
this.categories = null; this.categories = null;
} }
...@@ -339,7 +335,7 @@ class FunctionListTree { ...@@ -339,7 +335,7 @@ class FunctionListTree {
let codeId = stack[i]; let codeId = stack[i];
if (codeId < 0 || this.codeVisited[codeId]) continue; if (codeId < 0 || this.codeVisited[codeId]) continue;
let code = codeId >= 0 ? file.code[codeId] : undefined; let code = file.code[codeId];
if (this.filter) { if (this.filter) {
let type = code ? code.type : undefined; let type = code ? code.type : undefined;
let kind = code ? code.kind : undefined; let kind = code ? code.kind : undefined;
...@@ -601,3 +597,15 @@ function computeOptimizationStats(file, ...@@ -601,3 +597,15 @@ function computeOptimizationStats(file,
softDeoptimizations, 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);
}
}
...@@ -19,6 +19,10 @@ body { ...@@ -19,6 +19,10 @@ body {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
} }
#source-status {
display: inline-block;
}
.tree-row-arrow { .tree-row-arrow {
margin-right: 0.2em; margin-right: 0.2em;
text-align: right; text-align: right;
...@@ -35,6 +39,7 @@ body { ...@@ -35,6 +39,7 @@ body {
.tree-row-name { .tree-row-name {
margin-left: 0.2em; margin-left: 0.2em;
margin-right: 0.2em;
} }
.codeid-link { .codeid-link {
...@@ -42,6 +47,54 @@ body { ...@@ -42,6 +47,54 @@ body {
cursor: pointer; 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 { div.mode-button {
padding: 1em 3em; padding: 1em 3em;
display: inline-block; display: inline-block;
......
...@@ -8,6 +8,12 @@ function $(id) { ...@@ -8,6 +8,12 @@ function $(id) {
return document.getElementById(id); return document.getElementById(id);
} }
function removeAllChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
let components; let components;
function createViews() { function createViews() {
components = [ components = [
...@@ -16,6 +22,7 @@ function createViews() { ...@@ -16,6 +22,7 @@ function createViews() {
new HelpView(), new HelpView(),
new SummaryView(), new SummaryView(),
new ModeBarView(), new ModeBarView(),
new ScriptSourceView(),
]; ];
} }
...@@ -24,6 +31,7 @@ function emptyState() { ...@@ -24,6 +31,7 @@ function emptyState() {
file : null, file : null,
mode : null, mode : null,
currentCodeId : null, currentCodeId : null,
viewingSource: false,
start : 0, start : 0,
end : Infinity, end : Infinity,
timelineSize : { timelineSize : {
...@@ -34,7 +42,8 @@ function emptyState() { ...@@ -34,7 +42,8 @@ function emptyState() {
attribution : "js-exclude-bc", attribution : "js-exclude-bc",
categories : "code-type", categories : "code-type",
sort : "time" sort : "time"
} },
sourceData: null
}; };
} }
...@@ -119,11 +128,27 @@ let main = { ...@@ -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) { setFile(file) {
if (file !== main.currentState.file) { if (file !== main.currentState.file) {
let lastMode = main.currentState.mode || "summary"; let lastMode = main.currentState.mode || "summary";
main.currentState = emptyState(); main.currentState = emptyState();
main.currentState.file = file; main.currentState.file = file;
main.updateSources(file);
main.setMode(lastMode); main.setMode(lastMode);
main.delayRender(); main.delayRender();
} }
...@@ -137,6 +162,14 @@ let main = { ...@@ -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() { onResize() {
main.delayRender(); main.delayRender();
}, },
...@@ -328,6 +361,20 @@ function createFunctionNode(name, codeId) { ...@@ -328,6 +361,20 @@ function createFunctionNode(name, codeId) {
return nameElement; 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 COLLAPSED_ARROW = "\u25B6";
const EXPANDED_ARROW = "\u25BC"; const EXPANDED_ARROW = "\u25BC";
...@@ -448,6 +495,10 @@ class CallTreeView { ...@@ -448,6 +495,10 @@ class CallTreeView {
nameCell.appendChild(arrow); nameCell.appendChild(arrow);
nameCell.appendChild(createTypeNode(node.type)); nameCell.appendChild(createTypeNode(node.type));
nameCell.appendChild(createFunctionNode(node.name, node.codeId)); 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. // Inclusive ticks cell.
c = row.insertCell(); c = row.insertCell();
...@@ -793,8 +844,8 @@ class TimelineView { ...@@ -793,8 +844,8 @@ class TimelineView {
return; return;
} }
let width = Math.round(window.innerWidth - 20); let width = Math.round(document.documentElement.clientWidth - 20);
let height = Math.round(window.innerHeight / 5); let height = Math.round(document.documentElement.clientHeight / 5);
if (oldState) { if (oldState) {
if (width === oldState.timelineSize.width && if (width === oldState.timelineSize.width &&
...@@ -1010,9 +1061,7 @@ class TimelineView { ...@@ -1010,9 +1061,7 @@ class TimelineView {
cell.appendChild(document.createTextNode(" " + desc.text)); cell.appendChild(document.createTextNode(" " + desc.text));
} }
while (this.currentCode.firstChild) { removeAllChildren(this.currentCode);
this.currentCode.removeChild(this.currentCode.firstChild);
}
if (currentCodeId) { if (currentCodeId) {
let currentCode = file.code[currentCodeId]; let currentCode = file.code[currentCodeId];
this.currentCode.appendChild(document.createTextNode(currentCode.name)); this.currentCode.appendChild(document.createTextNode(currentCode.name));
...@@ -1083,10 +1132,7 @@ class SummaryView { ...@@ -1083,10 +1132,7 @@ class SummaryView {
} }
this.element.style.display = "inherit"; this.element.style.display = "inherit";
removeAllChildren(this.element);
while (this.element.firstChild) {
this.element.removeChild(this.element.firstChild);
}
let stats = computeOptimizationStats( let stats = computeOptimizationStats(
this.currentState.file, newState.start, newState.end); this.currentState.file, newState.start, newState.end);
...@@ -1237,6 +1283,217 @@ class SummaryView { ...@@ -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 { class HelpView {
constructor() { constructor() {
this.element = $("help"); this.element = $("help");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment