parse-processor.mjs 37.1 KB
Newer Older
1 2 3
// 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.
4 5
import { LogReader, parseString } from "./logreader.mjs";
import { BaseArgumentsProcessor } from "./arguments.mjs";
6 7 8 9 10 11 12

// ===========================================================================

// 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.
13
const numberFormat = new Intl.NumberFormat('de-CH', {
14 15 16 17 18
  maximumFractionDigits: 2,
  minimumFractionDigits: 2,
});

function formatNumber(value) {
19
  return numberFormat.format(value);
20 21
}

22
export function BYTES(bytes, total) {
23 24 25 26 27 28 29 30
  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];
31
  if (total !== undefined && total != 0) {
32 33 34 35 36
    result += PERCENT(bytes, total).padStart(5);
  }
  return result;
}

37
export function PERCENT(value, total) {
38 39 40 41
  return Math.round(value / total * 100) + "%";
}

// ===========================================================================
42 43 44 45 46 47 48 49 50 51 52
const kNoTimeMetrics = {
  __proto__: null,
  executionDuration: 0,
  firstEventTimestamp: 0,
  firstParseEventTimestamp: 0,
  lastParseEventTimestamp: 0,
  lastEventTimestamp: 0
};

class CompilationUnit {
  constructor() {
53
    this.isEval = false;
54
    this.isDeserialized = false;
55

56 57 58
    // Lazily computed properties.
    this.firstEventTimestamp = -1;
    this.firstParseEventTimestamp = -1;
59
    this.firstCompileEventTimestamp = -1;
60 61
    this.lastParseEventTimestamp = -1;
    this.lastEventTimestamp = -1;
62
    this.deserializationTimestamp = -1;
63 64 65 66 67 68

    this.preparseTimestamp = -1;
    this.parseTimestamp = -1;
    this.parse2Timestamp = -1;
    this.resolutionTimestamp = -1;
    this.compileTimestamp = -1;
69
    this.lazyCompileTimestamp = -1;
70
    this.executionTimestamp = -1;
71 72
    this.baselineTimestamp = -1;
    this.optimizeTimestamp = -1;
73

74
    this.deserializationDuration = -0.0;
75 76 77 78 79 80 81
    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;
82
    this.baselineDuration = -0.0;
83
    this.optimizeDuration = -0.0;
84 85

    this.ownBytes = -1;
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    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,
101
        this.baselineTimestamp, this.lazyCompileTimestamp);
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    // 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;
148 149 150 151 152
  }
}

// ===========================================================================
class 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 159 160 161 162
    this.file = '';
    this.id = id;

    this.isNative = false;
    this.isBackgroundCompiled = false;
163
    this.isStreamingCompiled = false;
164

165 166 167 168 169
    this.funktions = [];
    this.metrics = new Map();
    this.maxNestingLevel = 0;

    this.width = 0;
170
    this.bytesTotal = -1;
171 172
    this.finalized = false;
    this.summary = '';
173
    this.source = '';
174 175 176 177 178 179 180 181
  }

  setFile(name) {
    this.file = name;
    this.isNative = name.startsWith('native ');
  }

  isEmpty() {
182
    return this.funktions.length === 0;
183 184
  }

185
  getFunktionAtStartPosition(start) {
186 187 188
    if (!this.isEval && start === 0) {
      throw 'position 0 is reserved for the script';
    }
189 190 191
    if (this.finalized) {
      return this.funktions.find(funktion => funktion.start == start);
    }
192 193 194
    return this.funktions[start];
  }

195 196 197 198 199 200 201 202 203 204
  // 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;
  }

205 206 207
  addMissingFunktions(list) {
    if (this.finalized) throw 'script is finalized!';
    list.forEach(fn => {
208
      if (this.funktions[fn.start] === undefined) {
209 210 211 212 213 214 215
        this.addFunktion(fn);
      }
    });
  }

  addFunktion(fn) {
    if (this.finalized) throw 'script is finalized!';
216 217
    if (fn.start === undefined) throw "Funktion has no start position";
    if (this.funktions[fn.start] !== undefined) {
218 219 220 221 222 223 224 225 226 227 228 229 230 231
      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 => {
232
      fn.isEval = this.isEval;
233 234 235 236 237 238 239 240 241 242 243 244 245
      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;
      }
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
    });
    // 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);

261
      this.lastEventTimestamp =
262
          this.timestampMax(this.lastEventTimestamp, fn.lastEventTimestamp);
263 264
    });
    this.maxNestingLevel = maxNesting;
265 266 267 268 269 270 271 272 273

    // 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);
274
    if (this.isDeserialized || this.isEval || this.isStreamingCompiled) {
275 276 277 278 279
      if (this.getBytes() === -1) {
        this.bytesTotal = toplevelFunktionBytes;
      }
    }
    this.ownBytes = this.bytesTotal - toplevelFunktionBytes;
280 281 282 283 284 285 286
    // 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);
287
    }
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
  }

  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();
312
    return this.getOwnBytes();
313 314
  }

315
  getMetricDuration(name) {
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
    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';
331
    log(`SCRIPT: ${this.id}`);
332 333 334 335 336 337 338 339 340 341 342 343 344 345
    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);
346
      log((`  - ${name}`).padEnd(20) + value);
347 348 349 350 351 352 353
      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));
    };

354
    log(`  - file:         ${this.file}`);
355 356
    log('  - details:      ' +
        'isEval=' + this.isEval + ' deserialized=' + this.isDeserialized +
357
        ' streamed=' + this.isStreamingCompiled);
358 359 360
    info("scripts", this.getScripts());
    info("functions", all);
    info("toplevel fn", all.filter(each => each.isToplevel()));
361
    info('preparsed', all.filter(each => each.preparseDuration > 0));
362

363 364 365
    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));
366
    info("executed", all.filter(each => each.executionTimestamp > 0));
367
    info('forEval', all.filter(each => each.isEval));
368 369 370
    info("lazy compiled", all.filter(each => each.lazyCompileTimestamp > 0));
    info("eager compiled", all.filter(each => each.compileTimestamp > 0));

371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    info("baseline", all.filter(each => each.baselineTimestamp > 0));
    info("optimized", all.filter(each => each.optimizeTimestamp > 0));

    const costs = [
      ['parse', each => each.parseDuration],
      ['preparse', each => each.preparseDuration],
      ['resolution', each => each.resolutionDuration],
      ['compile-eager',  each => each.compileDuration],
      ['compile-lazy',  each => each.lazyCompileDuration],
      ['baseline',  each => each.baselineDuration],
      ['optimize', each => each.optimizeDuration],
    ];
    for (let [name, fn] of costs) {
      const executionCost = new ExecutionCost(name, all, fn);
      executionCost.setMetrics(this.metrics);
      log(executionCost.toString());
    }
388 389 390

    let nesting = new NestingDistribution(all);
    nesting.setMetrics(this.metrics);
391
    log(nesting.toString());
392 393 394 395

    if (printSummary) console.log(this.summary);
  }

396 397
  getAccumulatedTimeMetrics(
      metrics, start, end, delta, cumulative = true, useDuration = false) {
398
    // Returns an array of the following format:
399 400
    // [ [start,         acc(metric0, start, start), acc(metric1, ...), ...],
    //   [start+delta,   acc(metric0, start, start+delta), ...],
401 402 403
    //   [start+delta*2, acc(metric0, start, start+delta*2), ...],
    //   ...
    // ]
404
    if (end <= start) throw `Invalid ranges [${start},${end}]`;
405 406 407 408 409
    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:
410
    // [ [0, 300, ...], [1,  15, ...], [2, 100, ...], [3,   0, ...] ... ]
411 412 413 414 415 416 417 418 419
    // 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');
420
      if (useDuration) metricProperties.push(each + 'Duration');
421 422 423 424 425 426 427 428 429 430 431
    });
    // 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 => {
432 433
      // Iterate over the Funktion's metric names, skipping position 0 which
      // is the time.
434 435
      const kMetricIncrement = useDuration ? 2 : 1;
      for (let i = 1; i < metricProperties.length; i += kMetricIncrement) {
436 437
        let timestampPropertyName = metricProperties[i];
        let timestamp = funktionOrScript[timestampPropertyName];
438
        if (timestamp === undefined) continue;
439 440
        if (timestamp < start || end < timestamp) continue;
        timestamp -= start;
441 442 443 444 445 446 447 448 449
        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.
450
        row[i] += funktionOrScript.getMetricBytes(timestampPropertyName);
451
        if (!useDuration) continue;
452 453
        let durationPropertyName = metricProperties[i + 1];
        row[i + 1] += funktionOrScript.getMetricDuration(durationPropertyName);
454 455 456 457 458 459 460 461 462 463 464
      }
    });
    // 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.
465
        if (!cumulative && rows[t - 1] !== null) {
466 467 468 469 470 471
          let duplicate = rowTemplate.slice();
          duplicate[0] = indexToTime(t);
          result.push(duplicate);
        }
        continue;
      }
472
      if (cumulative) {
473 474 475 476 477 478 479
        // 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) {
480
        let duplicate = (!cumulative ? rowTemplate : previous).slice();
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
        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;
587
    this.maxDuration = 0;
588

589 590 591 592 593 594 595 596 597 598
    for (let i = 0; i < funktions.length; i++) {
      const funktion = funktions[i];
      const value = time_fn(funktion);
      if (funktion.hasBeenExecuted()) {
        this.executedCost +=  value;
      } else {
        this.nonExecutedCost += value;
      }
      this.maxDuration = Math.max(this.maxDuration, value);
    }
599 600 601 602 603 604 605
  }

  print() {
    console.log(this.toString())
  }

  toString() {
606 607 608 609
    return `  - ${this.prefix}-time:`.padEnd(24) +
      ` executed=${formatNumber(this.executedCost)}ms`.padEnd(20) +
      ` non-executed=${formatNumber(this.nonExecutedCost)}ms`.padEnd(24) +
      ` max=${formatNumber(this.maxDuration)}ms`;
610 611 612 613 614 615 616 617 618 619
  }

  setMetrics(dict) {
    dict.set('parseMetric', this.executionCost);
    dict.set('parseMetricNegative', this.nonExecutionCost);
  }
}

// ===========================================================================

620
class Funktion extends CompilationUnit {
621
  constructor(name, start, end, script) {
622
    super();
623
    if (start < 0) throw `invalid start position: ${start}`;
624 625 626
    if (script.isEval) {
      if (end < start) throw 'invalid start end positions';
    } else {
627
      if (end <= 0) throw `invalid end position: ${end}`;
628 629
      if (end <= start) throw 'invalid start end positions';
    }
630 631 632 633 634 635 636 637 638 639 640 641

    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);
  }

642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
  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();
  }

659 660
  getMetricBytes(name) {
    if (name == 'lazyCompileTimestamp') return this.getOwnBytes();
661
    return this.getOwnBytes();
662 663
  }

664
  getMetricDuration(name) {
665 666 667 668 669 670 671 672 673 674
    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() {
675
    return this.parent === null;
676 677
  }

678 679
  containsPosition(position) {
    return this.start <= position && position <= this.end;
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
  }

  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) {
722
    let result = `function${this.name ? ` ${this.name}` : ''}` +
723
        `() range=${this.start}-${this.end}`;
724 725 726 727 728 729 730 731
    if (details) result += ` script=${this.script ? this.script.id : 'X'}`;
    return result;
  }
}


// ===========================================================================

732 733
export const kTimestampFactor = 1000;
export const kSecondsToMillis = 1000;
734 735 736 737 738 739 740 741 742 743 744 745

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;
}


746
export class ParseProcessor extends LogReader {
747 748
  constructor() {
    super();
749
    this.setDispatchTable({
750 751 752
      // Avoid accidental leaking of __proto__ properties and force this object
      // to be in dictionary-mode.
      __proto__: null,
753 754 755
      // "function",{event type},
      // {script id},{start position},{end position},{time},{timestamp},
      // {function name}
756 757
      'function': {
        parsers: [
758 759
          parseString, parseInt, parseInt, parseInt, parseFloat, parseInt,
          parseString
760 761
        ],
        processor: this.processFunctionEvent
762
      },
763 764
      // "compilation-cache","hit"|"put",{type},{scriptid},{start position},
      // {end position},{timestamp}
765
      'compilation-cache': {
766 767
        parsers:
            [parseString, parseString, parseInt, parseInt, parseInt, parseInt],
768
        processor: this.processCompilationCacheEvent
769 770
      },
      'script': {
771
        parsers: [parseString, parseInt, parseInt],
772 773 774 775 776 777
        processor: this.processScriptEvent
      },
      // "script-details", {script_id}, {file}, {line}, {column}, {size}
      'script-details': {
        parsers: [parseInt, parseString, parseInt, parseInt, parseInt],
        processor: this.processScriptDetails
778 779 780 781 782
      },
      'script-source': {
        parsers: [parseInt, parseString, parseString],
        processor: this.processScriptSource
      },
783
    });
784 785 786 787 788
    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),
789 790 791 792
      'parse-function': this.processParseFunction.bind(this),
      // TODO(cbruni): make sure arrow functions emit a normal parse-function
      // event.
      'parse': this.processParseFunction.bind(this),
793 794
      'parse-script': this.processParseScript.bind(this),
      'parse-eval': this.processParseEval.bind(this),
795 796 797 798
      '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),
799 800
      'compile': this.processCompile.bind(this),
      'compile-eval': this.processCompileEval.bind(this),
801 802
      'baseline': this.processBaselineLazy.bind(this),
      'baseline-lazy': this.processBaselineLazy.bind(this),
803 804
      'optimize-lazy': this.processOptimizeLazy.bind(this),
      'deserialize': this.processDeserialize.bind(this),
805 806 807 808 809 810 811
    };

    this.idToScript = new Map();
    this.fileToScript = new Map();
    this.nameToFunction = new Map();
    this.scripts = [];
    this.totalScript = new TotalScript();
812 813 814
    this.firstEventTimestamp = -1;
    this.lastParseEventTimestamp = -1;
    this.lastEventTimestamp = -1;
815 816 817 818 819 820 821 822
  }

  print() {
    console.log("scripts:");
    this.idToScript.forEach(script => script.print());
  }

  processString(string) {
823
    this.processLogChunk(string);
824 825 826 827 828 829
    this.postProcess();
  }

  processLogFile(fileName) {
    this.collectEntries = true
    this.lastLogFileName_ = fileName;
830
    let line;
831 832 833 834 835 836 837 838 839 840
    while (line = readline()) {
      this.processLogLine(line);
    }
    this.postProcess();
  }

  postProcess() {
    this.scripts = Array.from(this.idToScript.values())
      .filter(each => !each.isNative);

841 842 843 844
    this.scripts.forEach(script => {
      script.finalize();
      script.calculateMetrics(false)
    });
845 846 847

    this.scripts.forEach(script => this.totalScript.addAllFunktions(script));
    this.totalScript.calculateMetrics(true);
848 849 850 851 852 853 854 855

    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));

856 857 858 859 860 861 862 863 864 865
    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);
866 867
    this.totalScript.getAccumulatedTimeMetrics(
        metrics, 0, this.lastEventTimestamp, 10);
868 869 870
  }

  processFunctionEvent(
871 872
      eventName, scriptId, startPosition, endPosition, duration, timestamp,
      functionName) {
873 874
    let handlerFn = this.functionEventDispatchTable_[eventName];
    if (handlerFn === undefined) {
875
      console.error(`Couldn't find handler for function event:${eventName}`);
876 877
    }
    handlerFn(
878
        scriptId, startPosition, endPosition, duration, timestamp,
879 880
        functionName);
  }
881 882 883 884 885

  addEntry(entry) {
    this.entries.push(entry);
  }

886 887
  lookupScript(id) {
    return this.idToScript.get(id);
888 889
  }

890 891 892
  getOrCreateFunction(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    if (scriptId == -1) {
893 894
      return this.lookupFunktionByRange(startPosition, endPosition);
    }
895
    let script = this.lookupScript(scriptId);
896
    let funktion = script.getFunktionAtStartPosition(startPosition);
897
    if (funktion === undefined) {
898 899 900 901 902
      funktion = new Funktion(functionName, startPosition, endPosition, script);
    }
    return funktion;
  }

903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920
  // 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];
  }

921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
  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:
954
        console.error(`Unhandled script event: ${eventName}`);
955 956 957 958 959 960 961 962
    }
  }

  processScriptDetails(scriptId, file, startLine, startColumn, size) {
    let script = this.lookupScript(scriptId);
    script.setFile(file);
  }

963 964 965 966 967
  processScriptSource(scriptId, url, source) {
    let script = this.lookupScript(scriptId);
    script.source = source;
  }

968
  processParseEval(
969
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
970 971 972 973
    if (startPosition != 0 && startPosition != -1) {
      console.error('Invalid start position for parse-eval', arguments);
    }
    let script = this.processParseScript(...arguments);
974 975 976
    script.isEval = true;
  }

977 978
  processFull(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
979 980
    if (startPosition == 0) {
      // This should only happen for eval.
981
      let script = this.lookupScript(scriptId);
982 983 984
      script.isEval = true;
      return;
    }
985
    let funktion = this.getOrCreateFunction(...arguments);
986 987 988
    // TODO(cbruni): this should never happen, emit differen event from the
    // parser.
    if (funktion.parseTimestamp > 0) return;
989
    funktion.parseTimestamp = startOf(timestamp, duration);
990
    funktion.parseDuration = duration;
991 992
  }

993 994 995
  processParseFunction(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    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 1011 1012
  processPreparseResolution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
1013 1014 1015
    // TODO(cbruni): this should never happen, emit different event from the
    // parser.
    if (funktion.resolutionTimestamp > 0) return;
1016
    funktion.resolutionTimestamp = startOf(timestamp, duration);
1017
    funktion.resolutionDuration = duration;
1018 1019
  }

1020 1021 1022
  processPreparseNoResolution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
1023
    funktion.preparseTimestamp = startOf(timestamp, duration);
1024
    funktion.preparseDuration = duration;
1025 1026
  }

1027 1028 1029
  processFirstExecution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let script = this.lookupScript(scriptId);
1030 1031 1032 1033 1034 1035
    if (startPosition === 0) {
      // undefined = eval fn execution
      if (script) {
        script.executionTimestamp = toTimestamp(timestamp);
      }
    } else {
1036
      let funktion = script.getFunktionAtStartPosition(startPosition);
1037 1038
      if (funktion) {
        funktion.executionTimestamp = toTimestamp(timestamp);
1039 1040
      } else {
        // TODO(cbruni): handle funktions from  compilation-cache hits.
1041 1042 1043 1044
      }
    }
  }

1045 1046 1047
  processCompileLazy(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
1048
    funktion.lazyCompileTimestamp = startOf(timestamp, duration);
1049
    funktion.lazyCompileDuration = duration;
1050 1051
  }

1052 1053 1054
  processCompile(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    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 1063
      if (funktion === undefined) {
        // This should not happen since any funktion has to be parsed first.
1064 1065
        console.error('processCompile funktion not found', ...arguments);
        return;
1066 1067
      }
      funktion.compileTimestamp = startOf(timestamp, duration);
1068
      funktion.compileDuration = duration;
1069
      return funktion;
1070 1071
    }
  }
1072

1073 1074
  processCompileEval(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1075 1076
    let compilationUnit = this.processCompile(...arguments);
    compilationUnit.isEval = true;
1077 1078
  }

1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
  processBaselineLazy(
      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('processBaselineLazy funktion not found', ...arguments);
        return;
      }
    }
    compilationUnit.baselineTimestamp = startOf(timestamp, duration);
    compilationUnit.baselineDuration = duration;
  }

1095 1096
  processOptimizeLazy(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
    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;
      }
    }
1107 1108
    compilationUnit.optimizeTimestamp = startOf(timestamp, duration);
    compilationUnit.optimizeDuration = duration;
1109 1110
  }

1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
  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;
1121
    compilationUnit.isDeserialized = true;
1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
  }

  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));
1133 1134
  }

1135 1136 1137
}


1138
export class ArgumentsProcessor extends BaseArgumentsProcessor {
1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
  getArgsDispatch() {
    return {};
  }

  getDefaultResults() {
    return {
      logFileName: 'v8.log',
      range: 'auto,auto',
    };
  }
}