v8/tools/system-analyzer/map-processor.js
zeynepCankara e7357f1902 [tools] Add Indicium
Indicium is a new tool that integrates all our Map and IC processing
tools into one tool.

This CL does not attempt to cleanly integrate the Map Processor
and IC explorer, but provides an in initial starting point for further
integration work.

Bug: v8:10644
Change-Id: I753c116fd409c8c07613bf15f22e14aa1e8c8a0e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2259935
Commit-Queue: Sathya Gunasekaran  <gsathya@chromium.org>
Reviewed-by: Sathya Gunasekaran  <gsathya@chromium.org>
Reviewed-by: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#68605}
2020-06-30 09:51:27 +00:00

763 lines
18 KiB
JavaScript

// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ===========================================================================
const kChunkHeight = 250;
const kChunkWidth = 10;
function define(prototype, name, fn) {
Object.defineProperty(prototype, name, {value: fn, enumerable: false});
}
define(Array.prototype, 'max', function(fn) {
if (this.length === 0) return undefined;
if (fn === undefined) fn = (each) => each;
let max = fn(this[0]);
for (let i = 1; i < this.length; i++) {
max = Math.max(max, fn(this[i]));
}
return max;
})
define(Array.prototype, 'first', function() {
return this[0]
});
define(Array.prototype, 'last', function() {
return this[this.length - 1]
});
// ===========================================================================
class MapProcessor extends LogReader {
constructor() {
super();
this.dispatchTable_ = {
__proto__: null,
'code-creation': {
parsers: [
parseString, parseInt, parseInt, parseInt, parseInt, parseString,
parseVarArgs
],
processor: this.processCodeCreation
},
'code-move': {
parsers: [parseInt, parseInt],
'sfi-move':
{parsers: [parseInt, parseInt], processor: this.processCodeMove},
'code-delete': {parsers: [parseInt], processor: this.processCodeDelete},
processor: this.processFunctionMove
},
'map-create':
{parsers: [parseInt, parseString], processor: this.processMapCreate},
'map': {
parsers: [
parseString, parseInt, parseString, parseString, parseInt, parseInt,
parseString, parseString, parseString
],
processor: this.processMap
},
'map-details': {
parsers: [parseInt, parseString, parseString],
processor: this.processMapDetails
}
};
this.profile_ = new Profile();
this.timeline_ = new Timeline();
this.formatPCRegexp_ = /(.*):[0-9]+:[0-9]+$/;
}
printError(str) {
console.error(str);
throw str
}
processString(string) {
let end = string.length;
let current = 0;
let next = 0;
let line;
let i = 0;
let entry;
try {
while (current < end) {
next = string.indexOf('\n', current);
if (next === -1) break;
i++;
line = string.substring(current, next);
current = next + 1;
this.processLogLine(line);
}
} catch (e) {
console.error('Error occurred during parsing, trying to continue: ' + e);
}
return this.finalize();
}
processLogFile(fileName) {
this.collectEntries = true;
this.lastLogFileName_ = fileName;
let i = 1;
let line;
try {
while (line = readline()) {
this.processLogLine(line);
i++;
}
} catch (e) {
console.error(
'Error occurred during parsing line ' + i +
', trying to continue: ' + e);
}
return this.finalize();
}
finalize() {
// TODO(cbruni): print stats;
this.timeline_.finalize();
return this.timeline_;
}
addEntry(entry) {
this.entries.push(entry);
}
/**
* Parser for dynamic code optimization state.
*/
parseState(s) {
switch (s) {
case '':
return Profile.CodeState.COMPILED;
case '~':
return Profile.CodeState.OPTIMIZABLE;
case '*':
return Profile.CodeState.OPTIMIZED;
}
throw new Error('unknown code state: ' + s);
}
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
if (maybe_func.length) {
let funcAddr = parseInt(maybe_func[0]);
let state = this.parseState(maybe_func[1]);
this.profile_.addFuncCode(
type, name, timestamp, start, size, funcAddr, state);
} else {
this.profile_.addCode(type, name, timestamp, start, size);
}
}
processCodeMove(from, to) {
this.profile_.moveCode(from, to);
}
processCodeDelete(start) {
this.profile_.deleteCode(start);
}
processFunctionMove(from, to) {
this.profile_.moveFunc(from, to);
}
formatPC(pc, line, column) {
let entry = this.profile_.findEntry(pc);
if (!entry) return '<unknown>'
if (entry.type === 'Builtin') {
return entry.name;
}
let name = entry.func.getName();
let array = this.formatPCRegexp_.exec(name);
if (array === null) {
entry = name;
} else {
entry = entry.getState() + array[1];
}
return entry + ':' + line + ':' + column;
}
processMap(type, time, from, to, pc, line, column, reason, name) {
let time_ = parseInt(time);
if (type === 'Deprecate') return this.deprecateMap(type, time_, from);
let from_ = this.getExistingMap(from, time_);
let to_ = this.getExistingMap(to, time_);
let edge = new Edge(type, name, reason, time, from_, to_);
to_.filePosition = this.formatPC(pc, line, column);
edge.finishSetup();
}
deprecateMap(type, time, id) {
this.getExistingMap(id, time).deprecate();
}
processMapCreate(time, id) {
// map-create events might override existing maps if the addresses get
// recycled. Hence we do not check for existing maps.
let map = this.createMap(id, time);
}
processMapDetails(time, id, string) {
// TODO(cbruni): fix initial map logging.
let map = this.getExistingMap(id, time);
map.description = string;
}
createMap(id, time) {
let map = new V8Map(id, time);
this.timeline_.push(map);
return map;
}
getExistingMap(id, time) {
if (id === '0x000000000000') return undefined;
let map = V8Map.get(id, time);
if (map === undefined) {
console.error('No map details provided: id=' + id);
// Manually patch in a map to continue running.
return this.createMap(id, time);
};
return map;
}
}
// ===========================================================================
class V8Map {
constructor(id, time = -1) {
if (!id) throw 'Invalid ID';
this.id = id;
this.time = time;
if (!(time > 0)) throw 'Invalid time';
this.description = '';
this.edge = void 0;
this.children = [];
this.depth = 0;
this._isDeprecated = false;
this.deprecationTargets = null;
V8Map.set(id, this);
this.leftId = 0;
this.rightId = 0;
this.filePosition = '';
}
finalizeRootMap(id) {
let stack = [this];
while (stack.length > 0) {
let current = stack.pop();
if (current.leftId !== 0) {
console.error('Skipping potential parent loop between maps:', current)
continue;
}
current.finalize(id)
id += 1;
current.children.forEach(edge => stack.push(edge.to))
// TODO implement rightId
}
return id;
}
finalize(id) {
// Initialize preorder tree traversal Ids for fast subtree inclusion checks
if (id <= 0) throw 'invalid id';
let currentId = id;
this.leftId = currentId
}
parent() {
if (this.edge === void 0) return void 0;
return this.edge.from;
}
isDeprecated() {
return this._isDeprecated;
}
deprecate() {
this._isDeprecated = true;
}
isRoot() {
return this.edge === void 0 || this.edge.from === void 0;
}
contains(map) {
return this.leftId < map.leftId && map.rightId < this.rightId;
}
addEdge(edge) {
this.children.push(edge);
}
chunkIndex(chunks) {
// Did anybody say O(n)?
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
if (chunk.isEmpty()) continue;
if (chunk.last().time < this.time) continue;
return i;
}
return -1;
}
position(chunks) {
let index = this.chunkIndex(chunks);
let xFrom = (index + 0.5) * kChunkWidth;
let yFrom = kChunkHeight - chunks[index].yOffset(this);
return [xFrom, yFrom];
}
transitions() {
let transitions = Object.create(null);
let current = this;
while (current) {
let edge = current.edge;
if (edge && edge.isTransition()) {
transitions[edge.name] = edge;
}
current = current.parent()
}
return transitions;
}
getType() {
return this.edge === void 0 ? 'new' : this.edge.type;
}
isBootstrapped() {
return this.edge === void 0;
}
getParents() {
let parents = [];
let current = this.parent();
while (current) {
parents.push(current);
current = current.parent();
}
return parents;
}
static get(id, time = undefined) {
let maps = this.cache.get(id);
if (maps) {
for (let i = 0; i < maps.length; i++) {
// TODO: Implement time based map search
if (maps[i].time === time) {
return maps[i];
}
}
// default return the latest
return maps[maps.length - 1];
}
}
static set(id, map) {
if (this.cache.has(id)) {
this.cache.get(id).push(map);
} else {
this.cache.set(id, [map]);
}
}
}
V8Map.cache = new Map();
// ===========================================================================
class Edge {
constructor(type, name, reason, time, from, to) {
this.type = type;
this.name = name;
this.reason = reason;
this.time = time;
this.from = from;
this.to = to;
}
finishSetup() {
let from = this.from;
if (from) from.addEdge(this);
let to = this.to;
if (to === undefined) return;
to.edge = this;
if (from === undefined) return;
if (to === from) throw 'From and to must be distinct.';
if (to.time < from.time) {
console.error('invalid time order');
}
let newDepth = from.depth + 1;
if (to.depth > 0 && to.depth != newDepth) {
console.error('Depth has already been initialized');
}
to.depth = newDepth;
}
chunkIndex(chunks) {
// Did anybody say O(n)?
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
if (chunk.isEmpty()) continue;
if (chunk.last().time < this.time) continue;
return i;
}
return -1;
}
parentEdge() {
if (!this.from) return undefined;
return this.from.edge;
}
chainLength() {
let length = 0;
let prev = this;
while (prev) {
prev = this.parent;
length++;
}
return length;
}
isTransition() {
return this.type === 'Transition'
}
isFastToSlow() {
return this.type === 'Normalize'
}
isSlowToFast() {
return this.type === 'SlowToFast'
}
isInitial() {
return this.type === 'InitialMap'
}
isBootstrapped() {
return this.type === 'new'
}
isReplaceDescriptors() {
return this.type === 'ReplaceDescriptors'
}
isCopyAsPrototype() {
return this.reason === 'CopyAsPrototype'
}
isOptimizeAsPrototype() {
return this.reason === 'OptimizeAsPrototype'
}
symbol() {
if (this.isTransition()) return '+';
if (this.isFastToSlow()) return '⊡';
if (this.isSlowToFast()) return '⊛';
if (this.isReplaceDescriptors()) {
if (this.name) return '+';
return '∥';
}
return '';
}
toString() {
let s = this.symbol();
if (this.isTransition()) return s + this.name;
if (this.isFastToSlow()) return s + this.reason;
if (this.isCopyAsPrototype()) return s + 'Copy as Prototype';
if (this.isOptimizeAsPrototype()) {
return s + 'Optimize as Prototype';
}
if (this.isReplaceDescriptors() && this.name) {
return this.type + ' ' + this.symbol() + this.name;
}
return this.type + ' ' + (this.reason ? this.reason : '') + ' ' +
(this.name ? this.name : '')
}
}
// ===========================================================================
class Marker {
constructor(time, name) {
this.time = parseInt(time);
this.name = name;
}
}
// ===========================================================================
class Timeline {
constructor() {
this.values = [];
this.transitions = new Map();
this.markers = [];
this.startTime = 0;
this.endTime = 0;
}
push(map) {
let time = map.time;
if (!this.isEmpty() && this.last().time > time) {
// Invalid insertion order, might happen without --single-process,
// finding insertion point.
let insertionPoint = this.find(time);
this.values.splice(insertionPoint, map);
} else {
this.values.push(map);
}
if (time > 0) {
this.endTime = Math.max(this.endTime, time);
if (this.startTime === 0) {
this.startTime = time;
} else {
this.startTime = Math.min(this.startTime, time);
}
}
}
addMarker(time, message) {
this.markers.push(new Marker(time, message));
}
finalize() {
let id = 0;
this.forEach(map => {
if (map.isRoot()) id = map.finalizeRootMap(id + 1);
if (map.edge && map.edge.name) {
let edge = map.edge;
let list = this.transitions.get(edge.name);
if (list === undefined) {
this.transitions.set(edge.name, [edge]);
} else {
list.push(edge);
}
}
});
this.markers.sort((a, b) => b.time - a.time);
}
at(index) {
return this.values[index]
}
isEmpty() {
return this.size() === 0
}
size() {
return this.values.length
}
first() {
return this.values.first()
}
last() {
return this.values.last()
}
duration() {
return this.last().time - this.first().time
}
forEachChunkSize(count, fn) {
const increment = this.duration() / count;
let currentTime = this.first().time + increment;
let index = 0;
for (let i = 0; i < count; i++) {
let nextIndex = this.find(currentTime, index);
let nextTime = currentTime + increment;
fn(index, nextIndex, currentTime, nextTime);
index = nextIndex
currentTime = nextTime;
}
}
chunkSizes(count) {
let chunks = [];
this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
return chunks;
}
chunks(count) {
let chunks = [];
let emptyMarkers = [];
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
let items = this.values.slice(start, end);
let markers = this.markersAt(startTime, endTime);
chunks.push(new Chunk(chunks.length, startTime, endTime, items, markers));
});
return chunks;
}
range(start, end) {
const first = this.find(start);
if (first < 0) return [];
const last = this.find(end, first);
return this.values.slice(first, last);
}
find(time, offset = 0) {
return this.basicFind(this.values, each => each.time - time, offset);
}
markersAt(startTime, endTime) {
let start = this.basicFind(this.markers, each => each.time - startTime);
let end = this.basicFind(this.markers, each => each.time - endTime, start);
return this.markers.slice(start, end);
}
basicFind(array, cmp, offset = 0) {
let min = offset;
let max = array.length;
while (min < max) {
let mid = min + Math.floor((max - min) / 2);
let result = cmp(array[mid]);
if (result > 0) {
max = mid - 1;
} else {
min = mid + 1;
}
}
return min;
}
count(filter) {
return this.values.reduce((sum, each) => {
return sum + (filter(each) === true ? 1 : 0);
}, 0);
}
filter(predicate) {
return this.values.filter(predicate);
}
filterUniqueTransitions(filter) {
// Returns a list of Maps whose parent is not in the list.
return this.values.filter(map => {
if (filter(map) === false) return false;
let parent = map.parent();
if (parent === undefined) return true;
return filter(parent) === false;
});
}
depthHistogram() {
return this.values.histogram(each => each.depth);
}
fanOutHistogram() {
return this.values.histogram(each => each.children.length);
}
forEach(fn) {
return this.values.forEach(fn)
}
}
// ===========================================================================
class Chunk {
constructor(index, start, end, items, markers) {
this.index = index;
this.start = start;
this.end = end;
this.items = items;
this.markers = markers;
this.height = 0;
}
isEmpty() {
return this.items.length === 0;
}
last() {
return this.at(this.size() - 1);
}
first() {
return this.at(0);
}
at(index) {
return this.items[index];
}
size() {
return this.items.length;
}
yOffset(map) {
// items[0] == oldest map, displayed at the top of the chunk
// items[n-1] == youngest map, displayed at the bottom of the chunk
return (1 - (this.indexOf(map) + 0.5) / this.size()) * this.height;
}
indexOf(map) {
return this.items.indexOf(map);
}
has(map) {
if (this.isEmpty()) return false;
return this.first().time <= map.time && map.time <= this.last().time;
}
next(chunks) {
return this.findChunk(chunks, 1);
}
prev(chunks) {
return this.findChunk(chunks, -1);
}
findChunk(chunks, delta) {
let i = this.index + delta;
let chunk = chunks[i];
while (chunk && chunk.size() === 0) {
i += delta;
chunk = chunks[i]
}
return chunk;
}
getTransitionBreakdown() {
return BreakDown(this.items, map => map.getType())
}
getUniqueTransitions() {
// Filter out all the maps that have parents within the same chunk.
return this.items.filter(map => !map.parent() || !this.has(map.parent()));
}
}
// ===========================================================================
function BreakDown(list, map_fn) {
if (map_fn === void 0) {
map_fn = each => each;
}
let breakdown = {__proto__: null};
list.forEach(each => {
let type = map_fn(each);
let v = breakdown[type];
breakdown[type] = (v | 0) + 1
});
return Object.entries(breakdown).sort((a, b) => a[1] - b[1]);
}
// ===========================================================================
class ArgumentsProcessor extends BaseArgumentsProcessor {
getArgsDispatch() {
return {
'--range':
['range', 'auto,auto', 'Specify the range limit as [start],[end]'],
'--source-map': [
'sourceMap', null,
'Specify the source map that should be used for output'
]
};
}
getDefaultResults() {
return {
logFileName: 'v8.log',
range: 'auto,auto',
};
}
}