| // 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"; | |
| /** | |
| * A thin wrapper around shell's 'read' function showing a file name on error. | |
| */ | |
| function readFile(fileName) { | |
| try { | |
| return read(fileName); | |
| } catch (e) { | |
| console.log(fileName + ': ' + (e.message || e)); | |
| throw e; | |
| } | |
| } | |
| // =========================================================================== | |
| // This is the only true formatting, why? For an international audience the | |
| // confusion between the decimal and thousands separator is big (alternating | |
| // between comma "," vs dot "."). The Swiss formatting uses "'" as a thousands | |
| // separator, dropping most of that confusion. | |
| const numberFormat = new Intl.NumberFormat('de-CH', { | |
| maximumFractionDigits: 2, | |
| minimumFractionDigits: 2, | |
| }); | |
| function formatNumber(value) { | |
| return numberFormat.format(value); | |
| } | |
| function BYTES(bytes, total) { | |
| let units = ['B ', 'kB', 'mB', 'gB']; | |
| let unitIndex = 0; | |
| let value = bytes; | |
| while (value > 1000 && unitIndex < units.length) { | |
| value /= 1000; | |
| unitIndex++; | |
| } | |
| let result = formatNumber(value).padStart(10) + ' ' + units[unitIndex]; | |
| if (total !== void 0 && total != 0) { | |
| result += PERCENT(bytes, total).padStart(5); | |
| } | |
| return result; | |
| } | |
| function PERCENT(value, total) { | |
| return Math.round(value / total * 100) + "%"; | |
| } | |
| // =========================================================================== | |
| const kNoTimeMetrics = { | |
| __proto__: null, | |
| executionDuration: 0, | |
| firstEventTimestamp: 0, | |
| firstParseEventTimestamp: 0, | |
| lastParseEventTimestamp: 0, | |
| lastEventTimestamp: 0 | |
| }; | |
| class CompilationUnit { | |
| constructor() { | |
| this.isEval = false; | |
| // Lazily computed properties. | |
| this.firstEventTimestamp = -1; | |
| this.firstParseEventTimestamp = -1; | |
| this.firstCompileEventTimestamp = -1; | |
| this.lastParseEventTimestamp = -1; | |
| this.lastEventTimestamp = -1; | |
| this.deserializationTimestamp = -1; | |
| this.preparseTimestamp = -1; | |
| this.parseTimestamp = -1; | |
| this.parse2Timestamp = -1; | |
| this.resolutionTimestamp = -1; | |
| this.compileTimestamp = -1; | |
| this.lazyCompileTimestamp = -1; | |
| this.executionTimestamp = -1; | |
| this.optimizationTimestamp = -1; | |
| this.deserializationDuration = -0.0; | |
| this.preparseDuration = -0.0; | |
| this.parseDuration = -0.0; | |
| this.parse2Duration = -0.0; | |
| this.resolutionDuration = -0.0; | |
| this.scopeResolutionDuration = -0.0; | |
| this.lazyCompileDuration = -0.0; | |
| this.compileDuration = -0.0; | |
| this.optimizeDuration = -0.0; | |
| this.ownBytes = -1; | |
| this.compilationCacheHits = []; | |
| } | |
| finalize() { | |
| this.firstEventTimestamp = this.timestampMin( | |
| this.deserializationTimestamp, this.parseTimestamp, | |
| this.preparseTimestamp, this.resolutionTimestamp, | |
| this.executionTimestamp); | |
| this.firstParseEventTimestamp = this.timestampMin( | |
| this.deserializationTimestamp, this.parseTimestamp, | |
| this.preparseTimestamp, this.resolutionTimestamp); | |
| this.firstCompileEventTimestamp = this.rawTimestampMin( | |
| this.deserializationTimestamp, this.compileTimestamp, | |
| this.lazyCompileTimestamp); | |
| // Any excuted script needs to be compiled. | |
| if (this.hasBeenExecuted() && | |
| (this.firstCompileEventTimestamp <= 0 || | |
| this.executionTimestamp < this.firstCompileTimestamp)) { | |
| console.error('Compile < execution timestamp', this); | |
| } | |
| if (this.ownBytes < 0) console.error(this, 'Own bytes must be positive'); | |
| } | |
| hasBeenExecuted() { | |
| return this.executionTimestamp > 0; | |
| } | |
| addCompilationCacheHit(timestamp) { | |
| this.compilationCacheHits.push(timestamp); | |
| } | |
| // Returns the smallest timestamp from the given list, ignoring | |
| // uninitialized (-1) values. | |
| rawTimestampMin(...timestamps) { | |
| timestamps = timestamps.length == 1 ? timestamps[0] : timestamps; | |
| let result = timestamps.reduce((min, item) => { | |
| return item == -1 ? min : (min == -1 ? item : Math.min(item, item)); | |
| }, -1); | |
| return result; | |
| } | |
| timestampMin(...timestamps) { | |
| let result = this.rawTimestampMin(...timestamps); | |
| if (Number.isNaN(result) || result < 0) { | |
| console.error( | |
| 'Invalid timestamp min:', {result, timestamps, script: this}); | |
| return 0; | |
| } | |
| return result; | |
| } | |
| timestampMax(...timestamps) { | |
| timestamps = timestamps.length == 1 ? timestamps[0] : timestamps; | |
| let result = Math.max(...timestamps); | |
| if (Number.isNaN(result) || result < 0) { | |
| console.error( | |
| 'Invalid timestamp max:', {result, timestamps, script: this}); | |
| return 0; | |
| } | |
| return result; | |
| } | |
| } | |
| // =========================================================================== | |
| class Script extends CompilationUnit { | |
| constructor(id) { | |
| super(); | |
| if (id === void 0 || id <= 0) { | |
| throw new Error(`Invalid id=${id} for script`); | |
| } | |
| this.file = ''; | |
| this.id = id; | |
| this.isNative = false; | |
| this.isBackgroundCompiled = false; | |
| this.isStreamingCompiled = false; | |
| this.funktions = []; | |
| this.metrics = new Map(); | |
| this.maxNestingLevel = 0; | |
| this.width = 0; | |
| this.bytesTotal = -1; | |
| this.finalized = false; | |
| this.summary = ''; | |
| this.source = ''; | |
| } | |
| setFile(name) { | |
| this.file = name; | |
| this.isNative = name.startsWith('native '); | |
| } | |
| isEmpty() { | |
| return this.funktions.length === 0; | |
| } | |
| getFunktionAtStartPosition(start) { | |
| if (!this.isEval && start === 0) { | |
| throw 'position 0 is reserved for the script'; | |
| } | |
| if (this.finalized) { | |
| return this.funktions.find(funktion => funktion.start == start); | |
| } | |
| return this.funktions[start]; | |
| } | |
| // Return the innermost function at the given source position. | |
| getFunktionForPosition(position) { | |
| if (!this.finalized) throw 'Incomplete script'; | |
| for (let i = this.funktions.length - 1; i >= 0; i--) { | |
| let funktion = this.funktions[i]; | |
| if (funktion.containsPosition(position)) return funktion; | |
| } | |
| return undefined; | |
| } | |
| addMissingFunktions(list) { | |
| if (this.finalized) throw 'script is finalized!'; | |
| list.forEach(fn => { | |
| if (this.funktions[fn.start] === void 0) { | |
| this.addFunktion(fn); | |
| } | |
| }); | |
| } | |
| addFunktion(fn) { | |
| if (this.finalized) throw 'script is finalized!'; | |
| if (fn.start === void 0) throw "Funktion has no start position"; | |
| if (this.funktions[fn.start] !== void 0) { | |
| fn.print(); | |
| throw "adding same function twice to script"; | |
| } | |
| this.funktions[fn.start] = fn; | |
| } | |
| finalize() { | |
| this.finalized = true; | |
| // Compact funktions as we no longer need access via start byte position. | |
| this.funktions = this.funktions.filter(each => true); | |
| let parent = null; | |
| let maxNesting = 0; | |
| // Iterate over the Funktions in byte position order. | |
| this.funktions.forEach(fn => { | |
| fn.isEval = this.isEval; | |
| if (parent === null) { | |
| parent = fn; | |
| } else { | |
| // Walk up the nested chain of Funktions to find the parent. | |
| while (parent !== null && !fn.isNestedIn(parent)) { | |
| parent = parent.parent; | |
| } | |
| fn.parent = parent; | |
| if (parent) { | |
| maxNesting = Math.max(maxNesting, parent.addNestedFunktion(fn)); | |
| } | |
| parent = fn; | |
| } | |
| }); | |
| // Sanity checks to ensure that scripts are executed and parsed before any | |
| // of its funktions. | |
| let funktionFirstParseEventTimestamp = -1; | |
| // Second iteration step to finalize the funktions once the proper | |
| // hierarchy has been set up. | |
| this.funktions.forEach(fn => { | |
| fn.finalize(); | |
| funktionFirstParseEventTimestamp = this.timestampMin( | |
| funktionFirstParseEventTimestamp, fn.firstParseEventTimestamp); | |
| this.lastParseEventTimestamp = this.timestampMax( | |
| this.lastParseEventTimestamp, fn.lastParseEventTimestamp); | |
| this.lastEventTimestamp = | |
| this.timestampMax(this.lastEventTimestamp, fn.lastEventTimestamp); | |
| }); | |
| this.maxNestingLevel = maxNesting; | |
| // Initialize sizes. | |
| if (!this.ownBytes === -1) throw 'Invalid state'; | |
| if (this.funktions.length == 0) { | |
| this.bytesTotal = this.ownBytes = 0; | |
| return; | |
| } | |
| let toplevelFunktionBytes = this.funktions.reduce( | |
| (bytes, each) => bytes + (each.isToplevel() ? each.getBytes() : 0), 0); | |
| if (this.isDeserialized || this.isEval || this.isStreamingCompiled) { | |
| if (this.getBytes() === -1) { | |
| this.bytesTotal = toplevelFunktionBytes; | |
| } | |
| } | |
| this.ownBytes = this.bytesTotal - toplevelFunktionBytes; | |
| // Initialize common properties. | |
| super.finalize(); | |
| // Sanity checks after the minimum timestamps have been computed. | |
| if (funktionFirstParseEventTimestamp < this.firstParseEventTimestamp) { | |
| console.error( | |
| 'invalid firstCompileEventTimestamp', this, | |
| funktionFirstParseEventTimestamp, this.firstParseEventTimestamp); | |
| } | |
| } | |
| print() { | |
| console.log(this.toString()); | |
| } | |
| toString() { | |
| let str = `SCRIPT id=${this.id} file=${this.file}\n` + | |
| `functions[${this.funktions.length}]:`; | |
| this.funktions.forEach(fn => str += fn.toString()); | |
| return str; | |
| } | |
| getBytes() { | |
| return this.bytesTotal; | |
| } | |
| getOwnBytes() { | |
| return this.ownBytes; | |
| } | |
| // Also see Funktion.prototype.getMetricBytes | |
| getMetricBytes(name) { | |
| if (name == 'lazyCompileTimestamp') return this.getOwnBytes(); | |
| return this.getOwnBytes(); | |
| } | |
| getMetricDuration(name) { | |
| return this[name]; | |
| } | |
| forEach(fn) { | |
| fn(this); | |
| this.funktions.forEach(fn); | |
| } | |
| // Container helper for TotalScript / Script. | |
| getScripts() { | |
| return [this]; | |
| } | |
| calculateMetrics(printSummary) { | |
| let log = (str) => this.summary += str + '\n'; | |
| log("SCRIPT: " + this.id); | |
| let all = this.funktions; | |
| if (all.length === 0) return; | |
| let nofFunktions = all.length; | |
| let ownBytesSum = list => { | |
| return list.reduce((bytes, each) => bytes + each.getOwnBytes(), 0) | |
| }; | |
| let info = (name, funktions) => { | |
| let ownBytes = ownBytesSum(funktions); | |
| let nofPercent = Math.round(funktions.length / nofFunktions * 100); | |
| let value = (funktions.length + "").padStart(6) + | |
| (nofPercent + "%").padStart(5) + | |
| BYTES(ownBytes, this.bytesTotal).padStart(10); | |
| log((" - " + name).padEnd(20) + value); | |
| this.metrics.set(name + "-bytes", ownBytes); | |
| this.metrics.set(name + "-count", funktions.length); | |
| this.metrics.set(name + "-count-percent", nofPercent); | |
| this.metrics.set(name + "-bytes-percent", | |
| Math.round(ownBytes / this.bytesTotal * 100)); | |
| }; | |
| log(" - file: " + this.file); | |
| log(' - details: ' + | |
| 'isEval=' + this.isEval + ' deserialized=' + this.isDeserialized + | |
| ' streamed=' + this.isStreamingCompiled); | |
| info("scripts", this.getScripts()); | |
| info("functions", all); | |
| info("toplevel fn", all.filter(each => each.isToplevel())); | |
| info('preparsed', all.filter(each => each.preparseDuration > 0)); | |
| info('fully parsed', all.filter(each => each.parseDuration > 0)); | |
| // info("fn parsed", all.filter(each => each.parse2Duration > 0)); | |
| // info("resolved", all.filter(each => each.resolutionDuration > 0)); | |
| info("executed", all.filter(each => each.executionTimestamp > 0)); | |
| info('forEval', all.filter(each => each.isEval)); | |
| info("lazy compiled", all.filter(each => each.lazyCompileTimestamp > 0)); | |
| info("eager compiled", all.filter(each => each.compileTimestamp > 0)); | |
| let parsingCost = | |
| new ExecutionCost('parse', all, each => each.parseDuration); | |
| parsingCost.setMetrics(this.metrics); | |
| log(parsingCost.toString()); | |
| let preParsingCost = | |
| new ExecutionCost('preparse', all, each => each.preparseDuration); | |
| preParsingCost.setMetrics(this.metrics); | |
| log(preParsingCost.toString()); | |
| let resolutionCost = | |
| new ExecutionCost('resolution', all, each => each.resolutionDuration); | |
| resolutionCost.setMetrics(this.metrics); | |
| log(resolutionCost.toString()); | |
| let nesting = new NestingDistribution(all); | |
| nesting.setMetrics(this.metrics); | |
| log(nesting.toString()); | |
| if (printSummary) console.log(this.summary); | |
| } | |
| getAccumulatedTimeMetrics( | |
| metrics, start, end, delta, cumulative = true, useDuration = false) { | |
| // Returns an array of the following format: | |
| // [ [start, acc(metric0, start, start), acc(metric1, ...), ...], | |
| // [start+delta, acc(metric0, start, start+delta), ...], | |
| // [start+delta*2, acc(metric0, start, start+delta*2), ...], | |
| // ... | |
| // ] | |
| if (end <= start) throw 'Invalid ranges [' + start + ',' + end + ']'; | |
| const timespan = end - start; | |
| const kSteps = Math.ceil(timespan / delta); | |
| // To reduce the time spent iterating over the funktions of this script | |
| // we iterate once over all funktions and add the metric changes to each | |
| // timepoint: | |
| // [ [0, 300, ...], [1, 15, ...], [2, 100, ...], [3, 0, ...] ... ] | |
| // In a second step we accumulate all values: | |
| // [ [0, 300, ...], [1, 315, ...], [2, 415, ...], [3, 415, ...] ... ] | |
| // | |
| // To limit the number of data points required in the resulting graphs, | |
| // only the rows for entries with actual changes are created. | |
| const metricProperties = ["time"]; | |
| metrics.forEach(each => { | |
| metricProperties.push(each + 'Timestamp'); | |
| if (useDuration) metricProperties.push(each + 'Duration'); | |
| }); | |
| // Create a packed {rowTemplate} which is copied later-on. | |
| let indexToTime = (t) => (start + t * delta) / kSecondsToMillis; | |
| let rowTemplate = [indexToTime(0)]; | |
| for (let i = 1; i < metricProperties.length; i++) rowTemplate.push(0.0); | |
| // Create rows with 0-time entry. | |
| let rows = new Array(rowTemplate.slice()); | |
| for (let t = 1; t <= kSteps; t++) rows.push(null); | |
| // Create the real metric's property name on the Funktion object. | |
| // Add the increments of each Funktion's metric to the result. | |
| this.forEach(funktionOrScript => { | |
| // Iterate over the Funktion's metric names, skipping position 0 which | |
| // is the time. | |
| const kMetricIncrement = useDuration ? 2 : 1; | |
| for (let i = 1; i < metricProperties.length; i += kMetricIncrement) { | |
| let timestampPropertyName = metricProperties[i]; | |
| let timestamp = funktionOrScript[timestampPropertyName]; | |
| if (timestamp === void 0) continue; | |
| if (timestamp < start || end < timestamp) continue; | |
| timestamp -= start; | |
| let index = Math.floor(timestamp / delta); | |
| let row = rows[index]; | |
| if (row === null) { | |
| // Add a new row if it didn't exist, | |
| row = rows[index] = rowTemplate.slice(); | |
| // .. add the time offset. | |
| row[0] = indexToTime(index); | |
| } | |
| // Add the metric value. | |
| row[i] += funktionOrScript.getMetricBytes(timestampPropertyName); | |
| if (!useDuration) continue; | |
| let durationPropertyName = metricProperties[i + 1]; | |
| row[i + 1] += funktionOrScript.getMetricDuration(durationPropertyName); | |
| } | |
| }); | |
| // Create a packed array again with only the valid entries. | |
| // Accumulate the incremental results by adding the metric values from | |
| // the previous time window. | |
| let previous = rows[0]; | |
| let result = [previous]; | |
| for (let t = 1; t < rows.length; t++) { | |
| let current = rows[t]; | |
| if (current === null) { | |
| // Ensure a zero data-point after each non-zero point. | |
| if (!cumulative && rows[t - 1] !== null) { | |
| let duplicate = rowTemplate.slice(); | |
| duplicate[0] = indexToTime(t); | |
| result.push(duplicate); | |
| } | |
| continue; | |
| } | |
| if (cumulative) { | |
| // Skip i==0 where the corresponding time value in seconds is. | |
| for (let i = 1; i < metricProperties.length; i++) { | |
| current[i] += previous[i]; | |
| } | |
| } | |
| // Make sure we have a data-point in time right before the current one. | |
| if (rows[t - 1] === null) { | |
| let duplicate = (!cumulative ? rowTemplate : previous).slice(); | |
| duplicate[0] = indexToTime(t - 1); | |
| result.push(duplicate); | |
| } | |
| previous = current; | |
| result.push(current); | |
| } | |
| // Make sure there is an entry at the last position to make sure all graphs | |
| // have the same width. | |
| const lastIndex = rows.length - 1; | |
| if (rows[lastIndex] === null) { | |
| let duplicate = previous.slice(); | |
| duplicate[0] = indexToTime(lastIndex); | |
| result.push(duplicate); | |
| } | |
| return result; | |
| } | |
| getFunktionsAtTime(time, delta, metric) { | |
| // Returns a list of Funktions whose metric changed in the | |
| // [time-delta, time+delta] range. | |
| return this.funktions.filter( | |
| funktion => funktion.didMetricChange(time, delta, metric)); | |
| return result; | |
| } | |
| } | |
| class TotalScript extends Script { | |
| constructor() { | |
| super('all files', 'all files'); | |
| this.scripts = []; | |
| } | |
| addAllFunktions(script) { | |
| // funktions is indexed by byte offset and as such not packed. Add every | |
| // Funktion one by one to keep this.funktions packed. | |
| script.funktions.forEach(fn => this.funktions.push(fn)); | |
| this.scripts.push(script); | |
| this.bytesTotal += script.bytesTotal; | |
| } | |
| // Iterate over all Scripts and nested Funktions. | |
| forEach(fn) { | |
| this.scripts.forEach(script => script.forEach(fn)); | |
| } | |
| getScripts() { | |
| return this.scripts; | |
| } | |
| } | |
| // =========================================================================== | |
| class NestingDistribution { | |
| constructor(funktions) { | |
| // Stores the nof bytes per function nesting level. | |
| this.accumulator = [0, 0, 0, 0, 0]; | |
| // Max nof bytes encountered at any nesting level. | |
| this.max = 0; | |
| // avg bytes per nesting level. | |
| this.avg = 0; | |
| this.totalBytes = 0; | |
| funktions.forEach(each => each.accumulateNestingLevel(this.accumulator)); | |
| this.max = this.accumulator.reduce((max, each) => Math.max(max, each), 0); | |
| this.totalBytes = this.accumulator.reduce((sum, each) => sum + each, 0); | |
| for (let i = 0; i < this.accumulator.length; i++) { | |
| this.avg += this.accumulator[i] * i; | |
| } | |
| this.avg /= this.totalBytes; | |
| } | |
| print() { | |
| console.log(this.toString()) | |
| } | |
| toString() { | |
| let ticks = " ▁▂▃▄▅▆▇█"; | |
| let accString = this.accumulator.reduce((str, each) => { | |
| let index = Math.round(each / this.max * (ticks.length - 1)); | |
| return str + ticks[index]; | |
| }, ''); | |
| let percent0 = this.accumulator[0] | |
| let percent1 = this.accumulator[1]; | |
| let percent2plus = this.accumulator.slice(2) | |
| .reduce((sum, each) => sum + each, 0); | |
| return " - nesting level: " + | |
| ' avg=' + formatNumber(this.avg) + | |
| ' l0=' + PERCENT(percent0, this.totalBytes) + | |
| ' l1=' + PERCENT(percent1, this.totalBytes) + | |
| ' l2+=' + PERCENT(percent2plus, this.totalBytes) + | |
| ' distribution=[' + accString + ']'; | |
| } | |
| setMetrics(dict) {} | |
| } | |
| class ExecutionCost { | |
| constructor(prefix, funktions, time_fn) { | |
| this.prefix = prefix; | |
| // Time spent on executed functions. | |
| this.executedCost = 0 | |
| // Time spent on not executed functions. | |
| this.nonExecutedCost = 0; | |
| this.executedCost = funktions.reduce((sum, each) => { | |
| return sum + (each.hasBeenExecuted() ? time_fn(each) : 0) | |
| }, 0); | |
| this.nonExecutedCost = funktions.reduce((sum, each) => { | |
| return sum + (each.hasBeenExecuted() ? 0 : time_fn(each)) | |
| }, 0); | |
| } | |
| print() { | |
| console.log(this.toString()) | |
| } | |
| toString() { | |
| return (' - ' + this.prefix + '-time:').padEnd(24) + | |
| (" executed=" + formatNumber(this.executedCost) + 'ms').padEnd(20) + | |
| " non-executed=" + formatNumber(this.nonExecutedCost) + 'ms'; | |
| } | |
| setMetrics(dict) { | |
| dict.set('parseMetric', this.executionCost); | |
| dict.set('parseMetricNegative', this.nonExecutionCost); | |
| } | |
| } | |
| // =========================================================================== | |
| class Funktion extends CompilationUnit { | |
| constructor(name, start, end, script) { | |
| super(); | |
| if (start < 0) throw "invalid start position: " + start; | |
| if (script.isEval) { | |
| if (end < start) throw 'invalid start end positions'; | |
| } else { | |
| if (end <= 0) throw 'invalid end position: ' + end; | |
| if (end <= start) throw 'invalid start end positions'; | |
| } | |
| this.name = name; | |
| this.start = start; | |
| this.end = end; | |
| this.script = script; | |
| this.parent = null; | |
| this.nested = []; | |
| this.nestingLevel = 0; | |
| if (script) this.script.addFunktion(this); | |
| } | |
| finalize() { | |
| this.lastParseEventTimestamp = Math.max( | |
| this.preparseTimestamp + this.preparseDuration, | |
| this.parseTimestamp + this.parseDuration, | |
| this.resolutionTimestamp + this.resolutionDuration); | |
| if (!(this.lastParseEventTimestamp > 0)) this.lastParseEventTimestamp = 0; | |
| this.lastEventTimestamp = | |
| Math.max(this.lastParseEventTimestamp, this.executionTimestamp); | |
| if (!(this.lastEventTimestamp > 0)) this.lastEventTimestamp = 0; | |
| this.ownBytes = this.nested.reduce( | |
| (bytes, each) => bytes - each.getBytes(), this.getBytes()); | |
| super.finalize(); | |
| } | |
| getMetricBytes(name) { | |
| if (name == 'lazyCompileTimestamp') return this.getOwnBytes(); | |
| return this.getOwnBytes(); | |
| } | |
| getMetricDuration(name) { | |
| if (name in kNoTimeMetrics) return 0; | |
| return this[name]; | |
| } | |
| isNestedIn(funktion) { | |
| if (this.script != funktion.script) throw "Incompatible script"; | |
| return funktion.start < this.start && this.end <= funktion.end; | |
| } | |
| isToplevel() { | |
| return this.parent === null; | |
| } | |
| containsPosition(position) { | |
| return this.start <= position && position <= this.end; | |
| } | |
| accumulateNestingLevel(accumulator) { | |
| let value = accumulator[this.nestingLevel] || 0; | |
| accumulator[this.nestingLevel] = value + this.getOwnBytes(); | |
| } | |
| addNestedFunktion(child) { | |
| if (this.script != child.script) throw "Incompatible script"; | |
| if (child == null) throw "Nesting non child"; | |
| this.nested.push(child); | |
| if (this.nested.length > 1) { | |
| // Make sure the nested children don't overlap and have been inserted in | |
| // byte start position order. | |
| let last = this.nested[this.nested.length - 2]; | |
| if (last.end > child.start || last.start > child.start || | |
| last.end > child.end || last.start > child.end) { | |
| throw "Wrongly nested child added"; | |
| } | |
| } | |
| child.nestingLevel = this.nestingLevel + 1; | |
| return child.nestingLevel; | |
| } | |
| getBytes() { | |
| return this.end - this.start; | |
| } | |
| getOwnBytes() { | |
| return this.ownBytes; | |
| } | |
| didMetricChange(time, delta, name) { | |
| let value = this[name + 'Timestamp']; | |
| return (time - delta) <= value && value <= (time + delta); | |
| } | |
| print() { | |
| console.log(this.toString()); | |
| } | |
| toString(details = true) { | |
| let result = 'function' + (this.name ? ' ' + this.name : '') + | |
| `() range=${this.start}-${this.end}`; | |
| if (details) result += ` script=${this.script ? this.script.id : 'X'}`; | |
| return result; | |
| } | |
| } | |
| // =========================================================================== | |
| const kTimestampFactor = 1000; | |
| const kSecondsToMillis = 1000; | |
| function toTimestamp(microseconds) { | |
| return microseconds / kTimestampFactor | |
| } | |
| function startOf(timestamp, time) { | |
| let result = toTimestamp(timestamp) - time; | |
| if (result < 0) throw "start timestamp cannnot be negative"; | |
| return result; | |
| } | |
| class ParseProcessor extends LogReader { | |
| constructor() { | |
| super(); | |
| this.dispatchTable_ = { | |
| // Avoid accidental leaking of __proto__ properties and force this object | |
| // to be in dictionary-mode. | |
| __proto__: null, | |
| // "function",{event type}, | |
| // {script id},{start position},{end position},{time},{timestamp}, | |
| // {function name} | |
| 'function': { | |
| parsers: [ | |
| parseString, parseInt, parseInt, parseInt, parseFloat, parseInt, | |
| parseString | |
| ], | |
| processor: this.processFunctionEvent | |
| }, | |
| // "compilation-cache","hit"|"put",{type},{scriptid},{start position}, | |
| // {end position},{timestamp} | |
| 'compilation-cache': { | |
| parsers: | |
| [parseString, parseString, parseInt, parseInt, parseInt, parseInt], | |
| processor: this.processCompilationCacheEvent | |
| }, | |
| 'script': { | |
| parsers: [parseString, parseInt, parseInt], | |
| processor: this.processScriptEvent | |
| }, | |
| // "script-details", {script_id}, {file}, {line}, {column}, {size} | |
| 'script-details': { | |
| parsers: [parseInt, parseString, parseInt, parseInt, parseInt], | |
| processor: this.processScriptDetails | |
| }, | |
| 'script-source': { | |
| parsers: [parseInt, parseString, parseString], | |
| processor: this.processScriptSource | |
| }, | |
| }; | |
| this.functionEventDispatchTable_ = { | |
| // Avoid accidental leaking of __proto__ properties and force this object | |
| // to be in dictionary-mode. | |
| __proto__: null, | |
| 'full-parse': this.processFull.bind(this), | |
| 'parse-function': this.processParseFunction.bind(this), | |
| // TODO(cbruni): make sure arrow functions emit a normal parse-function | |
| // event. | |
| 'parse': this.processParseFunction.bind(this), | |
| 'parse-script': this.processParseScript.bind(this), | |
| 'parse-eval': this.processParseEval.bind(this), | |
| 'preparse-no-resolution': this.processPreparseNoResolution.bind(this), | |
| 'preparse-resolution': this.processPreparseResolution.bind(this), | |
| 'first-execution': this.processFirstExecution.bind(this), | |
| 'compile-lazy': this.processCompileLazy.bind(this), | |
| 'compile': this.processCompile.bind(this), | |
| 'compile-eval': this.processCompileEval.bind(this), | |
| 'optimize-lazy': this.processOptimizeLazy.bind(this), | |
| 'deserialize': this.processDeserialize.bind(this), | |
| }; | |
| this.idToScript = new Map(); | |
| this.fileToScript = new Map(); | |
| this.nameToFunction = new Map(); | |
| this.scripts = []; | |
| this.totalScript = new TotalScript(); | |
| this.firstEventTimestamp = -1; | |
| this.lastParseEventTimestamp = -1; | |
| this.lastEventTimestamp = -1; | |
| } | |
| print() { | |
| console.log("scripts:"); | |
| this.idToScript.forEach(script => script.print()); | |
| } | |
| processString(string) { | |
| let end = string.length; | |
| let current = 0; | |
| let next = 0; | |
| let line; | |
| let i = 0; | |
| let entry; | |
| while (current < end) { | |
| next = string.indexOf("\n", current); | |
| if (next === -1) break; | |
| i++; | |
| line = string.substring(current, next); | |
| current = next + 1; | |
| this.processLogLine(line); | |
| } | |
| this.postProcess(); | |
| } | |
| processLogFile(fileName) { | |
| this.collectEntries = true | |
| this.lastLogFileName_ = fileName; | |
| var line; | |
| while (line = readline()) { | |
| this.processLogLine(line); | |
| } | |
| this.postProcess(); | |
| } | |
| postProcess() { | |
| this.scripts = Array.from(this.idToScript.values()) | |
| .filter(each => !each.isNative); | |
| this.scripts.forEach(script => { | |
| script.finalize(); | |
| script.calculateMetrics(false) | |
| }); | |
| this.scripts.forEach(script => this.totalScript.addAllFunktions(script)); | |
| this.totalScript.calculateMetrics(true); | |
| this.firstEventTimestamp = this.totalScript.timestampMin( | |
| this.scripts.map(each => each.firstEventTimestamp)); | |
| this.lastParseEventTimestamp = this.totalScript.timestampMax( | |
| this.scripts.map(each => each.lastParseEventTimestamp)); | |
| this.lastEventTimestamp = this.totalScript.timestampMax( | |
| this.scripts.map(each => each.lastEventTimestamp)); | |
| const series = { | |
| firstParseEvent: 'Any Parse Event', | |
| parse: 'Parsing', | |
| preparse: 'Preparsing', | |
| resolution: 'Preparsing with Var. Resolution', | |
| lazyCompile: 'Lazy Compilation', | |
| compile: 'Eager Compilation', | |
| execution: 'First Execution', | |
| }; | |
| let metrics = Object.keys(series); | |
| this.totalScript.getAccumulatedTimeMetrics( | |
| metrics, 0, this.lastEventTimestamp, 10); | |
| } | |
| processFunctionEvent( | |
| eventName, scriptId, startPosition, endPosition, duration, timestamp, | |
| functionName) { | |
| let handlerFn = this.functionEventDispatchTable_[eventName]; | |
| if (handlerFn === undefined) { | |
| console.error('Couldn\'t find handler for function event:' + eventName); | |
| } | |
| handlerFn( | |
| scriptId, startPosition, endPosition, duration, timestamp, | |
| functionName); | |
| } | |
| addEntry(entry) { | |
| this.entries.push(entry); | |
| } | |
| lookupScript(id) { | |
| return this.idToScript.get(id); | |
| } | |
| getOrCreateFunction( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| if (scriptId == -1) { | |
| return this.lookupFunktionByRange(startPosition, endPosition); | |
| } | |
| let script = this.lookupScript(scriptId); | |
| let funktion = script.getFunktionAtStartPosition(startPosition); | |
| if (funktion === void 0) { | |
| funktion = new Funktion(functionName, startPosition, endPosition, script); | |
| } | |
| return funktion; | |
| } | |
| // Iterates over all functions and tries to find matching ones. | |
| lookupFunktionsByRange(start, end) { | |
| let results = []; | |
| this.idToScript.forEach(script => { | |
| script.forEach(funktion => { | |
| if (funktion.startPostion == start && funktion.endPosition == end) { | |
| results.push(funktion); | |
| } | |
| }); | |
| }); | |
| return results; | |
| } | |
| lookupFunktionByRange(start, end) { | |
| let results = this.lookupFunktionsByRange(start, end); | |
| if (results.length != 1) throw "Could not find unique function by range"; | |
| return results[0]; | |
| } | |
| processScriptEvent(eventName, scriptId, timestamp) { | |
| let script = this.idToScript.get(scriptId); | |
| switch (eventName) { | |
| case 'create': | |
| case 'reserve-id': | |
| case 'deserialize': { | |
| if (script !== undefined) return; | |
| script = new Script(scriptId); | |
| this.idToScript.set(scriptId, script); | |
| if (eventName == 'deserialize') { | |
| script.deserializationTimestamp = toTimestamp(timestamp); | |
| } | |
| return; | |
| } | |
| case 'background-compile': | |
| if (script.isBackgroundCompiled) { | |
| throw 'Cannot background-compile twice'; | |
| } | |
| script.isBackgroundCompiled = true; | |
| // TODO(cbruni): remove once backwards compatibility is no longer needed. | |
| script.isStreamingCompiled = true; | |
| // TODO(cbruni): fix parse events for background compilation scripts | |
| script.preparseTimestamp = toTimestamp(timestamp); | |
| return; | |
| case 'streaming-compile': | |
| if (script.isStreamingCompiled) throw 'Cannot stream-compile twice'; | |
| // TODO(cbruni): remove once backwards compatibility is no longer needed. | |
| script.isBackgroundCompiled = true; | |
| script.isStreamingCompiled = true; | |
| // TODO(cbruni): fix parse events for background compilation scripts | |
| script.preparseTimestamp = toTimestamp(timestamp); | |
| return; | |
| default: | |
| console.error('Unhandled script event: ' + eventName); | |
| } | |
| } | |
| processScriptDetails(scriptId, file, startLine, startColumn, size) { | |
| let script = this.lookupScript(scriptId); | |
| script.setFile(file); | |
| } | |
| processScriptSource(scriptId, url, source) { | |
| let script = this.lookupScript(scriptId); | |
| script.source = source; | |
| } | |
| processParseEval( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| if (startPosition != 0 && startPosition != -1) { | |
| console.error('Invalid start position for parse-eval', arguments); | |
| } | |
| let script = this.processParseScript(...arguments); | |
| script.isEval = true; | |
| } | |
| processFull( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| if (startPosition == 0) { | |
| // This should only happen for eval. | |
| let script = this.lookupScript(scriptId); | |
| script.isEval = true; | |
| return; | |
| } | |
| let funktion = this.getOrCreateFunction(...arguments); | |
| // TODO(cbruni): this should never happen, emit differen event from the | |
| // parser. | |
| if (funktion.parseTimestamp > 0) return; | |
| funktion.parseTimestamp = startOf(timestamp, duration); | |
| funktion.parseDuration = duration; | |
| } | |
| processParseFunction( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let funktion = this.getOrCreateFunction(...arguments); | |
| funktion.parseTimestamp = startOf(timestamp, duration); | |
| funktion.parseDuration = duration; | |
| } | |
| processParseScript( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| // TODO timestamp and duration | |
| let script = this.lookupScript(scriptId); | |
| let ts = startOf(timestamp, duration); | |
| script.parseTimestamp = ts; | |
| script.parseDuration = duration; | |
| return script; | |
| } | |
| processPreparseResolution( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let funktion = this.getOrCreateFunction(...arguments); | |
| // TODO(cbruni): this should never happen, emit different event from the | |
| // parser. | |
| if (funktion.resolutionTimestamp > 0) return; | |
| funktion.resolutionTimestamp = startOf(timestamp, duration); | |
| funktion.resolutionDuration = duration; | |
| } | |
| processPreparseNoResolution( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let funktion = this.getOrCreateFunction(...arguments); | |
| funktion.preparseTimestamp = startOf(timestamp, duration); | |
| funktion.preparseDuration = duration; | |
| } | |
| processFirstExecution( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let script = this.lookupScript(scriptId); | |
| if (startPosition === 0) { | |
| // undefined = eval fn execution | |
| if (script) { | |
| script.executionTimestamp = toTimestamp(timestamp); | |
| } | |
| } else { | |
| let funktion = script.getFunktionAtStartPosition(startPosition); | |
| if (funktion) { | |
| funktion.executionTimestamp = toTimestamp(timestamp); | |
| } else { | |
| // TODO(cbruni): handle funktions from compilation-cache hits. | |
| } | |
| } | |
| } | |
| processCompileLazy( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let funktion = this.getOrCreateFunction(...arguments); | |
| funktion.lazyCompileTimestamp = startOf(timestamp, duration); | |
| funktion.lazyCompileDuration = duration; | |
| } | |
| processCompile( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let script = this.lookupScript(scriptId); | |
| if (startPosition === 0) { | |
| script.compileTimestamp = startOf(timestamp, duration); | |
| script.compileDuration = duration; | |
| script.bytesTotal = endPosition; | |
| return script; | |
| } else { | |
| let funktion = script.getFunktionAtStartPosition(startPosition); | |
| if (funktion === undefined) { | |
| // This should not happen since any funktion has to be parsed first. | |
| console.error('processCompile funktion not found', ...arguments); | |
| return; | |
| } | |
| funktion.compileTimestamp = startOf(timestamp, duration); | |
| funktion.compileDuration = duration; | |
| return funktion; | |
| } | |
| } | |
| processCompileEval( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let compilationUnit = this.processCompile(...arguments); | |
| compilationUnit.isEval = true; | |
| } | |
| processOptimizeLazy( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let compilationUnit = this.lookupScript(scriptId); | |
| if (startPosition > 0) { | |
| compilationUnit = | |
| compilationUnit.getFunktionAtStartPosition(startPosition); | |
| if (compilationUnit === undefined) { | |
| // This should not happen since any funktion has to be parsed first. | |
| console.error('processOptimizeLazy funktion not found', ...arguments); | |
| return; | |
| } | |
| } | |
| compilationUnit.optimizationTimestamp = startOf(timestamp, duration); | |
| compilationUnit.optimizationDuration = duration; | |
| } | |
| processDeserialize( | |
| scriptId, startPosition, endPosition, duration, timestamp, functionName) { | |
| let compilationUnit = this.lookupScript(scriptId); | |
| if (startPosition === 0) { | |
| compilationUnit.bytesTotal = endPosition; | |
| } else { | |
| compilationUnit = this.getOrCreateFunction(...arguments); | |
| } | |
| compilationUnit.deserializationTimestamp = startOf(timestamp, duration); | |
| compilationUnit.deserializationDuration = duration; | |
| } | |
| processCompilationCacheEvent( | |
| eventType, cacheType, scriptId, startPosition, endPosition, timestamp) { | |
| if (eventType !== 'hit') return; | |
| let compilationUnit = this.lookupScript(scriptId); | |
| if (startPosition > 0) { | |
| compilationUnit = | |
| compilationUnit.getFunktionAtStartPosition(startPosition); | |
| } | |
| compilationUnit.addCompilationCacheHit(toTimestamp(timestamp)); | |
| } | |
| } | |
| class ArgumentsProcessor extends BaseArgumentsProcessor { | |
| getArgsDispatch() { | |
| return {}; | |
| } | |
| getDefaultResults() { | |
| return { | |
| logFileName: 'v8.log', | |
| range: 'auto,auto', | |
| }; | |
| } | |
| } |