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