| // 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. |
| |
| // =========================================================================== |
| 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, parseInt, parseString], |
| processor: this.processMapCreate |
| }, |
| 'map': { |
| parsers: [parseString, parseInt, parseInt, parseInt, parseInt, parseInt, |
| parseString, parseString, parseString |
| ], |
| processor: this.processMap |
| }, |
| 'map-details': { |
| parsers: [parseInt, parseInt, 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, string) { |
| // map-create events might override existing maps if the addresses get |
| // rcycled. Hence we do not check for existing maps. |
| let map = this.createMap(id, time); |
| map.description = string; |
| } |
| |
| processMapDetails(time, id, string) { |
| //TODO(cbruni): fix initial map logging. |
| let map = this.getExistingMap(id, time); |
| if (!map.description) { |
| //map.description = string; |
| } |
| } |
| |
| createMap(id, time) { |
| let map = new V8Map(id, time); |
| this.timeline_.push(map); |
| return map; |
| } |
| |
| getExistingMap(id, time) { |
| if (id === 0) return undefined; |
| let map = V8Map.get(id); |
| 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) { |
| return this.cache.get(id); |
| } |
| |
| static set(id, map) { |
| 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', |
| }; |
| } |
| } |