[profiler] Graphical front-end for tick processor.
Improvements: - top-down call tree. - interactive restriction to time interval. Review-Url: https://codereview.chromium.org/2696903002 Cr-Commit-Position: refs/heads/master@{#43599}
This commit is contained in:
parent
885ec93327
commit
fcce4797bc
@ -88,12 +88,12 @@ ProfileTestDriver.prototype.enter = function(funcName) {
|
||||
// Stack looks like this: [pc, caller, ..., main].
|
||||
// Therefore, we are adding entries at the beginning.
|
||||
this.stack_.unshift(this.funcAddrs_[funcName]);
|
||||
this.profile.recordTick(this.stack_);
|
||||
this.profile.recordTick(0, 0, this.stack_);
|
||||
};
|
||||
|
||||
|
||||
ProfileTestDriver.prototype.stay = function() {
|
||||
this.profile.recordTick(this.stack_);
|
||||
this.profile.recordTick(0, 0, this.stack_);
|
||||
};
|
||||
|
||||
|
||||
|
@ -131,15 +131,14 @@
|
||||
// shell executable
|
||||
MacCppEntriesProvider.prototype.loadSymbols = function(libName) {
|
||||
this.symbols = [[
|
||||
' U operator delete[]',
|
||||
'00001000 A __mh_execute_header',
|
||||
'00001b00 T start',
|
||||
'00001b40 t dyld_stub_binding_helper',
|
||||
'0011b710 T v8::internal::RegExpMacroAssembler::CheckPosition',
|
||||
'00134250 t v8::internal::Runtime_StringReplaceRegExpWithString',
|
||||
'00137220 T v8::internal::Runtime::GetElementOrCharAt',
|
||||
'00137400 t v8::internal::Runtime_DebugGetPropertyDetails',
|
||||
'001c1a80 b _private_mem\n'
|
||||
' operator delete[]',
|
||||
'00001000 __mh_execute_header',
|
||||
'00001b00 start',
|
||||
'00001b40 dyld_stub_binding_helper',
|
||||
'0011b710 v8::internal::RegExpMacroAssembler::CheckPosition',
|
||||
'00134250 v8::internal::Runtime_StringReplaceRegExpWithString',
|
||||
'00137220 v8::internal::Runtime::GetElementOrCharAt',
|
||||
'00137400 v8::internal::Runtime_DebugGetPropertyDetails\n'
|
||||
].join('\n'), ''];
|
||||
};
|
||||
|
||||
@ -161,10 +160,10 @@
|
||||
// stdc++ library
|
||||
MacCppEntriesProvider.prototype.loadSymbols = function(libName) {
|
||||
this.symbols = [[
|
||||
'0000107a T __gnu_cxx::balloc::__mini_vector<std::pair<__gnu_cxx::bitmap_allocator<char>::_Alloc_block*, __gnu_cxx::bitmap_allocator<char>::_Alloc_block*> >::__mini_vector',
|
||||
'0002c410 T std::basic_streambuf<char, std::char_traits<char> >::pubseekoff',
|
||||
'0002c488 T std::basic_streambuf<char, std::char_traits<char> >::pubseekpos',
|
||||
'000466aa T ___cxa_pure_virtual\n'].join('\n'), ''];
|
||||
'0000107a __gnu_cxx::balloc::__mini_vector<std::pair<__gnu_cxx::bitmap_allocator<char>::_Alloc_block*, __gnu_cxx::bitmap_allocator<char>::_Alloc_block*> >::__mini_vector',
|
||||
'0002c410 std::basic_streambuf<char, std::char_traits<char> >::pubseekoff',
|
||||
'0002c488 std::basic_streambuf<char, std::char_traits<char> >::pubseekpos',
|
||||
'000466aa ___cxa_pure_virtual\n'].join('\n'), ''];
|
||||
};
|
||||
var stdc_prov = new MacCppEntriesProvider();
|
||||
var stdc_syms = [];
|
||||
|
@ -175,7 +175,43 @@ CodeMap.prototype.isAddressBelongsTo_ = function(addr, node) {
|
||||
*/
|
||||
CodeMap.prototype.findInTree_ = function(tree, addr) {
|
||||
var node = tree.findGreatestLessThan(addr);
|
||||
return node && this.isAddressBelongsTo_(addr, node) ? node.value : null;
|
||||
return node && this.isAddressBelongsTo_(addr, node) ? node : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Finds a code entry that contains the specified address. Both static and
|
||||
* dynamic code entries are considered. Returns the code entry and the offset
|
||||
* within the entry.
|
||||
*
|
||||
* @param {number} addr Address.
|
||||
*/
|
||||
CodeMap.prototype.findAddress = function(addr) {
|
||||
var pageAddr = addr >>> CodeMap.PAGE_ALIGNMENT;
|
||||
if (pageAddr in this.pages_) {
|
||||
// Static code entries can contain "holes" of unnamed code.
|
||||
// In this case, the whole library is assigned to this address.
|
||||
var result = this.findInTree_(this.statics_, addr);
|
||||
if (!result) {
|
||||
result = this.findInTree_(this.libraries_, addr);
|
||||
if (!result) return null;
|
||||
}
|
||||
return { entry : result.value, offset : addr - result.key };
|
||||
}
|
||||
var min = this.dynamics_.findMin();
|
||||
var max = this.dynamics_.findMax();
|
||||
if (max != null && addr < (max.key + max.value.size) && addr >= min.key) {
|
||||
var dynaEntry = this.findInTree_(this.dynamics_, addr);
|
||||
if (dynaEntry == null) return null;
|
||||
// Dedupe entry name.
|
||||
var entry = dynaEntry.value;
|
||||
if (!entry.nameUpdated_) {
|
||||
entry.name = this.dynamicsNameGen_.getName(entry.name);
|
||||
entry.nameUpdated_ = true;
|
||||
}
|
||||
return { entry : entry, offset : addr - dynaEntry.key };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@ -186,26 +222,8 @@ CodeMap.prototype.findInTree_ = function(tree, addr) {
|
||||
* @param {number} addr Address.
|
||||
*/
|
||||
CodeMap.prototype.findEntry = function(addr) {
|
||||
var pageAddr = addr >>> CodeMap.PAGE_ALIGNMENT;
|
||||
if (pageAddr in this.pages_) {
|
||||
// Static code entries can contain "holes" of unnamed code.
|
||||
// In this case, the whole library is assigned to this address.
|
||||
return this.findInTree_(this.statics_, addr) ||
|
||||
this.findInTree_(this.libraries_, addr);
|
||||
}
|
||||
var min = this.dynamics_.findMin();
|
||||
var max = this.dynamics_.findMax();
|
||||
if (max != null && addr < (max.key + max.value.size) && addr >= min.key) {
|
||||
var dynaEntry = this.findInTree_(this.dynamics_, addr);
|
||||
if (dynaEntry == null) return null;
|
||||
// Dedupe entry name.
|
||||
if (!dynaEntry.nameUpdated_) {
|
||||
dynaEntry.name = this.dynamicsNameGen_.getName(dynaEntry.name);
|
||||
dynaEntry.nameUpdated_ = true;
|
||||
}
|
||||
return dynaEntry;
|
||||
}
|
||||
return null;
|
||||
var result = this.findAddress(addr);
|
||||
return result ? result.entry : null;
|
||||
};
|
||||
|
||||
|
||||
|
@ -14,5 +14,6 @@
|
||||
if [ "`which c++filt`" == "" ]; then
|
||||
nm "$@"
|
||||
else
|
||||
nm "$@" | c++filt -p -i
|
||||
nm "$@" | sed -n "s/\([0-9a-fA-F]\{8,16\}\) [iItT] \(.*\)/\\1 \\2/p"\
|
||||
| c++filt -p -i
|
||||
fi
|
||||
|
146
tools/profile.js
146
tools/profile.js
@ -37,6 +37,7 @@ function Profile() {
|
||||
this.topDownTree_ = new CallTree();
|
||||
this.bottomUpTree_ = new CallTree();
|
||||
this.c_entries_ = {};
|
||||
this.ticks_ = [];
|
||||
};
|
||||
|
||||
|
||||
@ -235,7 +236,7 @@ Profile.prototype.findEntry = function(addr) {
|
||||
*
|
||||
* @param {Array<number>} stack Stack sample.
|
||||
*/
|
||||
Profile.prototype.recordTick = function(stack) {
|
||||
Profile.prototype.recordTick = function(time_ns, vmState, stack) {
|
||||
var processedStack = this.resolveAndFilterFuncs_(stack);
|
||||
this.bottomUpTree_.addPath(processedStack);
|
||||
processedStack.reverse();
|
||||
@ -832,3 +833,146 @@ CallTree.Node.prototype.descendToChild = function(
|
||||
}
|
||||
return curr;
|
||||
};
|
||||
|
||||
function JsonProfile() {
|
||||
this.codeMap_ = new CodeMap();
|
||||
this.codeEntries_ = [];
|
||||
this.functionEntries_ = [];
|
||||
this.ticks_ = [];
|
||||
}
|
||||
|
||||
JsonProfile.prototype.addLibrary = function(
|
||||
name, startAddr, endAddr) {
|
||||
var entry = new CodeMap.CodeEntry(
|
||||
endAddr - startAddr, name, 'SHARED_LIB');
|
||||
this.codeMap_.addLibrary(startAddr, entry);
|
||||
|
||||
entry.codeId = this.codeEntries_.length;
|
||||
this.codeEntries_.push({name : entry.name, type : entry.type});
|
||||
return entry;
|
||||
};
|
||||
|
||||
JsonProfile.prototype.addStaticCode = function(
|
||||
name, startAddr, endAddr) {
|
||||
var entry = new CodeMap.CodeEntry(
|
||||
endAddr - startAddr, name, 'CPP');
|
||||
this.codeMap_.addStaticCode(startAddr, entry);
|
||||
|
||||
entry.codeId = this.codeEntries_.length;
|
||||
this.codeEntries_.push({name : entry.name, type : entry.type});
|
||||
return entry;
|
||||
};
|
||||
|
||||
JsonProfile.prototype.addCode = function(
|
||||
kind, name, start, size) {
|
||||
var entry = new CodeMap.CodeEntry(size, name, 'CODE');
|
||||
this.codeMap_.addCode(start, entry);
|
||||
|
||||
entry.codeId = this.codeEntries_.length;
|
||||
this.codeEntries_.push({name : entry.name, type : entry.type, kind : kind});
|
||||
|
||||
return entry;
|
||||
};
|
||||
|
||||
JsonProfile.prototype.addFuncCode = function(
|
||||
kind, name, start, size, funcAddr, state) {
|
||||
// As code and functions are in the same address space,
|
||||
// it is safe to put them in a single code map.
|
||||
var func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr);
|
||||
if (!func) {
|
||||
var func = new CodeMap.CodeEntry(0, name, 'SFI');
|
||||
this.codeMap_.addCode(funcAddr, func);
|
||||
|
||||
func.funcId = this.functionEntries_.length;
|
||||
this.functionEntries_.push({name : name, codes : []});
|
||||
} else if (func.name !== name) {
|
||||
// Function object has been overwritten with a new one.
|
||||
func.name = name;
|
||||
|
||||
func.funcId = this.functionEntries_.length;
|
||||
this.functionEntries_.push({name : name, codes : []});
|
||||
}
|
||||
// TODO(jarin): Insert the code object into the SFI's code list.
|
||||
var entry = this.codeMap_.findDynamicEntryByStartAddress(start);
|
||||
if (entry) {
|
||||
// TODO(jarin) This does not look correct, we should really
|
||||
// update the code object (remove the old one and insert this one).
|
||||
if (entry.size === size && entry.func === func) {
|
||||
// Entry state has changed.
|
||||
entry.state = state;
|
||||
}
|
||||
} else {
|
||||
var entry = new CodeMap.CodeEntry(size, name, 'JS');
|
||||
this.codeMap_.addCode(start, entry);
|
||||
|
||||
entry.codeId = this.codeEntries_.length;
|
||||
|
||||
this.functionEntries_[func.funcId].codes.push(entry.codeId);
|
||||
|
||||
if (state === 0) {
|
||||
kind = "Builtin";
|
||||
} else if (state === 1) {
|
||||
kind = "Unopt";
|
||||
} else if (state === 2) {
|
||||
kind = "Opt";
|
||||
}
|
||||
|
||||
this.codeEntries_.push({
|
||||
name : entry.name,
|
||||
type : entry.type,
|
||||
kind : kind,
|
||||
func : func.funcId
|
||||
});
|
||||
}
|
||||
return entry;
|
||||
};
|
||||
|
||||
JsonProfile.prototype.moveCode = function(from, to) {
|
||||
try {
|
||||
this.codeMap_.moveCode(from, to);
|
||||
} catch (e) {
|
||||
printErr("Move: unknown source " + from);
|
||||
}
|
||||
};
|
||||
|
||||
JsonProfile.prototype.deleteCode = function(start) {
|
||||
try {
|
||||
this.codeMap_.deleteCode(start);
|
||||
} catch (e) {
|
||||
printErr("Delete: unknown address " + start);
|
||||
}
|
||||
};
|
||||
|
||||
JsonProfile.prototype.moveFunc = function(from, to) {
|
||||
if (this.codeMap_.findDynamicEntryByStartAddress(from)) {
|
||||
this.codeMap_.moveCode(from, to);
|
||||
}
|
||||
};
|
||||
|
||||
JsonProfile.prototype.findEntry = function(addr) {
|
||||
return this.codeMap_.findEntry(addr);
|
||||
};
|
||||
|
||||
JsonProfile.prototype.recordTick = function(time_ns, vmState, stack) {
|
||||
// TODO(jarin) Resolve the frame-less case (when top of stack is
|
||||
// known code).
|
||||
var processedStack = [];
|
||||
for (var i = 0; i < stack.length; i++) {
|
||||
var resolved = this.codeMap_.findAddress(stack[i]);
|
||||
if (resolved) {
|
||||
processedStack.push(resolved.entry.codeId, resolved.offset);
|
||||
} else {
|
||||
processedStack.push(-1, stack[i]);
|
||||
}
|
||||
}
|
||||
this.ticks_.push({ tm : time_ns, vm : vmState, s : processedStack });
|
||||
};
|
||||
|
||||
JsonProfile.prototype.writeJson = function() {
|
||||
var toplevel = {
|
||||
code : this.codeEntries_,
|
||||
functions : this.functionEntries_,
|
||||
ticks : this.ticks_
|
||||
};
|
||||
write(JSON.stringify(toplevel));
|
||||
};
|
||||
|
106
tools/profview/index.html
Normal file
106
tools/profview/index.html
Normal file
@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Copyright 2017 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. -->
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>V8 Tick Processor</title>
|
||||
<link rel="stylesheet" href="profview.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet">
|
||||
|
||||
<script src="profview.js"></script>
|
||||
<script src="profile-utils.js"></script>
|
||||
</head>
|
||||
|
||||
<body onLoad="main.onLoad();" onResize="main.onResize();">
|
||||
<h3 style="margin-top: 2px;">
|
||||
Chrome V8 profiling log processor
|
||||
</h3>
|
||||
|
||||
<input type="file" id="fileinput" />
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<div id="help">
|
||||
Usage:
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
Record the profile:
|
||||
<pre>
|
||||
d8 --prof your-file.js
|
||||
</pre>
|
||||
|
||||
Then process the file (this resolves C++ symbols and produces
|
||||
a JSON file with the profile data):
|
||||
|
||||
<pre>
|
||||
<v8-dir>/tools/linux-tick-processor --preprocess v8.log > v8.json
|
||||
</pre>
|
||||
|
||||
To view the profile, click the <i>Choose file</i> button above and choose
|
||||
the file in the dialog box.
|
||||
|
||||
</div>
|
||||
|
||||
<div id="timeline" style="display : none">
|
||||
<div>
|
||||
<canvas id="timeline-canvas"/>
|
||||
</div>
|
||||
<table>
|
||||
<tr id="timeline-legend">
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div id="calltree" style="display : none">
|
||||
<div id="mode-bar">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
Attribution:
|
||||
<select id="calltree-attribution">
|
||||
</select>
|
||||
Top-level tree buckets:
|
||||
<select id="calltree-categories">
|
||||
</select>
|
||||
Sort by:
|
||||
<select id="calltree-sort">
|
||||
</select>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<table id="calltree-table" class="calltree">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="numeric">Time (incl)</th>
|
||||
<th class="numeric">% of parent</th>
|
||||
<th id="calltree-table-own-time-header" class="numeric">Own time</th>
|
||||
<th>Function/category</th>
|
||||
<th class="numeric">Ticks</th>
|
||||
<th id="calltree-table-own-ticks-header" class="numeric">Own ticks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p style="font-style:italic;">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
Copyright the V8 Authors - Last change to this page: 2017/02/15
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
278
tools/profview/profile-utils.js
Normal file
278
tools/profview/profile-utils.js
Normal file
@ -0,0 +1,278 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
"use strict"
|
||||
|
||||
let codeKinds = [
|
||||
"UNKNOWN",
|
||||
"CPPCOMP",
|
||||
"CPPGC",
|
||||
"CPPEXT",
|
||||
"CPP",
|
||||
"LIB",
|
||||
"IC",
|
||||
"BC",
|
||||
"STUB",
|
||||
"BUILTIN",
|
||||
"REGEXP",
|
||||
"JSOPT",
|
||||
"JSUNOPT"
|
||||
];
|
||||
|
||||
function resolveCodeKind(code) {
|
||||
if (!code || !code.type) {
|
||||
return "UNKNOWN";
|
||||
} else if (code.type === "CPP") {
|
||||
return "CPP";
|
||||
} else if (code.type === "SHARED_LIB") {
|
||||
return "LIB";
|
||||
} else if (code.type === "CODE") {
|
||||
if (code.kind === "LoadIC" ||
|
||||
code.kind === "StoreIC" ||
|
||||
code.kind === "KeyedStoreIC" ||
|
||||
code.kind === "KeyedLoadIC" ||
|
||||
code.kind === "LoadGlobalIC" ||
|
||||
code.kind === "Handler") {
|
||||
return "IC";
|
||||
} else if (code.kind === "BytecodeHandler") {
|
||||
return "BC";
|
||||
} else if (code.kind === "Stub") {
|
||||
return "STUB";
|
||||
} else if (code.kind === "Builtin") {
|
||||
return "BUILTIN";
|
||||
} else if (code.kind === "RegExp") {
|
||||
return "REGEXP";
|
||||
}
|
||||
console.log("Unknown CODE: '" + code.kind + "'.");
|
||||
return "CODE";
|
||||
} else if (code.type === "JS") {
|
||||
if (code.kind === "Builtin") {
|
||||
return "JSUNOPT";
|
||||
} else if (code.kind === "Opt") {
|
||||
return "JSOPT";
|
||||
} else if (code.kind === "Unopt") {
|
||||
return "JSUNOPT";
|
||||
}
|
||||
}
|
||||
console.log("Unknown code type '" + type + "'.");
|
||||
}
|
||||
|
||||
function resolveCodeKindAndVmState(code, vmState) {
|
||||
let kind = resolveCodeKind(code);
|
||||
if (kind === "CPP") {
|
||||
if (vmState === 1) {
|
||||
kind = "CPPGC";
|
||||
} else if (vmState === 2) {
|
||||
kind = "CPPCOMP";
|
||||
} else if (vmState === 4) {
|
||||
kind = "CPPEXT";
|
||||
}
|
||||
}
|
||||
return kind;
|
||||
}
|
||||
|
||||
function createNodeFromStackEntry(code) {
|
||||
let name = code ? code.name : "UNKNOWN";
|
||||
|
||||
return { name, type : resolveCodeKind(code),
|
||||
children : [], ownTicks : 0, ticks : 0 };
|
||||
}
|
||||
|
||||
function addStackToTree(file, stack, tree, filter, ascending, start) {
|
||||
if (start === undefined) {
|
||||
start = ascending ? 0 : stack.length - 2;
|
||||
}
|
||||
tree.ticks++;
|
||||
for (let i = start;
|
||||
ascending ? (i < stack.length) : (i >= 0);
|
||||
i += ascending ? 2 : -2) {
|
||||
let codeId = stack[i];
|
||||
let code = codeId >= 0 ? file.code[codeId] : undefined;
|
||||
if (filter) {
|
||||
let type = code ? code.type : undefined;
|
||||
let kind = code ? code.kind : undefined;
|
||||
if (!filter(type, kind)) continue;
|
||||
}
|
||||
|
||||
// For JavaScript function, pretend there is one instance of optimized
|
||||
// function and one instance of unoptimized function per SFI.
|
||||
let type = resolveCodeKind(code);
|
||||
let childId;
|
||||
if (type === "JSOPT") {
|
||||
childId = code.func * 4 + 1;
|
||||
} else if (type === "JSUNOPT") {
|
||||
childId = code.func * 4 + 2;
|
||||
} else {
|
||||
childId = codeId * 4;
|
||||
}
|
||||
let child = tree.children[childId];
|
||||
if (!child) {
|
||||
child = createNodeFromStackEntry(code);
|
||||
tree.children[childId] = child;
|
||||
}
|
||||
child.ticks++;
|
||||
tree = child;
|
||||
}
|
||||
tree.ownTicks++;
|
||||
}
|
||||
|
||||
function createEmptyNode(name) {
|
||||
return {
|
||||
name : name,
|
||||
type : "CAT",
|
||||
children : [],
|
||||
ownTicks : 0,
|
||||
ticks : 0
|
||||
};
|
||||
}
|
||||
|
||||
class PlainCallTreeProcessor {
|
||||
constructor(filter, isBottomUp) {
|
||||
this.filter = filter;
|
||||
this.tree = createEmptyNode("root");
|
||||
this.isBottomUp = isBottomUp;
|
||||
}
|
||||
|
||||
addStack(file, timestamp, vmState, stack) {
|
||||
addStackToTree(file, stack, this.tree, this.filter, this.isBottomUp);
|
||||
}
|
||||
}
|
||||
|
||||
class CategorizedCallTreeProcessor {
|
||||
constructor(filter, isBottomUp) {
|
||||
this.filter = filter;
|
||||
let root = createEmptyNode("root");
|
||||
let categories = {};
|
||||
function addCategory(name, types) {
|
||||
let n = createEmptyNode(name);
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
categories[types[i]] = n;
|
||||
}
|
||||
root.children.push(n);
|
||||
}
|
||||
addCategory("JS Optimized", [ "JSOPT" ]);
|
||||
addCategory("JS Unoptimized", [ "JSUNOPT", "BC" ]);
|
||||
addCategory("IC", [ "IC" ]);
|
||||
addCategory("RegExp", [ "REGEXP" ]);
|
||||
addCategory("Other generated", [ "STUB", "BUILTIN" ]);
|
||||
addCategory("C++", [ "CPP", "LIB" ]);
|
||||
addCategory("C++/GC", [ "CPPGC" ]);
|
||||
addCategory("C++/Compiler", [ "CPPCOMP" ]);
|
||||
addCategory("C++/External", [ "CPPEXT" ]);
|
||||
addCategory("Unknown", [ "UNKNOWN" ]);
|
||||
|
||||
this.tree = root;
|
||||
this.categories = categories;
|
||||
this.isBottomUp = isBottomUp;
|
||||
}
|
||||
|
||||
addStack(file, timestamp, vmState, stack) {
|
||||
if (stack.length === 0) return;
|
||||
let codeId = stack[0];
|
||||
let code = codeId >= 0 ? file.code[codeId] : undefined;
|
||||
let kind = resolveCodeKindAndVmState(code, vmState);
|
||||
let node = this.categories[kind];
|
||||
|
||||
this.tree.ticks++;
|
||||
|
||||
console.assert(node);
|
||||
|
||||
addStackToTree(file, stack, node, this.filter, this.isBottomUp);
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionListTree {
|
||||
constructor(filter) {
|
||||
this.tree = { name : "root", children : [], ownTicks : 0, ticks : 0 };
|
||||
this.codeVisited = [];
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
addStack(file, timestamp, vmState, stack) {
|
||||
this.tree.ticks++;
|
||||
let child = null;
|
||||
for (let i = stack.length - 2; i >= 0; i -= 2) {
|
||||
let codeId = stack[i];
|
||||
if (codeId < 0 || this.codeVisited[codeId]) continue;
|
||||
|
||||
let code = codeId >= 0 ? file.code[codeId] : undefined;
|
||||
if (this.filter) {
|
||||
let type = code ? code.type : undefined;
|
||||
let kind = code ? code.kind : undefined;
|
||||
if (!this.filter(type, kind)) continue;
|
||||
}
|
||||
child = this.tree.children[codeId];
|
||||
if (!child) {
|
||||
child = createNodeFromStackEntry(code);
|
||||
this.tree.children[codeId] = child;
|
||||
}
|
||||
child.ticks++;
|
||||
this.codeVisited[codeId] = true;
|
||||
}
|
||||
if (child) {
|
||||
child.ownTicks++;
|
||||
}
|
||||
|
||||
for (let i = 0; i < stack.length; i += 2) {
|
||||
let codeId = stack[i];
|
||||
if (codeId >= 0) this.codeVisited[codeId] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CategorySampler {
|
||||
constructor(file, bucketCount) {
|
||||
this.bucketCount = bucketCount;
|
||||
|
||||
this.firstTime = file.ticks[0].tm;
|
||||
let lastTime = file.ticks[file.ticks.length - 1].tm;
|
||||
this.step = (lastTime - this.firstTime) / bucketCount;
|
||||
|
||||
this.buckets = [];
|
||||
let bucket = {};
|
||||
for (let i = 0; i < codeKinds.length; i++) {
|
||||
bucket[codeKinds[i]] = 0;
|
||||
}
|
||||
for (let i = 0; i < bucketCount; i++) {
|
||||
this.buckets.push(Object.assign({ total : 0 }, bucket));
|
||||
}
|
||||
}
|
||||
|
||||
addStack(file, timestamp, vmState, stack) {
|
||||
let i = Math.floor((timestamp - this.firstTime) / this.step);
|
||||
if (i == this.buckets.length) i--;
|
||||
console.assert(i >= 0 && i < this.buckets.length);
|
||||
|
||||
let bucket = this.buckets[i];
|
||||
bucket.total++;
|
||||
|
||||
let codeId = (stack.length > 0) ? stack[0] : -1;
|
||||
let code = codeId >= 0 ? file.code[codeId] : undefined;
|
||||
let kind = resolveCodeKindAndVmState(code, vmState);
|
||||
bucket[kind]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a tree out of a ticks sequence.
|
||||
// {file} is the JSON files with the ticks and code objects.
|
||||
// {startTime}, {endTime} is the interval.
|
||||
// {tree} is the processor of stacks.
|
||||
function generateTree(
|
||||
file, startTime, endTime, tree) {
|
||||
let ticks = file.ticks;
|
||||
let i = 0;
|
||||
while (i < ticks.length && ticks[i].tm < startTime) {
|
||||
i++;
|
||||
}
|
||||
|
||||
let tickCount = 0;
|
||||
while (i < ticks.length && ticks[i].tm < endTime) {
|
||||
tree.addStack(file, ticks[i].tm, ticks[i].vm, ticks[i].s);
|
||||
i++;
|
||||
tickCount++;
|
||||
}
|
||||
|
||||
return tickCount;
|
||||
}
|
53
tools/profview/profview.css
Normal file
53
tools/profview/profview.css
Normal file
@ -0,0 +1,53 @@
|
||||
table.calltree {
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
.numeric {
|
||||
width : 12ex;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
div.code-type-chip {
|
||||
display : inline-block;
|
||||
padding : 0.0em;
|
||||
}
|
||||
|
||||
span.code-type-chip {
|
||||
border-radius : 1em;
|
||||
display : inline-block;
|
||||
padding : 0.1em;
|
||||
background-color : #4040c0;
|
||||
color: #ffffff;
|
||||
font-size : small;
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
span.code-type-chip-space {
|
||||
width : 0.5ex;
|
||||
display : inline-block;
|
||||
}
|
||||
|
||||
div.mode-button {
|
||||
padding: 1em 3em;
|
||||
display: inline-block;
|
||||
background-color: #6070ff;
|
||||
color : #ffffff;
|
||||
margin: 0 0.2em 2em 0;
|
||||
box-shadow: 3px 3px 2px #d0d0ff;
|
||||
}
|
||||
|
||||
div.mode-button:hover {
|
||||
background-color: #4858ff;
|
||||
}
|
||||
|
||||
div.active-mode-button {
|
||||
background-color: #0000ff;
|
||||
box-shadow: 3px 3px 2px #a0a0ff;
|
||||
}
|
||||
|
||||
div.active-mode-button:hover {
|
||||
background-color: #0000ff;
|
||||
}
|
812
tools/profview/profview.js
Normal file
812
tools/profview/profview.js
Normal file
@ -0,0 +1,812 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
"use strict"
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
let components = [];
|
||||
|
||||
function createViews() {
|
||||
components.push(new CallTreeView());
|
||||
components.push(new TimelineView());
|
||||
components.push(new HelpView());
|
||||
|
||||
let modeBar = $("mode-bar");
|
||||
|
||||
function addMode(id, text, active) {
|
||||
let div = document.createElement("div");
|
||||
div.classList = "mode-button" + (active ? " active-mode-button" : "");
|
||||
div.id = "mode-" + id;
|
||||
div.textContent = text;
|
||||
div.onclick = () => {
|
||||
if (main.currentState.callTree.mode === id) return;
|
||||
let old = $("mode-" + main.currentState.callTree.mode);
|
||||
old.classList = "mode-button";
|
||||
div.classList = "mode-button active-mode-button";
|
||||
main.setMode(id);
|
||||
};
|
||||
modeBar.appendChild(div);
|
||||
}
|
||||
|
||||
addMode("bottom-up", "Bottom up", true);
|
||||
addMode("top-down", "Top down");
|
||||
addMode("function-list", "Functions");
|
||||
|
||||
main.setMode("bottom-up");
|
||||
}
|
||||
|
||||
function emptyState() {
|
||||
return {
|
||||
file : null,
|
||||
start : 0,
|
||||
end : Infinity,
|
||||
timeLine : {
|
||||
width : 100,
|
||||
height : 100
|
||||
},
|
||||
callTree : {
|
||||
mode : "none",
|
||||
attribution : "js-exclude-bc",
|
||||
categories : "code-type",
|
||||
sort : "time"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setCallTreeState(state, callTreeState) {
|
||||
state = Object.assign({}, state);
|
||||
state.callTree = callTreeState;
|
||||
return state;
|
||||
}
|
||||
|
||||
let main = {
|
||||
currentState : emptyState(),
|
||||
|
||||
setMode(mode) {
|
||||
if (mode != main.currentState.mode) {
|
||||
let callTreeState = Object.assign({}, main.currentState.callTree);
|
||||
callTreeState.mode = mode;
|
||||
switch (mode) {
|
||||
case "bottom-up":
|
||||
callTreeState.attribution = "js-exclude-bc";
|
||||
callTreeState.categories = "code-type";
|
||||
callTreeState.sort = "time";
|
||||
break;
|
||||
case "top-down":
|
||||
callTreeState.attribution = "js-exclude-bc";
|
||||
callTreeState.categories = "none";
|
||||
callTreeState.sort = "time";
|
||||
break;
|
||||
case "function-list":
|
||||
callTreeState.attribution = "js-exclude-bc";
|
||||
callTreeState.categories = "none";
|
||||
callTreeState.sort = "own-time";
|
||||
break;
|
||||
default:
|
||||
console.error("Invalid mode");
|
||||
}
|
||||
main.currentState = setCallTreeState(main.currentState, callTreeState);
|
||||
main.delayRender();
|
||||
}
|
||||
},
|
||||
|
||||
setCallTreeAttribution(attribution) {
|
||||
if (attribution != main.currentState.attribution) {
|
||||
let callTreeState = Object.assign({}, main.currentState.callTree);
|
||||
callTreeState.attribution = attribution;
|
||||
main.currentState = setCallTreeState(main.currentState, callTreeState);
|
||||
main.delayRender();
|
||||
}
|
||||
},
|
||||
|
||||
setCallTreeSort(sort) {
|
||||
if (sort != main.currentState.sort) {
|
||||
let callTreeState = Object.assign({}, main.currentState.callTree);
|
||||
callTreeState.sort = sort;
|
||||
main.currentState = setCallTreeState(main.currentState, callTreeState);
|
||||
main.delayRender();
|
||||
}
|
||||
},
|
||||
|
||||
setCallTreeCategories(categories) {
|
||||
if (categories != main.currentState.categories) {
|
||||
let callTreeState = Object.assign({}, main.currentState.callTree);
|
||||
callTreeState.categories = categories;
|
||||
main.currentState = setCallTreeState(main.currentState, callTreeState);
|
||||
main.delayRender();
|
||||
}
|
||||
},
|
||||
|
||||
setViewInterval(start, end) {
|
||||
if (start != main.currentState.start ||
|
||||
end != main.currentState.end) {
|
||||
main.currentState = Object.assign({}, main.currentState);
|
||||
main.currentState.start = start;
|
||||
main.currentState.end = end;
|
||||
main.delayRender();
|
||||
}
|
||||
},
|
||||
|
||||
setTimeLineDimensions(width, height) {
|
||||
if (width != main.currentState.timeLine.width ||
|
||||
height != main.currentState.timeLine.height) {
|
||||
let timeLine = Object.assign({}, main.currentState.timeLine);
|
||||
timeLine.width = width;
|
||||
timeLine.height = height;
|
||||
main.currentState = Object.assign({}, main.currentState);
|
||||
main.currentState.timeLine = timeLine;
|
||||
main.delayRender();
|
||||
}
|
||||
},
|
||||
|
||||
setFile(file) {
|
||||
if (file != main.currentState.file) {
|
||||
main.currentState = Object.assign({}, main.currentState);
|
||||
main.currentState.file = file;
|
||||
main.delayRender();
|
||||
}
|
||||
},
|
||||
|
||||
onResize() {
|
||||
main.setTimeLineDimensions(
|
||||
window.innerWidth - 20, window.innerHeight / 8);
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
function loadHandler(evt) {
|
||||
let f = evt.target.files[0];
|
||||
if (f) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
let profData = JSON.parse(event.target.result);
|
||||
main.setViewInterval(0, Infinity);
|
||||
main.setFile(profData);
|
||||
};
|
||||
reader.onerror = function(event) {
|
||||
console.error(
|
||||
"File could not be read! Code " + event.target.error.code);
|
||||
};
|
||||
reader.readAsText(f);
|
||||
} else {
|
||||
main.setFile(null);
|
||||
}
|
||||
}
|
||||
$("fileinput").addEventListener(
|
||||
"change", loadHandler, false);
|
||||
createViews();
|
||||
main.onResize();
|
||||
},
|
||||
|
||||
delayRender() {
|
||||
Promise.resolve().then(() => {
|
||||
for (let c of components) {
|
||||
c.render(main.currentState);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let bucketDescriptors =
|
||||
[ { kinds : [ "JSOPT" ],
|
||||
color : "#ffb000",
|
||||
backgroundColor : "#ffe0c0",
|
||||
text : "JS Optimized" },
|
||||
{ kinds : [ "JSUNOPT", "BC" ],
|
||||
color : "#00ff00",
|
||||
backgroundColor : "#c0ffc0",
|
||||
text : "JS Unoptimized" },
|
||||
{ kinds : [ "IC" ],
|
||||
color : "#ffff00",
|
||||
backgroundColor : "#ffffc0",
|
||||
text : "IC" },
|
||||
{ kinds : [ "STUB", "BUILTIN", "REGEXP" ],
|
||||
color : "#ffb0b0",
|
||||
backgroundColor : "#fff0f0",
|
||||
text : "Other generated" },
|
||||
{ kinds : [ "CPP", "LIB" ],
|
||||
color : "#0000ff",
|
||||
backgroundColor : "#c0c0ff",
|
||||
text : "C++" },
|
||||
{ kinds : [ "CPPEXT" ],
|
||||
color : "#8080ff",
|
||||
backgroundColor : "#e0e0ff",
|
||||
text : "C++/external" },
|
||||
{ kinds : [ "CPPCOMP" ],
|
||||
color : "#00ffff",
|
||||
backgroundColor : "#c0ffff",
|
||||
text : "C++/Compiler" },
|
||||
{ kinds : [ "CPPGC" ],
|
||||
color : "#ff00ff",
|
||||
backgroundColor : "#ffc0ff",
|
||||
text : "C++/GC" },
|
||||
{ kinds : [ "UNKNOWN" ],
|
||||
color : "#f0f0f0",
|
||||
backgroundColor : "#e0e0e0",
|
||||
text : "Unknown" }
|
||||
];
|
||||
|
||||
function bucketFromKind(kind) {
|
||||
for (let i = 0; i < bucketDescriptors.length; i++) {
|
||||
let bucket = bucketDescriptors[i];
|
||||
for (let j = 0; j < bucket.kinds.length; j++) {
|
||||
if (bucket.kinds[j] === kind) {
|
||||
return bucket;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class CallTreeView {
|
||||
constructor() {
|
||||
this.element = $("calltree");
|
||||
this.treeElement = $("calltree-table");
|
||||
this.selectAttribution = $("calltree-attribution");
|
||||
this.selectCategories = $("calltree-categories");
|
||||
this.selectSort = $("calltree-sort");
|
||||
|
||||
this.selectAttribution.onchange = () => {
|
||||
main.setCallTreeAttribution(this.selectAttribution.value);
|
||||
};
|
||||
|
||||
this.selectCategories.onchange = () => {
|
||||
main.setCallTreeCategories(this.selectCategories.value);
|
||||
};
|
||||
|
||||
this.selectSort.onchange = () => {
|
||||
main.setCallTreeSort(this.selectSort.value);
|
||||
};
|
||||
|
||||
this.currentState = null;
|
||||
}
|
||||
|
||||
filterFromFilterId(id) {
|
||||
switch (id) {
|
||||
case "full-tree":
|
||||
return (type, kind) => true;
|
||||
case "js-funs":
|
||||
return (type, kind) => type !== 'CODE';
|
||||
case "js-exclude-bc":
|
||||
return (type, kind) =>
|
||||
type !== 'CODE' || !CallTreeView.IsBytecodeHandler(kind);
|
||||
}
|
||||
}
|
||||
|
||||
sortFromId(id) {
|
||||
switch (id) {
|
||||
case "time":
|
||||
return (c1, c2) => c2.ticks - c1.ticks;
|
||||
case "own-time":
|
||||
return (c1, c2) => c2.ownTicks - c1.ownTicks;
|
||||
case "category-time":
|
||||
return (c1, c2) => {
|
||||
if (c1.type === c2.type) return c2.ticks - c1.ticks;
|
||||
if (c1.type < c2.type) return 1;
|
||||
return -1;
|
||||
};
|
||||
case "category-own-time":
|
||||
return (c1, c2) => {
|
||||
if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks;
|
||||
if (c1.type < c2.type) return 1;
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static IsBytecodeHandler(kind) {
|
||||
return kind === "BytecodeHandler";
|
||||
}
|
||||
|
||||
createExpander(indent) {
|
||||
let div = document.createElement("div");
|
||||
div.style.width = (1 + indent) + "em";
|
||||
div.style.display = "inline-block";
|
||||
div.style.textAlign = "right";
|
||||
return div;
|
||||
}
|
||||
|
||||
codeTypeToText(type) {
|
||||
switch (type) {
|
||||
case "UNKNOWN":
|
||||
return "Unknown";
|
||||
case "CPPCOMP":
|
||||
return "C++ (compiler)";
|
||||
case "CPPGC":
|
||||
return "C++";
|
||||
case "CPPEXT":
|
||||
return "C++ External";
|
||||
case "CPP":
|
||||
return "C++";
|
||||
case "LIB":
|
||||
return "Library";
|
||||
case "IC":
|
||||
return "IC";
|
||||
case "BC":
|
||||
return "Bytecode";
|
||||
case "STUB":
|
||||
return "Stub";
|
||||
case "BUILTIN":
|
||||
return "Builtin";
|
||||
case "REGEXP":
|
||||
return "RegExp";
|
||||
case "JSOPT":
|
||||
return "JS opt";
|
||||
case "JSUNOPT":
|
||||
return "JS unopt";
|
||||
}
|
||||
console.error("Unknown type: " + type);
|
||||
}
|
||||
|
||||
createTypeDiv(type) {
|
||||
if (type === "CAT") {
|
||||
return document.createTextNode("");
|
||||
}
|
||||
let div = document.createElement("div");
|
||||
div.classList.add("code-type-chip");
|
||||
|
||||
let span = document.createElement("span");
|
||||
span.classList.add("code-type-chip");
|
||||
span.textContent = this.codeTypeToText(type);
|
||||
div.appendChild(span);
|
||||
|
||||
span = document.createElement("span");
|
||||
span.classList.add("code-type-chip-space");
|
||||
div.appendChild(span);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
expandTree(tree, indent) {
|
||||
let that = this;
|
||||
let index = 0;
|
||||
let id = "R/";
|
||||
let row = tree.row;
|
||||
let expander = tree.expander;
|
||||
|
||||
if (row) {
|
||||
console.assert("expander");
|
||||
index = row.rowIndex;
|
||||
id = row.id;
|
||||
|
||||
// Make sure we collapse the children when the row is clicked
|
||||
// again.
|
||||
expander.textContent = "\u25BE";
|
||||
let expandHandler = expander.onclick;
|
||||
expander.onclick = () => {
|
||||
that.collapseRow(tree, expander, expandHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the children, and sort them by ticks.
|
||||
let children = [];
|
||||
for (let child in tree.children) {
|
||||
if (tree.children[child].ticks > 0) {
|
||||
children.push(tree.children[child]);
|
||||
}
|
||||
}
|
||||
children.sort(this.sortFromId(this.currentState.callTree.sort));
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let node = children[i];
|
||||
let row = this.rows.insertRow(index);
|
||||
row.id = id + i + "/";
|
||||
|
||||
if (node.type != "CAT") {
|
||||
row.style.backgroundColor = bucketFromKind(node.type).backgroundColor;
|
||||
}
|
||||
|
||||
// Inclusive time % cell.
|
||||
let c = row.insertCell();
|
||||
c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%";
|
||||
c.style.textAlign = "right";
|
||||
// Percent-of-parent cell.
|
||||
c = row.insertCell();
|
||||
c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%";
|
||||
c.style.textAlign = "right";
|
||||
// Exclusive time % cell.
|
||||
if (this.currentState.callTree.mode !== "bottom-up") {
|
||||
c = row.insertCell(-1);
|
||||
c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%";
|
||||
c.style.textAlign = "right";
|
||||
}
|
||||
|
||||
// Create the name cell.
|
||||
let nameCell = row.insertCell();
|
||||
let expander = this.createExpander(indent);
|
||||
nameCell.appendChild(expander);
|
||||
nameCell.appendChild(this.createTypeDiv(node.type));
|
||||
nameCell.appendChild(document.createTextNode(node.name));
|
||||
|
||||
// Inclusive ticks cell.
|
||||
c = row.insertCell();
|
||||
c.textContent = node.ticks;
|
||||
c.style.textAlign = "right";
|
||||
if (this.currentState.callTree.mode !== "bottom-up") {
|
||||
// Exclusive ticks cell.
|
||||
c = row.insertCell(-1);
|
||||
c.textContent = node.ownTicks;
|
||||
c.style.textAlign = "right";
|
||||
}
|
||||
if (node.children.length > 0) {
|
||||
expander.textContent = "\u25B8";
|
||||
expander.onclick = () => { that.expandTree(node, indent + 1); };
|
||||
}
|
||||
|
||||
node.row = row;
|
||||
node.expander = expander;
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
collapseRow(tree, expander, expandHandler) {
|
||||
let row = tree.row;
|
||||
let id = row.id;
|
||||
let index = row.rowIndex;
|
||||
while (row.rowIndex < this.rows.rows.length &&
|
||||
this.rows.rows[index].id.startsWith(id)) {
|
||||
this.rows.deleteRow(index);
|
||||
}
|
||||
|
||||
expander.textContent = "\u25B8";
|
||||
expander.onclick = expandHandler;
|
||||
}
|
||||
|
||||
fillSelects(calltree) {
|
||||
function addOptions(e, values, current) {
|
||||
while (e.options.length > 0) {
|
||||
e.remove(0);
|
||||
}
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
let option = document.createElement("option");
|
||||
option.value = values[i].value;
|
||||
option.textContent = values[i].text;
|
||||
e.appendChild(option);
|
||||
}
|
||||
e.value = current;
|
||||
}
|
||||
|
||||
let attributions = [
|
||||
{ value : "js-exclude-bc",
|
||||
text : "Attribute bytecode handlers to caller" },
|
||||
{ value : "full-tree",
|
||||
text : "Count each code object separately" },
|
||||
{ value : "js-funs",
|
||||
text : "Attribute non-functions to JS functions" }
|
||||
];
|
||||
|
||||
switch (calltree.mode) {
|
||||
case "bottom-up":
|
||||
addOptions(this.selectAttribution, attributions, calltree.attribution);
|
||||
addOptions(this.selectCategories, [
|
||||
{ value : "code-type", text : "Code type" },
|
||||
{ value : "none", text : "None" }
|
||||
], calltree.categories);
|
||||
addOptions(this.selectSort, [
|
||||
{ value : "time", text : "Time (including children)" },
|
||||
{ value : "category-time", text : "Code category, time" },
|
||||
], calltree.sort);
|
||||
return;
|
||||
case "top-down":
|
||||
addOptions(this.selectAttribution, attributions, calltree.attribution);
|
||||
addOptions(this.selectCategories, [
|
||||
{ value : "none", text : "None" }
|
||||
], calltree.categories);
|
||||
addOptions(this.selectSort, [
|
||||
{ value : "time", text : "Time (including children)" },
|
||||
{ value : "own-time", text : "Own time" },
|
||||
{ value : "category-time", text : "Code category, time" },
|
||||
{ value : "category-own-time", text : "Code category, own time"}
|
||||
], calltree.sort);
|
||||
return;
|
||||
case "function-list":
|
||||
addOptions(this.selectAttribution, attributions, calltree.attribution);
|
||||
addOptions(this.selectCategories, [
|
||||
{ value : "none", text : "None" }
|
||||
], calltree.categories);
|
||||
addOptions(this.selectSort, [
|
||||
{ value : "own-time", text : "Own time" },
|
||||
{ value : "time", text : "Time (including children)" },
|
||||
{ value : "category-own-time", text : "Code category, own time"},
|
||||
{ value : "category-time", text : "Code category, time" },
|
||||
], calltree.sort);
|
||||
return;
|
||||
}
|
||||
console.error("Unexpected mode");
|
||||
}
|
||||
|
||||
render(newState) {
|
||||
let oldState = this.currentState;
|
||||
if (!newState.file) {
|
||||
this.element.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentState = newState;
|
||||
if (oldState) {
|
||||
if (newState.file === oldState.file &&
|
||||
newState.start === oldState.start &&
|
||||
newState.end === oldState.end &&
|
||||
newState.callTree.mode === oldState.callTree.mode &&
|
||||
newState.callTree.attribution === oldState.callTree.attribution &&
|
||||
newState.callTree.categories === oldState.callTree.categories &&
|
||||
newState.callTree.sort === oldState.callTree.sort) {
|
||||
// No change => just return.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.element.style.display = "inherit";
|
||||
|
||||
let mode = this.currentState.callTree.mode;
|
||||
if (!oldState || mode !== oldState.callTree.mode) {
|
||||
// Technically, we should also call this if attribution, categories or
|
||||
// sort change, but the selection is already highlighted by the combobox
|
||||
// itself, so we do need to do anything here.
|
||||
this.fillSelects(newState.callTree);
|
||||
}
|
||||
|
||||
let inclusiveDisplay = (mode === "bottom-up") ? "none" : "inherit";
|
||||
let ownTimeTh = $(this.treeElement.id + "-own-time-header");
|
||||
ownTimeTh.style.display = inclusiveDisplay;
|
||||
let ownTicksTh = $(this.treeElement.id + "-own-ticks-header");
|
||||
ownTicksTh.style.display = inclusiveDisplay;
|
||||
|
||||
// Build the tree.
|
||||
let stackProcessor;
|
||||
let filter = this.filterFromFilterId(this.currentState.callTree.attribution);
|
||||
if (mode === "top-down") {
|
||||
stackProcessor =
|
||||
new PlainCallTreeProcessor(filter, false);
|
||||
} else if (mode === "function-list") {
|
||||
stackProcessor =
|
||||
new FunctionListTree(filter);
|
||||
|
||||
} else {
|
||||
console.assert(mode === "bottom-up");
|
||||
if (this.currentState.callTree.categories == "none") {
|
||||
stackProcessor =
|
||||
new PlainCallTreeProcessor(filter, true);
|
||||
} else {
|
||||
console.assert(this.currentState.callTree.categories === "code-type");
|
||||
stackProcessor =
|
||||
new CategorizedCallTreeProcessor(filter, true);
|
||||
}
|
||||
}
|
||||
this.tickCount =
|
||||
generateTree(this.currentState.file,
|
||||
this.currentState.start,
|
||||
this.currentState.end,
|
||||
stackProcessor);
|
||||
// TODO(jarin) Handle the case when tick count is negative.
|
||||
|
||||
this.tree = stackProcessor.tree;
|
||||
|
||||
// Remove old content of the table, replace with new one.
|
||||
let oldRows = this.treeElement.getElementsByTagName("tbody");
|
||||
let newRows = document.createElement("tbody");
|
||||
this.rows = newRows;
|
||||
|
||||
// Populate the table.
|
||||
this.expandTree(this.tree, 0);
|
||||
|
||||
// Swap in the new rows.
|
||||
this.treeElement.replaceChild(newRows, oldRows[0]);
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineView {
|
||||
constructor() {
|
||||
this.element = $("timeline");
|
||||
this.canvas = $("timeline-canvas");
|
||||
this.legend = $("timeline-legend");
|
||||
|
||||
this.canvas.onmousedown = this.onMouseDown.bind(this);
|
||||
this.canvas.onmouseup = this.onMouseUp.bind(this);
|
||||
this.canvas.onmousemove = this.onMouseMove.bind(this);
|
||||
|
||||
this.selectionStart = null;
|
||||
this.selectionEnd = null;
|
||||
this.selecting = false;
|
||||
|
||||
this.currentState = null;
|
||||
}
|
||||
|
||||
onMouseDown(e) {
|
||||
this.selectionStart =
|
||||
e.clientX - this.canvas.getBoundingClientRect().left;
|
||||
this.selectionEnd = this.selectionStart + 1;
|
||||
this.selecting = true;
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
if (this.selecting) {
|
||||
this.selectionEnd =
|
||||
e.clientX - this.canvas.getBoundingClientRect().left;
|
||||
this.drawSelection();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp(e) {
|
||||
if (this.selectionStart !== null) {
|
||||
let x = e.clientX - this.canvas.getBoundingClientRect().left;
|
||||
if (Math.abs(x - this.selectionStart) < 10) {
|
||||
this.selectionStart = null;
|
||||
this.selectionEnd = null;
|
||||
let ctx = this.canvas.getContext("2d");
|
||||
ctx.drawImage(this.buffer, 0, 0);
|
||||
} else {
|
||||
this.selectionEnd = x;
|
||||
this.drawSelection();
|
||||
}
|
||||
let file = this.currentState.file;
|
||||
if (file) {
|
||||
let start = this.selectionStart === null ? 0 : this.selectionStart;
|
||||
let end = this.selectionEnd === null ? Infinity : this.selectionEnd;
|
||||
let firstTime = file.ticks[0].tm;
|
||||
let lastTime = file.ticks[file.ticks.length - 1].tm;
|
||||
|
||||
let width = this.buffer.width;
|
||||
|
||||
start = (start / width) * (lastTime - firstTime) + firstTime;
|
||||
end = (end / width) * (lastTime - firstTime) + firstTime;
|
||||
|
||||
if (end < start) {
|
||||
let temp = start;
|
||||
start = end;
|
||||
end = temp;
|
||||
}
|
||||
|
||||
main.setViewInterval(start, end);
|
||||
}
|
||||
}
|
||||
this.selecting = false;
|
||||
}
|
||||
|
||||
drawSelection() {
|
||||
let ctx = this.canvas.getContext("2d");
|
||||
ctx.drawImage(this.buffer, 0, 0);
|
||||
|
||||
if (this.selectionStart !== null && this.selectionEnd !== null) {
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
|
||||
let left = Math.min(this.selectionStart, this.selectionEnd);
|
||||
let right = Math.max(this.selectionStart, this.selectionEnd);
|
||||
ctx.fillRect(0, 0, left, this.buffer.height);
|
||||
ctx.fillRect(right, 0, this.buffer.width - right, this.buffer.height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render(newState) {
|
||||
let oldState = this.currentState;
|
||||
|
||||
if (!newState.file) {
|
||||
this.element.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentState = newState;
|
||||
if (oldState) {
|
||||
if (newState.timeLine.width === oldState.timeLine.width &&
|
||||
newState.timeLine.height === oldState.timeLine.height &&
|
||||
newState.file === oldState.file &&
|
||||
newState.start === oldState.start &&
|
||||
newState.end === oldState.end) {
|
||||
// No change, nothing to do.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.element.style.display = "inherit";
|
||||
|
||||
// Make sure the canvas has the right dimensions.
|
||||
let width = this.currentState.timeLine.width;
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = this.currentState.timeLine.height;
|
||||
|
||||
let file = this.currentState.file;
|
||||
if (!file) return;
|
||||
|
||||
let firstTime = file.ticks[0].tm;
|
||||
let lastTime = file.ticks[file.ticks.length - 1].tm;
|
||||
let start = Math.max(this.currentState.start, firstTime);
|
||||
let end = Math.min(this.currentState.end, lastTime);
|
||||
|
||||
this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width;
|
||||
this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width;
|
||||
|
||||
let tickCount = file.ticks.length;
|
||||
|
||||
let minBucketPixels = 10;
|
||||
let minBucketSamples = 30;
|
||||
let bucketCount = Math.min(width / minBucketPixels,
|
||||
tickCount / minBucketSamples);
|
||||
|
||||
let stackProcessor = new CategorySampler(file, bucketCount);
|
||||
generateTree(file, 0, Infinity, stackProcessor);
|
||||
|
||||
let buffer = document.createElement("canvas");
|
||||
|
||||
buffer.width = this.canvas.width;
|
||||
buffer.height = this.canvas.height;
|
||||
|
||||
// Calculate the bar heights for each bucket.
|
||||
let graphHeight = buffer.height;
|
||||
let buckets = stackProcessor.buckets;
|
||||
let bucketsGraph = [];
|
||||
for (let i = 0; i < buckets.length; i++) {
|
||||
let sum = 0;
|
||||
let bucketData = [];
|
||||
let total = buckets[i].total;
|
||||
for (let j = 0; j < bucketDescriptors.length; j++) {
|
||||
let desc = bucketDescriptors[j];
|
||||
for (let k = 0; k < desc.kinds.length; k++) {
|
||||
sum += buckets[i][desc.kinds[k]];
|
||||
}
|
||||
bucketData.push(graphHeight * sum / total);
|
||||
}
|
||||
bucketsGraph.push(bucketData);
|
||||
}
|
||||
|
||||
// Draw the graph into the buffer.
|
||||
let bucketWidth = width / bucketCount;
|
||||
let ctx = buffer.getContext('2d');
|
||||
for (let i = 0; i < bucketsGraph.length - 1; i++) {
|
||||
let bucketData = bucketsGraph[i];
|
||||
let nextBucketData = bucketsGraph[i + 1];
|
||||
for (let j = 0; j < bucketData.length; j++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i * bucketWidth, j && bucketData[j - 1]);
|
||||
ctx.lineTo((i + 1) * bucketWidth, j && nextBucketData[j - 1]);
|
||||
ctx.lineTo((i + 1) * bucketWidth, nextBucketData[j]);
|
||||
ctx.lineTo(i * bucketWidth, bucketData[j]);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = bucketDescriptors[j].color;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Remember stuff for later.
|
||||
this.buffer = buffer;
|
||||
|
||||
// Draw the buffer.
|
||||
this.drawSelection();
|
||||
|
||||
// (Re-)Populate the graph legend.
|
||||
while (this.legend.cells.length > 0) {
|
||||
this.legend.deleteCell(0);
|
||||
}
|
||||
let cell = this.legend.insertCell(-1);
|
||||
cell.textContent = "Legend: ";
|
||||
cell.style.padding = "1ex";
|
||||
for (let i = 0; i < bucketDescriptors.length; i++) {
|
||||
let cell = this.legend.insertCell(-1);
|
||||
cell.style.padding = "1ex";
|
||||
let desc = bucketDescriptors[i];
|
||||
let div = document.createElement("div");
|
||||
div.style.display = "inline-block";
|
||||
div.style.width = "0.6em";
|
||||
div.style.height = "1.2ex";
|
||||
div.style.backgroundColor = desc.color;
|
||||
div.style.borderStyle = "solid";
|
||||
div.style.borderWidth = "1px";
|
||||
div.style.borderColor = "Black";
|
||||
cell.appendChild(div);
|
||||
cell.appendChild(document.createTextNode(" " + desc.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HelpView {
|
||||
constructor() {
|
||||
this.element = $("help");
|
||||
}
|
||||
|
||||
render(newState) {
|
||||
this.element.style.display = newState.file ? "none" : "inherit";
|
||||
}
|
||||
}
|
@ -73,6 +73,7 @@ var tickProcessor = new TickProcessor(
|
||||
params.timedRange,
|
||||
params.pairwiseTimedRange,
|
||||
params.onlySummary,
|
||||
params.runtimeTimerFilter);
|
||||
params.runtimeTimerFilter,
|
||||
params.preprocessJson);
|
||||
tickProcessor.processLogFile(params.logFileName);
|
||||
tickProcessor.printStatistics();
|
||||
|
@ -50,7 +50,7 @@ function readFile(fileName) {
|
||||
try {
|
||||
return read(fileName);
|
||||
} catch (e) {
|
||||
print(fileName + ': ' + (e.message || e));
|
||||
printErr(fileName + ': ' + (e.message || e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -81,7 +81,9 @@ function TickProcessor(
|
||||
timedRange,
|
||||
pairwiseTimedRange,
|
||||
onlySummary,
|
||||
runtimeTimerFilter) {
|
||||
runtimeTimerFilter,
|
||||
preprocessJson) {
|
||||
this.preprocessJson = preprocessJson;
|
||||
LogReader.call(this, {
|
||||
'shared-library': { parsers: [null, parseInt, parseInt, parseInt],
|
||||
processor: this.processSharedLibrary },
|
||||
@ -149,10 +151,10 @@ function TickProcessor(
|
||||
var op = Profile.Operation;
|
||||
switch (operation) {
|
||||
case op.MOVE:
|
||||
print('Code move event for unknown code: 0x' + addr.toString(16));
|
||||
printErr('Code move event for unknown code: 0x' + addr.toString(16));
|
||||
break;
|
||||
case op.DELETE:
|
||||
print('Code delete event for unknown code: 0x' + addr.toString(16));
|
||||
printErr('Code delete event for unknown code: 0x' + addr.toString(16));
|
||||
break;
|
||||
case op.TICK:
|
||||
// Only unknown PCs (the first frame) are reported as unaccounted,
|
||||
@ -165,7 +167,11 @@ function TickProcessor(
|
||||
}
|
||||
};
|
||||
|
||||
this.profile_ = new V8Profile(separateIc);
|
||||
if (preprocessJson) {
|
||||
this.profile_ = new JsonProfile();
|
||||
} else {
|
||||
this.profile_ = new V8Profile(separateIc);
|
||||
}
|
||||
this.codeTypes_ = {};
|
||||
// Count each tick as a time unit.
|
||||
this.viewBuilder_ = new ViewBuilder(1);
|
||||
@ -204,7 +210,7 @@ TickProcessor.CALL_GRAPH_SIZE = 5;
|
||||
* @override
|
||||
*/
|
||||
TickProcessor.prototype.printError = function(str) {
|
||||
print(str);
|
||||
printErr(str);
|
||||
};
|
||||
|
||||
|
||||
@ -333,7 +339,9 @@ TickProcessor.prototype.processTick = function(pc,
|
||||
}
|
||||
}
|
||||
|
||||
this.profile_.recordTick(this.processStack(pc, tos_or_external_callback, stack));
|
||||
this.profile_.recordTick(
|
||||
ns_since_start, vmState,
|
||||
this.processStack(pc, tos_or_external_callback, stack));
|
||||
};
|
||||
|
||||
|
||||
@ -367,6 +375,11 @@ TickProcessor.prototype.processHeapSampleEnd = function(space, state) {
|
||||
|
||||
|
||||
TickProcessor.prototype.printStatistics = function() {
|
||||
if (this.preprocessJson) {
|
||||
this.profile_.writeJson();
|
||||
return;
|
||||
}
|
||||
|
||||
print('Statistical profiling result from ' + this.lastLogFileName_ +
|
||||
', (' + this.ticks_.total +
|
||||
' ticks, ' + this.ticks_.unaccounted + ' unaccounted, ' +
|
||||
@ -676,7 +689,7 @@ UnixCppEntriesProvider.prototype.parseNextLine = function() {
|
||||
function MacCppEntriesProvider(nmExec, targetRootFS) {
|
||||
UnixCppEntriesProvider.call(this, nmExec, targetRootFS);
|
||||
// Note an empty group. It is required, as UnixCppEntriesProvider expects 3 groups.
|
||||
this.FUNC_RE = /^([0-9a-fA-F]{8,16}) ()[iItT] (.*)$/;
|
||||
this.FUNC_RE = /^([0-9a-fA-F]{8,16})() (.*)$/;
|
||||
};
|
||||
inherits(MacCppEntriesProvider, UnixCppEntriesProvider);
|
||||
|
||||
@ -823,7 +836,9 @@ function ArgumentsProcessor(args) {
|
||||
'--pairwise-timed-range': ['pairwiseTimedRange', true,
|
||||
'Ignore ticks outside pairs of Date.now() calls'],
|
||||
'--only-summary': ['onlySummary', true,
|
||||
'Print only tick summary, exclude other information']
|
||||
'Print only tick summary, exclude other information'],
|
||||
'--preprocess': ['preprocessJson', true,
|
||||
'Preprocess for consumption with web interface']
|
||||
};
|
||||
this.argsDispatch_['--js'] = this.argsDispatch_['-j'];
|
||||
this.argsDispatch_['--gc'] = this.argsDispatch_['-g'];
|
||||
@ -841,6 +856,7 @@ ArgumentsProcessor.DEFAULTS = {
|
||||
callGraphSize: 5,
|
||||
ignoreUnknown: false,
|
||||
separateIc: false,
|
||||
preprocessJson: null,
|
||||
targetRootFS: '',
|
||||
nm: 'nm',
|
||||
range: 'auto,auto',
|
||||
|
Loading…
Reference in New Issue
Block a user