// 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. import { LogReader, parseString, parseVarArgs } from "./logreader.mjs"; import { BaseArgumentsProcessor } from "./arguments.mjs"; import { Profile } from "./profile.mjs"; // =========================================================================== 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] }); /** * A thin wrapper around shell's 'read' function showing a file name on error. */ export function readFile(fileName) { try { return read(fileName); } catch (e) { console.log(fileName + ': ' + (e.message || e)); throw e; } } // =========================================================================== export 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 "" 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]); } // =========================================================================== export 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', }; } }