• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4"use strict";
5
6
7/**
8 * A thin wrapper around shell's 'read' function showing a file name on error.
9 */
10function readFile(fileName) {
11  try {
12    return read(fileName);
13  } catch (e) {
14    console.log(fileName + ': ' + (e.message || e));
15    throw e;
16  }
17}
18
19// ===========================================================================
20
21// This is the only true formatting, why? For an international audience the
22// confusion between the decimal and thousands separator is big (alternating
23// between comma "," vs dot "."). The Swiss formatting uses "'" as a thousands
24// separator, dropping most of that confusion.
25const numberFormat = new Intl.NumberFormat('de-CH', {
26  maximumFractionDigits: 2,
27  minimumFractionDigits: 2,
28});
29
30function formatNumber(value) {
31  return numberFormat.format(value);
32}
33
34function BYTES(bytes, total) {
35  let units = ['B ', 'kB', 'mB', 'gB'];
36  let unitIndex = 0;
37  let value = bytes;
38  while (value > 1000 && unitIndex < units.length) {
39    value /= 1000;
40    unitIndex++;
41  }
42  let result = formatNumber(value).padStart(10) + ' ' + units[unitIndex];
43  if (total !== void 0 && total != 0) {
44    result += PERCENT(bytes, total).padStart(5);
45  }
46  return result;
47}
48
49function PERCENT(value, total) {
50  return Math.round(value / total * 100) + "%";
51}
52
53// ===========================================================================
54const kNoTimeMetrics = {
55  __proto__: null,
56  executionDuration: 0,
57  firstEventTimestamp: 0,
58  firstParseEventTimestamp: 0,
59  lastParseEventTimestamp: 0,
60  lastEventTimestamp: 0
61};
62
63class CompilationUnit {
64  constructor() {
65    this.isEval = false;
66
67    // Lazily computed properties.
68    this.firstEventTimestamp = -1;
69    this.firstParseEventTimestamp = -1;
70    this.firstCompileEventTimestamp = -1;
71    this.lastParseEventTimestamp = -1;
72    this.lastEventTimestamp = -1;
73    this.deserializationTimestamp = -1;
74
75    this.preparseTimestamp = -1;
76    this.parseTimestamp = -1;
77    this.parse2Timestamp = -1;
78    this.resolutionTimestamp = -1;
79    this.compileTimestamp = -1;
80    this.lazyCompileTimestamp = -1;
81    this.executionTimestamp = -1;
82    this.optimizationTimestamp = -1;
83
84    this.deserializationDuration = -0.0;
85    this.preparseDuration = -0.0;
86    this.parseDuration = -0.0;
87    this.parse2Duration = -0.0;
88    this.resolutionDuration = -0.0;
89    this.scopeResolutionDuration = -0.0;
90    this.lazyCompileDuration = -0.0;
91    this.compileDuration = -0.0;
92    this.optimizeDuration = -0.0;
93
94    this.ownBytes = -1;
95    this.compilationCacheHits = [];
96  }
97
98  finalize() {
99    this.firstEventTimestamp = this.timestampMin(
100        this.deserializationTimestamp, this.parseTimestamp,
101        this.preparseTimestamp, this.resolutionTimestamp,
102        this.executionTimestamp);
103
104    this.firstParseEventTimestamp = this.timestampMin(
105        this.deserializationTimestamp, this.parseTimestamp,
106        this.preparseTimestamp, this.resolutionTimestamp);
107
108    this.firstCompileEventTimestamp = this.rawTimestampMin(
109        this.deserializationTimestamp, this.compileTimestamp,
110        this.lazyCompileTimestamp);
111    // Any excuted script needs to be compiled.
112    if (this.hasBeenExecuted() &&
113        (this.firstCompileEventTimestamp <= 0 ||
114         this.executionTimestamp < this.firstCompileTimestamp)) {
115      console.error('Compile < execution timestamp', this);
116    }
117
118    if (this.ownBytes < 0) console.error(this, 'Own bytes must be positive');
119  }
120
121  hasBeenExecuted() {
122    return this.executionTimestamp > 0;
123  }
124
125  addCompilationCacheHit(timestamp) {
126    this.compilationCacheHits.push(timestamp);
127  }
128
129  // Returns the smallest timestamp from the given list, ignoring
130  // uninitialized (-1) values.
131  rawTimestampMin(...timestamps) {
132    timestamps = timestamps.length == 1 ? timestamps[0] : timestamps;
133    let result = timestamps.reduce((min, item) => {
134      return item == -1 ? min : (min == -1 ? item : Math.min(item, item));
135    }, -1);
136    return result;
137  }
138  timestampMin(...timestamps) {
139    let result = this.rawTimestampMin(...timestamps);
140    if (Number.isNaN(result) || result < 0) {
141      console.error(
142          'Invalid timestamp min:', {result, timestamps, script: this});
143      return 0;
144    }
145    return result;
146  }
147
148  timestampMax(...timestamps) {
149    timestamps = timestamps.length == 1 ? timestamps[0] : timestamps;
150    let result = Math.max(...timestamps);
151    if (Number.isNaN(result) || result < 0) {
152      console.error(
153          'Invalid timestamp max:', {result, timestamps, script: this});
154      return 0;
155    }
156    return result;
157  }
158}
159
160// ===========================================================================
161class Script extends CompilationUnit {
162  constructor(id) {
163    super();
164    if (id === void 0 || id <= 0) {
165      throw new Error(`Invalid id=${id} for script`);
166    }
167    this.file = '';
168    this.id = id;
169
170    this.isNative = false;
171    this.isBackgroundCompiled = false;
172    this.isStreamingCompiled = false;
173
174    this.funktions = [];
175    this.metrics = new Map();
176    this.maxNestingLevel = 0;
177
178    this.width = 0;
179    this.bytesTotal = -1;
180    this.finalized = false;
181    this.summary = '';
182    this.source = '';
183  }
184
185  setFile(name) {
186    this.file = name;
187    this.isNative = name.startsWith('native ');
188  }
189
190  isEmpty() {
191    return this.funktions.length === 0;
192  }
193
194  getFunktionAtStartPosition(start) {
195    if (!this.isEval && start === 0) {
196      throw 'position 0 is reserved for the script';
197    }
198    if (this.finalized) {
199      return this.funktions.find(funktion => funktion.start == start);
200    }
201    return this.funktions[start];
202  }
203
204  // Return the innermost function at the given source position.
205  getFunktionForPosition(position) {
206    if (!this.finalized) throw 'Incomplete script';
207    for (let i = this.funktions.length - 1; i >= 0; i--) {
208      let funktion = this.funktions[i];
209      if (funktion.containsPosition(position)) return funktion;
210    }
211    return undefined;
212  }
213
214  addMissingFunktions(list) {
215    if (this.finalized) throw 'script is finalized!';
216    list.forEach(fn => {
217      if (this.funktions[fn.start] === void 0) {
218        this.addFunktion(fn);
219      }
220    });
221  }
222
223  addFunktion(fn) {
224    if (this.finalized) throw 'script is finalized!';
225    if (fn.start === void 0) throw "Funktion has no start position";
226    if (this.funktions[fn.start] !== void 0) {
227      fn.print();
228      throw "adding same function twice to script";
229    }
230    this.funktions[fn.start] = fn;
231  }
232
233  finalize() {
234    this.finalized = true;
235    // Compact funktions as we no longer need access via start byte position.
236    this.funktions = this.funktions.filter(each => true);
237    let parent = null;
238    let maxNesting = 0;
239    // Iterate over the Funktions in byte position order.
240    this.funktions.forEach(fn => {
241      fn.isEval = this.isEval;
242      if (parent === null) {
243        parent = fn;
244      } else {
245        // Walk up the nested chain of Funktions to find the parent.
246        while (parent !== null && !fn.isNestedIn(parent)) {
247          parent = parent.parent;
248        }
249        fn.parent = parent;
250        if (parent) {
251          maxNesting = Math.max(maxNesting, parent.addNestedFunktion(fn));
252        }
253        parent = fn;
254      }
255    });
256    // Sanity checks to ensure that scripts are executed and parsed before any
257    // of its funktions.
258    let funktionFirstParseEventTimestamp = -1;
259    // Second iteration step to finalize the funktions once the proper
260    // hierarchy has been set up.
261    this.funktions.forEach(fn => {
262      fn.finalize();
263
264      funktionFirstParseEventTimestamp = this.timestampMin(
265          funktionFirstParseEventTimestamp, fn.firstParseEventTimestamp);
266
267      this.lastParseEventTimestamp = this.timestampMax(
268          this.lastParseEventTimestamp, fn.lastParseEventTimestamp);
269
270      this.lastEventTimestamp =
271          this.timestampMax(this.lastEventTimestamp, fn.lastEventTimestamp);
272    });
273    this.maxNestingLevel = maxNesting;
274
275    // Initialize sizes.
276    if (!this.ownBytes === -1) throw 'Invalid state';
277    if (this.funktions.length == 0) {
278      this.bytesTotal = this.ownBytes = 0;
279      return;
280    }
281    let toplevelFunktionBytes = this.funktions.reduce(
282        (bytes, each) => bytes + (each.isToplevel() ? each.getBytes() : 0), 0);
283    if (this.isDeserialized || this.isEval || this.isStreamingCompiled) {
284      if (this.getBytes() === -1) {
285        this.bytesTotal = toplevelFunktionBytes;
286      }
287    }
288    this.ownBytes = this.bytesTotal - toplevelFunktionBytes;
289    // Initialize common properties.
290    super.finalize();
291    // Sanity checks after the minimum timestamps have been computed.
292    if (funktionFirstParseEventTimestamp < this.firstParseEventTimestamp) {
293      console.error(
294          'invalid firstCompileEventTimestamp', this,
295          funktionFirstParseEventTimestamp, this.firstParseEventTimestamp);
296    }
297  }
298
299  print() {
300    console.log(this.toString());
301  }
302
303  toString() {
304    let str = `SCRIPT id=${this.id} file=${this.file}\n` +
305      `functions[${this.funktions.length}]:`;
306    this.funktions.forEach(fn => str += fn.toString());
307    return str;
308  }
309
310  getBytes() {
311    return this.bytesTotal;
312  }
313
314  getOwnBytes() {
315    return this.ownBytes;
316  }
317
318  // Also see Funktion.prototype.getMetricBytes
319  getMetricBytes(name) {
320    if (name == 'lazyCompileTimestamp') return this.getOwnBytes();
321    return this.getOwnBytes();
322  }
323
324  getMetricDuration(name) {
325    return this[name];
326  }
327
328  forEach(fn) {
329    fn(this);
330    this.funktions.forEach(fn);
331  }
332
333  // Container helper for TotalScript / Script.
334  getScripts() {
335    return [this];
336  }
337
338  calculateMetrics(printSummary) {
339    let log = (str) => this.summary += str + '\n';
340    log("SCRIPT: " + this.id);
341    let all = this.funktions;
342    if (all.length === 0) return;
343
344    let nofFunktions = all.length;
345    let ownBytesSum = list => {
346      return list.reduce((bytes, each) => bytes + each.getOwnBytes(), 0)
347    };
348
349    let info = (name, funktions) => {
350      let ownBytes = ownBytesSum(funktions);
351      let nofPercent = Math.round(funktions.length / nofFunktions * 100);
352      let value = (funktions.length + "").padStart(6) +
353        (nofPercent + "%").padStart(5) +
354        BYTES(ownBytes, this.bytesTotal).padStart(10);
355      log(("  - " + name).padEnd(20) + value);
356      this.metrics.set(name + "-bytes", ownBytes);
357      this.metrics.set(name + "-count", funktions.length);
358      this.metrics.set(name + "-count-percent", nofPercent);
359      this.metrics.set(name + "-bytes-percent",
360        Math.round(ownBytes / this.bytesTotal * 100));
361    };
362
363    log("  - file:         " + this.file);
364    log('  - details:      ' +
365        'isEval=' + this.isEval + ' deserialized=' + this.isDeserialized +
366        ' streamed=' + this.isStreamingCompiled);
367    info("scripts", this.getScripts());
368    info("functions", all);
369    info("toplevel fn", all.filter(each => each.isToplevel()));
370    info('preparsed', all.filter(each => each.preparseDuration > 0));
371
372    info('fully parsed', all.filter(each => each.parseDuration > 0));
373    // info("fn parsed", all.filter(each => each.parse2Duration > 0));
374    // info("resolved", all.filter(each => each.resolutionDuration > 0));
375    info("executed", all.filter(each => each.executionTimestamp > 0));
376    info('forEval', all.filter(each => each.isEval));
377    info("lazy compiled", all.filter(each => each.lazyCompileTimestamp > 0));
378    info("eager compiled", all.filter(each => each.compileTimestamp > 0));
379
380    let parsingCost =
381        new ExecutionCost('parse', all, each => each.parseDuration);
382    parsingCost.setMetrics(this.metrics);
383    log(parsingCost.toString());
384
385    let preParsingCost =
386        new ExecutionCost('preparse', all, each => each.preparseDuration);
387    preParsingCost.setMetrics(this.metrics);
388    log(preParsingCost.toString());
389
390    let resolutionCost =
391        new ExecutionCost('resolution', all, each => each.resolutionDuration);
392    resolutionCost.setMetrics(this.metrics);
393    log(resolutionCost.toString());
394
395    let nesting = new NestingDistribution(all);
396    nesting.setMetrics(this.metrics);
397    log(nesting.toString());
398
399    if (printSummary) console.log(this.summary);
400  }
401
402  getAccumulatedTimeMetrics(
403      metrics, start, end, delta, cumulative = true, useDuration = false) {
404    // Returns an array of the following format:
405    // [ [start,         acc(metric0, start, start), acc(metric1, ...), ...],
406    //   [start+delta,   acc(metric0, start, start+delta), ...],
407    //   [start+delta*2, acc(metric0, start, start+delta*2), ...],
408    //   ...
409    // ]
410    if (end <= start) throw 'Invalid ranges [' + start + ',' + end + ']';
411    const timespan = end - start;
412    const kSteps = Math.ceil(timespan / delta);
413    // To reduce the time spent iterating over the funktions of this script
414    // we iterate once over all funktions and add the metric changes to each
415    // timepoint:
416    // [ [0, 300, ...], [1,  15, ...], [2, 100, ...], [3,   0, ...] ... ]
417    // In a second step we accumulate all values:
418    // [ [0, 300, ...], [1, 315, ...], [2, 415, ...], [3, 415, ...] ... ]
419    //
420    // To limit the number of data points required in the resulting graphs,
421    // only the rows for entries with actual changes are created.
422
423    const metricProperties = ["time"];
424    metrics.forEach(each => {
425      metricProperties.push(each + 'Timestamp');
426      if (useDuration) metricProperties.push(each + 'Duration');
427    });
428    // Create a packed {rowTemplate} which is copied later-on.
429    let indexToTime = (t) => (start + t * delta) / kSecondsToMillis;
430    let rowTemplate = [indexToTime(0)];
431    for (let i = 1; i < metricProperties.length; i++) rowTemplate.push(0.0);
432    // Create rows with 0-time entry.
433    let rows = new Array(rowTemplate.slice());
434    for (let t = 1; t <= kSteps; t++) rows.push(null);
435    // Create the real metric's property name on the Funktion object.
436    // Add the increments of each Funktion's metric to the result.
437    this.forEach(funktionOrScript => {
438      // Iterate over the Funktion's metric names, skipping position 0 which
439      // is the time.
440      const kMetricIncrement = useDuration ? 2 : 1;
441      for (let i = 1; i < metricProperties.length; i += kMetricIncrement) {
442        let timestampPropertyName = metricProperties[i];
443        let timestamp = funktionOrScript[timestampPropertyName];
444        if (timestamp === void 0) continue;
445        if (timestamp < start || end < timestamp) continue;
446        timestamp -= start;
447        let index = Math.floor(timestamp / delta);
448        let row = rows[index];
449        if (row === null) {
450          // Add a new row if it didn't exist,
451          row = rows[index] = rowTemplate.slice();
452          // .. add the time offset.
453          row[0] = indexToTime(index);
454        }
455        // Add the metric value.
456        row[i] += funktionOrScript.getMetricBytes(timestampPropertyName);
457        if (!useDuration) continue;
458        let durationPropertyName = metricProperties[i + 1];
459        row[i + 1] += funktionOrScript.getMetricDuration(durationPropertyName);
460      }
461    });
462    // Create a packed array again with only the valid entries.
463    // Accumulate the incremental results by adding the metric values from
464    // the previous time window.
465    let previous = rows[0];
466    let result = [previous];
467    for (let t = 1; t < rows.length; t++) {
468      let current = rows[t];
469      if (current === null) {
470        // Ensure a zero data-point after each non-zero point.
471        if (!cumulative && rows[t - 1] !== null) {
472          let duplicate = rowTemplate.slice();
473          duplicate[0] = indexToTime(t);
474          result.push(duplicate);
475        }
476        continue;
477      }
478      if (cumulative) {
479        // Skip i==0 where the corresponding time value in seconds is.
480        for (let i = 1; i < metricProperties.length; i++) {
481          current[i] += previous[i];
482        }
483      }
484      // Make sure we have a data-point in time right before the current one.
485      if (rows[t - 1] === null) {
486        let duplicate = (!cumulative ? rowTemplate : previous).slice();
487        duplicate[0] = indexToTime(t - 1);
488        result.push(duplicate);
489      }
490      previous = current;
491      result.push(current);
492    }
493    // Make sure there is an entry at the last position to make sure all graphs
494    // have the same width.
495    const lastIndex = rows.length - 1;
496    if (rows[lastIndex] === null) {
497      let duplicate = previous.slice();
498      duplicate[0] = indexToTime(lastIndex);
499      result.push(duplicate);
500    }
501    return result;
502  }
503
504  getFunktionsAtTime(time, delta, metric) {
505    // Returns a list of Funktions whose metric changed in the
506    // [time-delta, time+delta] range.
507    return this.funktions.filter(
508      funktion => funktion.didMetricChange(time, delta, metric));
509    return result;
510  }
511}
512
513
514class TotalScript extends Script {
515  constructor() {
516    super('all files', 'all files');
517    this.scripts = [];
518  }
519
520  addAllFunktions(script) {
521    // funktions is indexed by byte offset and as such not packed. Add every
522    // Funktion one by one to keep this.funktions packed.
523    script.funktions.forEach(fn => this.funktions.push(fn));
524    this.scripts.push(script);
525    this.bytesTotal += script.bytesTotal;
526  }
527
528  // Iterate over all Scripts and nested Funktions.
529  forEach(fn) {
530    this.scripts.forEach(script => script.forEach(fn));
531  }
532
533  getScripts() {
534    return this.scripts;
535  }
536}
537
538
539// ===========================================================================
540
541class NestingDistribution {
542  constructor(funktions) {
543    // Stores the nof bytes per function nesting level.
544    this.accumulator = [0, 0, 0, 0, 0];
545    // Max nof bytes encountered at any nesting level.
546    this.max = 0;
547    // avg bytes per nesting level.
548    this.avg = 0;
549    this.totalBytes = 0;
550
551    funktions.forEach(each => each.accumulateNestingLevel(this.accumulator));
552    this.max = this.accumulator.reduce((max, each) => Math.max(max, each), 0);
553    this.totalBytes = this.accumulator.reduce((sum, each) => sum + each, 0);
554    for (let i = 0; i < this.accumulator.length; i++) {
555      this.avg += this.accumulator[i] * i;
556    }
557    this.avg /= this.totalBytes;
558  }
559
560  print() {
561    console.log(this.toString())
562  }
563
564  toString() {
565    let ticks = " ▁▂▃▄▅▆▇█";
566    let accString = this.accumulator.reduce((str, each) => {
567      let index = Math.round(each / this.max * (ticks.length - 1));
568      return str + ticks[index];
569    }, '');
570    let percent0 = this.accumulator[0]
571    let percent1 = this.accumulator[1];
572    let percent2plus = this.accumulator.slice(2)
573      .reduce((sum, each) => sum + each, 0);
574    return "  - nesting level:      " +
575      ' avg=' + formatNumber(this.avg) +
576      ' l0=' + PERCENT(percent0, this.totalBytes) +
577      ' l1=' + PERCENT(percent1, this.totalBytes) +
578      ' l2+=' + PERCENT(percent2plus, this.totalBytes) +
579      ' distribution=[' + accString + ']';
580
581  }
582
583  setMetrics(dict) {}
584}
585
586class ExecutionCost {
587  constructor(prefix, funktions, time_fn) {
588    this.prefix = prefix;
589    // Time spent on executed functions.
590    this.executedCost = 0
591    // Time spent on not executed functions.
592    this.nonExecutedCost = 0;
593
594    this.executedCost = funktions.reduce((sum, each) => {
595      return sum + (each.hasBeenExecuted() ? time_fn(each) : 0)
596    }, 0);
597    this.nonExecutedCost = funktions.reduce((sum, each) => {
598      return sum + (each.hasBeenExecuted() ? 0 : time_fn(each))
599    }, 0);
600
601  }
602
603  print() {
604    console.log(this.toString())
605  }
606
607  toString() {
608    return ('  - ' + this.prefix + '-time:').padEnd(24) +
609      (" executed=" + formatNumber(this.executedCost) + 'ms').padEnd(20) +
610      " non-executed=" + formatNumber(this.nonExecutedCost) + 'ms';
611  }
612
613  setMetrics(dict) {
614    dict.set('parseMetric', this.executionCost);
615    dict.set('parseMetricNegative', this.nonExecutionCost);
616  }
617}
618
619// ===========================================================================
620
621class Funktion extends CompilationUnit {
622  constructor(name, start, end, script) {
623    super();
624    if (start < 0) throw "invalid start position: " + start;
625    if (script.isEval) {
626      if (end < start) throw 'invalid start end positions';
627    } else {
628      if (end <= 0) throw 'invalid end position: ' + end;
629      if (end <= start) throw 'invalid start end positions';
630    }
631
632    this.name = name;
633    this.start = start;
634    this.end = end;
635    this.script = script;
636    this.parent = null;
637    this.nested = [];
638    this.nestingLevel = 0;
639
640    if (script) this.script.addFunktion(this);
641  }
642
643  finalize() {
644    this.lastParseEventTimestamp = Math.max(
645        this.preparseTimestamp + this.preparseDuration,
646        this.parseTimestamp + this.parseDuration,
647        this.resolutionTimestamp + this.resolutionDuration);
648    if (!(this.lastParseEventTimestamp > 0)) this.lastParseEventTimestamp = 0;
649
650    this.lastEventTimestamp =
651        Math.max(this.lastParseEventTimestamp, this.executionTimestamp);
652    if (!(this.lastEventTimestamp > 0)) this.lastEventTimestamp = 0;
653
654    this.ownBytes = this.nested.reduce(
655        (bytes, each) => bytes - each.getBytes(), this.getBytes());
656
657    super.finalize();
658  }
659
660  getMetricBytes(name) {
661    if (name == 'lazyCompileTimestamp') return this.getOwnBytes();
662    return this.getOwnBytes();
663  }
664
665  getMetricDuration(name) {
666    if (name in kNoTimeMetrics) return 0;
667    return this[name];
668  }
669
670  isNestedIn(funktion) {
671    if (this.script != funktion.script) throw "Incompatible script";
672    return funktion.start < this.start && this.end <= funktion.end;
673  }
674
675  isToplevel() {
676    return this.parent === null;
677  }
678
679  containsPosition(position) {
680    return this.start <= position && position <= this.end;
681  }
682
683  accumulateNestingLevel(accumulator) {
684    let value = accumulator[this.nestingLevel] || 0;
685    accumulator[this.nestingLevel] = value + this.getOwnBytes();
686  }
687
688  addNestedFunktion(child) {
689    if (this.script != child.script) throw "Incompatible script";
690    if (child == null) throw "Nesting non child";
691    this.nested.push(child);
692    if (this.nested.length > 1) {
693      // Make sure the nested children don't overlap and have been inserted in
694      // byte start position order.
695      let last = this.nested[this.nested.length - 2];
696      if (last.end > child.start || last.start > child.start ||
697        last.end > child.end || last.start > child.end) {
698        throw "Wrongly nested child added";
699      }
700    }
701    child.nestingLevel = this.nestingLevel + 1;
702    return child.nestingLevel;
703  }
704
705  getBytes() {
706    return this.end - this.start;
707  }
708
709  getOwnBytes() {
710    return this.ownBytes;
711  }
712
713  didMetricChange(time, delta, name) {
714    let value = this[name + 'Timestamp'];
715    return (time - delta) <= value && value <= (time + delta);
716  }
717
718  print() {
719    console.log(this.toString());
720  }
721
722  toString(details = true) {
723    let result = 'function' + (this.name ? ' ' + this.name : '') +
724        `() range=${this.start}-${this.end}`;
725    if (details) result += ` script=${this.script ? this.script.id : 'X'}`;
726    return result;
727  }
728}
729
730
731// ===========================================================================
732
733const kTimestampFactor = 1000;
734const kSecondsToMillis = 1000;
735
736function toTimestamp(microseconds) {
737  return microseconds / kTimestampFactor
738}
739
740function startOf(timestamp, time) {
741  let result = toTimestamp(timestamp) - time;
742  if (result < 0) throw "start timestamp cannnot be negative";
743  return result;
744}
745
746
747class ParseProcessor extends LogReader {
748  constructor() {
749    super();
750    this.dispatchTable_ = {
751      // Avoid accidental leaking of __proto__ properties and force this object
752      // to be in dictionary-mode.
753      __proto__: null,
754      // "function",{event type},
755      // {script id},{start position},{end position},{time},{timestamp},
756      // {function name}
757      'function': {
758        parsers: [
759          parseString, parseInt, parseInt, parseInt, parseFloat, parseInt,
760          parseString
761        ],
762        processor: this.processFunctionEvent
763      },
764      // "compilation-cache","hit"|"put",{type},{scriptid},{start position},
765      // {end position},{timestamp}
766      'compilation-cache': {
767        parsers:
768            [parseString, parseString, parseInt, parseInt, parseInt, parseInt],
769        processor: this.processCompilationCacheEvent
770      },
771      'script': {
772        parsers: [parseString, parseInt, parseInt],
773        processor: this.processScriptEvent
774      },
775      // "script-details", {script_id}, {file}, {line}, {column}, {size}
776      'script-details': {
777        parsers: [parseInt, parseString, parseInt, parseInt, parseInt],
778        processor: this.processScriptDetails
779      },
780      'script-source': {
781        parsers: [parseInt, parseString, parseString],
782        processor: this.processScriptSource
783      },
784    };
785    this.functionEventDispatchTable_ = {
786      // Avoid accidental leaking of __proto__ properties and force this object
787      // to be in dictionary-mode.
788      __proto__: null,
789      'full-parse': this.processFull.bind(this),
790      'parse-function': this.processParseFunction.bind(this),
791      // TODO(cbruni): make sure arrow functions emit a normal parse-function
792      // event.
793      'parse': this.processParseFunction.bind(this),
794      'parse-script': this.processParseScript.bind(this),
795      'parse-eval': this.processParseEval.bind(this),
796      'preparse-no-resolution': this.processPreparseNoResolution.bind(this),
797      'preparse-resolution': this.processPreparseResolution.bind(this),
798      'first-execution': this.processFirstExecution.bind(this),
799      'compile-lazy': this.processCompileLazy.bind(this),
800      'compile': this.processCompile.bind(this),
801      'compile-eval': this.processCompileEval.bind(this),
802      'optimize-lazy': this.processOptimizeLazy.bind(this),
803      'deserialize': this.processDeserialize.bind(this),
804    };
805
806    this.idToScript = new Map();
807    this.fileToScript = new Map();
808    this.nameToFunction = new Map();
809    this.scripts = [];
810    this.totalScript = new TotalScript();
811    this.firstEventTimestamp = -1;
812    this.lastParseEventTimestamp = -1;
813    this.lastEventTimestamp = -1;
814  }
815
816  print() {
817    console.log("scripts:");
818    this.idToScript.forEach(script => script.print());
819  }
820
821  processString(string) {
822    let end = string.length;
823    let current = 0;
824    let next = 0;
825    let line;
826    let i = 0;
827    let entry;
828    while (current < end) {
829      next = string.indexOf("\n", current);
830      if (next === -1) break;
831      i++;
832      line = string.substring(current, next);
833      current = next + 1;
834      this.processLogLine(line);
835    }
836    this.postProcess();
837  }
838
839  processLogFile(fileName) {
840    this.collectEntries = true
841    this.lastLogFileName_ = fileName;
842    var line;
843    while (line = readline()) {
844      this.processLogLine(line);
845    }
846    this.postProcess();
847  }
848
849  postProcess() {
850    this.scripts = Array.from(this.idToScript.values())
851      .filter(each => !each.isNative);
852
853    this.scripts.forEach(script => {
854      script.finalize();
855      script.calculateMetrics(false)
856    });
857
858    this.scripts.forEach(script => this.totalScript.addAllFunktions(script));
859    this.totalScript.calculateMetrics(true);
860
861    this.firstEventTimestamp = this.totalScript.timestampMin(
862        this.scripts.map(each => each.firstEventTimestamp));
863    this.lastParseEventTimestamp = this.totalScript.timestampMax(
864        this.scripts.map(each => each.lastParseEventTimestamp));
865    this.lastEventTimestamp = this.totalScript.timestampMax(
866        this.scripts.map(each => each.lastEventTimestamp));
867
868    const series = {
869      firstParseEvent: 'Any Parse Event',
870      parse: 'Parsing',
871      preparse: 'Preparsing',
872      resolution: 'Preparsing with Var. Resolution',
873      lazyCompile: 'Lazy Compilation',
874      compile: 'Eager Compilation',
875      execution: 'First Execution',
876    };
877    let metrics = Object.keys(series);
878    this.totalScript.getAccumulatedTimeMetrics(
879        metrics, 0, this.lastEventTimestamp, 10);
880  }
881
882  processFunctionEvent(
883      eventName, scriptId, startPosition, endPosition, duration, timestamp,
884      functionName) {
885    let handlerFn = this.functionEventDispatchTable_[eventName];
886    if (handlerFn === undefined) {
887      console.error('Couldn\'t find handler for function event:' + eventName);
888    }
889    handlerFn(
890        scriptId, startPosition, endPosition, duration, timestamp,
891        functionName);
892  }
893
894  addEntry(entry) {
895    this.entries.push(entry);
896  }
897
898  lookupScript(id) {
899    return this.idToScript.get(id);
900  }
901
902  getOrCreateFunction(
903      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
904    if (scriptId == -1) {
905      return this.lookupFunktionByRange(startPosition, endPosition);
906    }
907    let script = this.lookupScript(scriptId);
908    let funktion = script.getFunktionAtStartPosition(startPosition);
909    if (funktion === void 0) {
910      funktion = new Funktion(functionName, startPosition, endPosition, script);
911    }
912    return funktion;
913  }
914
915  // Iterates over all functions and tries to find matching ones.
916  lookupFunktionsByRange(start, end) {
917    let results = [];
918    this.idToScript.forEach(script => {
919      script.forEach(funktion => {
920        if (funktion.startPostion == start && funktion.endPosition == end) {
921          results.push(funktion);
922        }
923      });
924    });
925    return results;
926  }
927  lookupFunktionByRange(start, end) {
928    let results = this.lookupFunktionsByRange(start, end);
929    if (results.length != 1) throw "Could not find unique function by range";
930    return results[0];
931  }
932
933  processScriptEvent(eventName, scriptId, timestamp) {
934    let script = this.idToScript.get(scriptId);
935    switch (eventName) {
936      case 'create':
937      case 'reserve-id':
938      case 'deserialize': {
939        if (script !== undefined) return;
940        script = new Script(scriptId);
941        this.idToScript.set(scriptId, script);
942        if (eventName == 'deserialize') {
943          script.deserializationTimestamp = toTimestamp(timestamp);
944        }
945        return;
946      }
947      case 'background-compile':
948        if (script.isBackgroundCompiled) {
949          throw 'Cannot background-compile twice';
950        }
951        script.isBackgroundCompiled = true;
952        // TODO(cbruni): remove once backwards compatibility is no longer needed.
953        script.isStreamingCompiled = true;
954        // TODO(cbruni): fix parse events for background compilation scripts
955        script.preparseTimestamp = toTimestamp(timestamp);
956        return;
957      case 'streaming-compile':
958        if (script.isStreamingCompiled) throw 'Cannot stream-compile twice';
959        // TODO(cbruni): remove once backwards compatibility is no longer needed.
960        script.isBackgroundCompiled = true;
961        script.isStreamingCompiled = true;
962        // TODO(cbruni): fix parse events for background compilation scripts
963        script.preparseTimestamp = toTimestamp(timestamp);
964        return;
965      default:
966        console.error('Unhandled script event: ' + eventName);
967    }
968  }
969
970  processScriptDetails(scriptId, file, startLine, startColumn, size) {
971    let script = this.lookupScript(scriptId);
972    script.setFile(file);
973  }
974
975  processScriptSource(scriptId, url, source) {
976    let script = this.lookupScript(scriptId);
977    script.source = source;
978  }
979
980  processParseEval(
981      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
982    if (startPosition != 0 && startPosition != -1) {
983      console.error('Invalid start position for parse-eval', arguments);
984    }
985    let script = this.processParseScript(...arguments);
986    script.isEval = true;
987  }
988
989  processFull(
990      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
991    if (startPosition == 0) {
992      // This should only happen for eval.
993      let script = this.lookupScript(scriptId);
994      script.isEval = true;
995      return;
996    }
997    let funktion = this.getOrCreateFunction(...arguments);
998    // TODO(cbruni): this should never happen, emit differen event from the
999    // parser.
1000    if (funktion.parseTimestamp > 0) return;
1001    funktion.parseTimestamp = startOf(timestamp, duration);
1002    funktion.parseDuration = duration;
1003  }
1004
1005  processParseFunction(
1006      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1007    let funktion = this.getOrCreateFunction(...arguments);
1008    funktion.parseTimestamp = startOf(timestamp, duration);
1009    funktion.parseDuration = duration;
1010  }
1011
1012  processParseScript(
1013      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1014    // TODO timestamp and duration
1015    let script = this.lookupScript(scriptId);
1016    let ts = startOf(timestamp, duration);
1017    script.parseTimestamp = ts;
1018    script.parseDuration = duration;
1019    return script;
1020  }
1021
1022  processPreparseResolution(
1023      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1024    let funktion = this.getOrCreateFunction(...arguments);
1025    // TODO(cbruni): this should never happen, emit different event from the
1026    // parser.
1027    if (funktion.resolutionTimestamp > 0) return;
1028    funktion.resolutionTimestamp = startOf(timestamp, duration);
1029    funktion.resolutionDuration = duration;
1030  }
1031
1032  processPreparseNoResolution(
1033      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1034    let funktion = this.getOrCreateFunction(...arguments);
1035    funktion.preparseTimestamp = startOf(timestamp, duration);
1036    funktion.preparseDuration = duration;
1037  }
1038
1039  processFirstExecution(
1040      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1041    let script = this.lookupScript(scriptId);
1042    if (startPosition === 0) {
1043      // undefined = eval fn execution
1044      if (script) {
1045        script.executionTimestamp = toTimestamp(timestamp);
1046      }
1047    } else {
1048      let funktion = script.getFunktionAtStartPosition(startPosition);
1049      if (funktion) {
1050        funktion.executionTimestamp = toTimestamp(timestamp);
1051      } else {
1052        // TODO(cbruni): handle funktions from  compilation-cache hits.
1053      }
1054    }
1055  }
1056
1057  processCompileLazy(
1058      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1059    let funktion = this.getOrCreateFunction(...arguments);
1060    funktion.lazyCompileTimestamp = startOf(timestamp, duration);
1061    funktion.lazyCompileDuration = duration;
1062  }
1063
1064  processCompile(
1065      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1066    let script = this.lookupScript(scriptId);
1067    if (startPosition === 0) {
1068      script.compileTimestamp = startOf(timestamp, duration);
1069      script.compileDuration = duration;
1070      script.bytesTotal = endPosition;
1071      return script;
1072    } else {
1073      let funktion = script.getFunktionAtStartPosition(startPosition);
1074      if (funktion === undefined) {
1075        // This should not happen since any funktion has to be parsed first.
1076        console.error('processCompile funktion not found', ...arguments);
1077        return;
1078      }
1079      funktion.compileTimestamp = startOf(timestamp, duration);
1080      funktion.compileDuration = duration;
1081      return funktion;
1082    }
1083  }
1084
1085  processCompileEval(
1086      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1087    let compilationUnit = this.processCompile(...arguments);
1088    compilationUnit.isEval = true;
1089  }
1090
1091  processOptimizeLazy(
1092      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1093    let compilationUnit = this.lookupScript(scriptId);
1094    if (startPosition > 0) {
1095      compilationUnit =
1096          compilationUnit.getFunktionAtStartPosition(startPosition);
1097      if (compilationUnit === undefined) {
1098        // This should not happen since any funktion has to be parsed first.
1099        console.error('processOptimizeLazy funktion not found', ...arguments);
1100        return;
1101      }
1102    }
1103    compilationUnit.optimizationTimestamp = startOf(timestamp, duration);
1104    compilationUnit.optimizationDuration = duration;
1105  }
1106
1107  processDeserialize(
1108      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1109    let compilationUnit = this.lookupScript(scriptId);
1110    if (startPosition === 0) {
1111      compilationUnit.bytesTotal = endPosition;
1112    } else {
1113      compilationUnit = this.getOrCreateFunction(...arguments);
1114    }
1115    compilationUnit.deserializationTimestamp = startOf(timestamp, duration);
1116    compilationUnit.deserializationDuration = duration;
1117  }
1118
1119  processCompilationCacheEvent(
1120      eventType, cacheType, scriptId, startPosition, endPosition, timestamp) {
1121    if (eventType !== 'hit') return;
1122    let compilationUnit = this.lookupScript(scriptId);
1123    if (startPosition > 0) {
1124      compilationUnit =
1125          compilationUnit.getFunktionAtStartPosition(startPosition);
1126    }
1127    compilationUnit.addCompilationCacheHit(toTimestamp(timestamp));
1128  }
1129
1130}
1131
1132
1133class ArgumentsProcessor extends BaseArgumentsProcessor {
1134  getArgsDispatch() {
1135    return {};
1136  }
1137
1138  getDefaultResults() {
1139    return {
1140      logFileName: 'v8.log',
1141      range: 'auto,auto',
1142    };
1143  }
1144}
1145