| // 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. |
| |
| // =========================================================================== |
| class MapProcessor extends LogReader { |
| constructor() { |
| super(); |
| this.dispatchTable_ = { |
| '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(); |
| } |
| |
| 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 line; |
| while (line = readline()) { |
| this.processLogLine(line); |
| } |
| 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 re = /(.*):[0-9]+:[0-9]+$/; |
| let array = re.exec(name); |
| if (!array) { |
| entry = name; |
| } else { |
| entry = entry.getState() + array[1]; |
| } |
| return entry + ":" + line + ":" + column; |
| } |
| |
| processMap(type, time, from, to, pc, line, column, reason, name) { |
| time = parseInt(time); |
| if (type == "Deprecate") return this.deprecateMap(type, time, from); |
| from = this.getExistingMap(from, time); |
| 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 = ""; |
| } |
| |
| finalize(id) { |
| // Initialize preorder tree traversal Ids for fast subtree inclusion checks |
| if (id <= 0) throw "invalid id"; |
| let currentId = id; |
| this.leftId = currentId |
| this.children.forEach(edge => { |
| let map = edge.to; |
| currentId = map.finalize(currentId + 1); |
| }); |
| this.rightId = currentId + 1; |
| return currentId + 1; |
| } |
| |
| 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) { |
| if (!this.cache) return undefined; |
| return this.cache.get(id); |
| } |
| |
| static set(id, map) { |
| if (!this.cache) this.cache = new Map(); |
| this.cache.set(id, 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() { |
| if (this.from) this.from.addEdge(this); |
| if (this.to) { |
| this.to.edge = this; |
| if (this.to === this.from) throw "From and to must be distinct."; |
| if (this.from) { |
| if (this.to.time < this.from.time) { |
| console.error("invalid time order"); |
| } |
| let newDepth = this.from.depth + 1; |
| if (this.to.depth > 0 && this.to.depth != newDepth) { |
| console.error("Depth has already been initialized"); |
| } |
| this.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.finalize(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) ? 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)) return false; |
| let parent = map.parent(); |
| if (!parent) return true; |
| return !filter(parent); |
| }); |
| } |
| |
| 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', |
| }; |
| } |
| } |