parse-processor.js 36.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
"use strict";


/**
 * A thin wrapper around shell's 'read' function showing a file name on error.
 */
function readFile(fileName) {
  try {
    return read(fileName);
  } catch (e) {
    console.log(fileName + ': ' + (e.message || e));
    throw e;
  }
}

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

// This is the only true formatting, why? For an international audience the
// confusion between the decimal and thousands separator is big (alternating
// between comma "," vs dot "."). The Swiss formatting uses "'" as a thousands
// separator, dropping most of that confusion.
25
const numberFormat = new Intl.NumberFormat('de-CH', {
26 27 28 29 30
  maximumFractionDigits: 2,
  minimumFractionDigits: 2,
});

function formatNumber(value) {
31
  return numberFormat.format(value);
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
}

function BYTES(bytes, total) {
  let units = ['B ', 'kB', 'mB', 'gB'];
  let unitIndex = 0;
  let value = bytes;
  while (value > 1000 && unitIndex < units.length) {
    value /= 1000;
    unitIndex++;
  }
  let result = formatNumber(value).padStart(10) + ' ' + units[unitIndex];
  if (total !== void 0 && total != 0) {
    result += PERCENT(bytes, total).padStart(5);
  }
  return result;
}

function PERCENT(value, total) {
  return Math.round(value / total * 100) + "%";
}

// ===========================================================================
54 55 56 57 58 59 60 61 62 63 64
const kNoTimeMetrics = {
  __proto__: null,
  executionDuration: 0,
  firstEventTimestamp: 0,
  firstParseEventTimestamp: 0,
  lastParseEventTimestamp: 0,
  lastEventTimestamp: 0
};

class CompilationUnit {
  constructor() {
65 66
    this.isEval = false;

67 68 69
    // Lazily computed properties.
    this.firstEventTimestamp = -1;
    this.firstParseEventTimestamp = -1;
70
    this.firstCompileEventTimestamp = -1;
71 72
    this.lastParseEventTimestamp = -1;
    this.lastEventTimestamp = -1;
73
    this.deserializationTimestamp = -1;
74 75 76 77 78 79

    this.preparseTimestamp = -1;
    this.parseTimestamp = -1;
    this.parse2Timestamp = -1;
    this.resolutionTimestamp = -1;
    this.compileTimestamp = -1;
80
    this.lazyCompileTimestamp = -1;
81
    this.executionTimestamp = -1;
82
    this.optimizationTimestamp = -1;
83

84
    this.deserializationDuration = -0.0;
85 86 87 88 89 90 91
    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;
92
    this.optimizeDuration = -0.0;
93 94

    this.ownBytes = -1;
95 96 97 98 99 100 101 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 148 149 150 151 152 153 154 155 156
    this.compilationCacheHits = [];
  }

  finalize() {
    this.firstEventTimestamp = this.timestampMin(
        this.deserializationTimestamp, this.parseTimestamp,
        this.preparseTimestamp, this.resolutionTimestamp,
        this.executionTimestamp);

    this.firstParseEventTimestamp = this.timestampMin(
        this.deserializationTimestamp, this.parseTimestamp,
        this.preparseTimestamp, this.resolutionTimestamp);

    this.firstCompileEventTimestamp = this.rawTimestampMin(
        this.deserializationTimestamp, this.compileTimestamp,
        this.lazyCompileTimestamp);
    // Any excuted script needs to be compiled.
    if (this.hasBeenExecuted() &&
        (this.firstCompileEventTimestamp <= 0 ||
         this.executionTimestamp < this.firstCompileTimestamp)) {
      console.error('Compile < execution timestamp', this);
    }

    if (this.ownBytes < 0) console.error(this, 'Own bytes must be positive');
  }

  hasBeenExecuted() {
    return this.executionTimestamp > 0;
  }

  addCompilationCacheHit(timestamp) {
    this.compilationCacheHits.push(timestamp);
  }

  // Returns the smallest timestamp from the given list, ignoring
  // uninitialized (-1) values.
  rawTimestampMin(...timestamps) {
    timestamps = timestamps.length == 1 ? timestamps[0] : timestamps;
    let result = timestamps.reduce((min, item) => {
      return item == -1 ? min : (min == -1 ? item : Math.min(item, item));
    }, -1);
    return result;
  }
  timestampMin(...timestamps) {
    let result = this.rawTimestampMin(...timestamps);
    if (Number.isNaN(result) || result < 0) {
      console.error(
          'Invalid timestamp min:', {result, timestamps, script: this});
      return 0;
    }
    return result;
  }

  timestampMax(...timestamps) {
    timestamps = timestamps.length == 1 ? timestamps[0] : timestamps;
    let result = Math.max(...timestamps);
    if (Number.isNaN(result) || result < 0) {
      console.error(
          'Invalid timestamp max:', {result, timestamps, script: this});
      return 0;
    }
    return result;
157 158 159 160 161
  }
}

// ===========================================================================
class 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 168 169 170 171
    this.file = '';
    this.id = id;

    this.isNative = false;
    this.isBackgroundCompiled = false;
172
    this.isStreamingCompiled = false;
173

174 175 176 177 178
    this.funktions = [];
    this.metrics = new Map();
    this.maxNestingLevel = 0;

    this.width = 0;
179
    this.bytesTotal = -1;
180 181
    this.finalized = false;
    this.summary = '';
182
    this.source = '';
183 184 185 186 187 188 189 190
  }

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

  isEmpty() {
191
    return this.funktions.length === 0;
192 193
  }

194
  getFunktionAtStartPosition(start) {
195 196 197
    if (!this.isEval && start === 0) {
      throw 'position 0 is reserved for the script';
    }
198 199 200
    if (this.finalized) {
      return this.funktions.find(funktion => funktion.start == start);
    }
201 202 203
    return this.funktions[start];
  }

204 205 206 207 208 209 210 211 212 213
  // 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;
  }

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  addMissingFunktions(list) {
    if (this.finalized) throw 'script is finalized!';
    list.forEach(fn => {
      if (this.funktions[fn.start] === void 0) {
        this.addFunktion(fn);
      }
    });
  }

  addFunktion(fn) {
    if (this.finalized) throw 'script is finalized!';
    if (fn.start === void 0) throw "Funktion has no start position";
    if (this.funktions[fn.start] !== void 0) {
      fn.print();
      throw "adding same function twice to script";
    }
    this.funktions[fn.start] = fn;
  }

  finalize() {
    this.finalized = true;
    // Compact funktions as we no longer need access via start byte position.
    this.funktions = this.funktions.filter(each => true);
    let parent = null;
    let maxNesting = 0;
    // Iterate over the Funktions in byte position order.
    this.funktions.forEach(fn => {
241
      fn.isEval = this.isEval;
242 243 244 245 246 247 248 249 250 251 252 253 254
      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;
      }
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
    });
    // 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);

270
      this.lastEventTimestamp =
271
          this.timestampMax(this.lastEventTimestamp, fn.lastEventTimestamp);
272 273
    });
    this.maxNestingLevel = maxNesting;
274 275 276 277 278 279 280 281 282

    // 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);
283
    if (this.isDeserialized || this.isEval || this.isStreamingCompiled) {
284 285 286 287 288
      if (this.getBytes() === -1) {
        this.bytesTotal = toplevelFunktionBytes;
      }
    }
    this.ownBytes = this.bytesTotal - toplevelFunktionBytes;
289 290 291 292 293 294 295
    // 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);
296
    }
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
  }

  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();
321
    return this.getOwnBytes();
322 323
  }

324
  getMetricDuration(name) {
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
    return this[name];
  }

  forEach(fn) {
    fn(this);
    this.funktions.forEach(fn);
  }

  // Container helper for TotalScript / Script.
  getScripts() {
    return [this];
  }

  calculateMetrics(printSummary) {
    let log = (str) => this.summary += str + '\n';
    log("SCRIPT: " + this.id);
    let all = this.funktions;
    if (all.length === 0) return;

    let nofFunktions = all.length;
    let ownBytesSum = list => {
      return list.reduce((bytes, each) => bytes + each.getOwnBytes(), 0)
    };

    let info = (name, funktions) => {
      let ownBytes = ownBytesSum(funktions);
      let nofPercent = Math.round(funktions.length / nofFunktions * 100);
      let value = (funktions.length + "").padStart(6) +
        (nofPercent + "%").padStart(5) +
        BYTES(ownBytes, this.bytesTotal).padStart(10);
      log(("  - " + name).padEnd(20) + value);
      this.metrics.set(name + "-bytes", ownBytes);
      this.metrics.set(name + "-count", funktions.length);
      this.metrics.set(name + "-count-percent", nofPercent);
      this.metrics.set(name + "-bytes-percent",
        Math.round(ownBytes / this.bytesTotal * 100));
    };

    log("  - file:         " + this.file);
364 365
    log('  - details:      ' +
        'isEval=' + this.isEval + ' deserialized=' + this.isDeserialized +
366
        ' streamed=' + this.isStreamingCompiled);
367 368 369
    info("scripts", this.getScripts());
    info("functions", all);
    info("toplevel fn", all.filter(each => each.isToplevel()));
370
    info('preparsed', all.filter(each => each.preparseDuration > 0));
371

372 373 374
    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));
375
    info("executed", all.filter(each => each.executionTimestamp > 0));
376
    info('forEval', all.filter(each => each.isEval));
377 378 379
    info("lazy compiled", all.filter(each => each.lazyCompileTimestamp > 0));
    info("eager compiled", all.filter(each => each.compileTimestamp > 0));

380 381
    let parsingCost =
        new ExecutionCost('parse', all, each => each.parseDuration);
382
    parsingCost.setMetrics(this.metrics);
383
    log(parsingCost.toString());
384

385 386
    let preParsingCost =
        new ExecutionCost('preparse', all, each => each.preparseDuration);
387
    preParsingCost.setMetrics(this.metrics);
388
    log(preParsingCost.toString());
389

390 391
    let resolutionCost =
        new ExecutionCost('resolution', all, each => each.resolutionDuration);
392
    resolutionCost.setMetrics(this.metrics);
393
    log(resolutionCost.toString());
394 395 396

    let nesting = new NestingDistribution(all);
    nesting.setMetrics(this.metrics);
397
    log(nesting.toString());
398 399 400 401

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

402 403
  getAccumulatedTimeMetrics(
      metrics, start, end, delta, cumulative = true, useDuration = false) {
404
    // Returns an array of the following format:
405 406
    // [ [start,         acc(metric0, start, start), acc(metric1, ...), ...],
    //   [start+delta,   acc(metric0, start, start+delta), ...],
407 408 409
    //   [start+delta*2, acc(metric0, start, start+delta*2), ...],
    //   ...
    // ]
410
    if (end <= start) throw 'Invalid ranges [' + start + ',' + end + ']';
411 412 413 414 415
    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:
416
    // [ [0, 300, ...], [1,  15, ...], [2, 100, ...], [3,   0, ...] ... ]
417 418 419 420 421 422 423 424 425
    // 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');
426
      if (useDuration) metricProperties.push(each + 'Duration');
427 428 429 430 431 432 433 434 435 436 437
    });
    // 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 => {
438 439
      // Iterate over the Funktion's metric names, skipping position 0 which
      // is the time.
440 441
      const kMetricIncrement = useDuration ? 2 : 1;
      for (let i = 1; i < metricProperties.length; i += kMetricIncrement) {
442 443
        let timestampPropertyName = metricProperties[i];
        let timestamp = funktionOrScript[timestampPropertyName];
444
        if (timestamp === void 0) continue;
445 446
        if (timestamp < start || end < timestamp) continue;
        timestamp -= start;
447 448 449 450 451 452 453 454 455
        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.
456
        row[i] += funktionOrScript.getMetricBytes(timestampPropertyName);
457
        if (!useDuration) continue;
458 459
        let durationPropertyName = metricProperties[i + 1];
        row[i + 1] += funktionOrScript.getMetricDuration(durationPropertyName);
460 461 462 463 464 465 466 467 468 469 470
      }
    });
    // 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.
471
        if (!cumulative && rows[t - 1] !== null) {
472 473 474 475 476 477
          let duplicate = rowTemplate.slice();
          duplicate[0] = indexToTime(t);
          result.push(duplicate);
        }
        continue;
      }
478
      if (cumulative) {
479 480 481 482 483 484 485
        // 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) {
486
        let duplicate = (!cumulative ? rowTemplate : previous).slice();
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 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
        duplicate[0] = indexToTime(t - 1);
        result.push(duplicate);
      }
      previous = current;
      result.push(current);
    }
    // Make sure there is an entry at the last position to make sure all graphs
    // have the same width.
    const lastIndex = rows.length - 1;
    if (rows[lastIndex] === null) {
      let duplicate = previous.slice();
      duplicate[0] = indexToTime(lastIndex);
      result.push(duplicate);
    }
    return result;
  }

  getFunktionsAtTime(time, delta, metric) {
    // Returns a list of Funktions whose metric changed in the
    // [time-delta, time+delta] range.
    return this.funktions.filter(
      funktion => funktion.didMetricChange(time, delta, metric));
    return result;
  }
}


class TotalScript extends Script {
  constructor() {
    super('all files', 'all files');
    this.scripts = [];
  }

  addAllFunktions(script) {
    // funktions is indexed by byte offset and as such not packed. Add every
    // Funktion one by one to keep this.funktions packed.
    script.funktions.forEach(fn => this.funktions.push(fn));
    this.scripts.push(script);
    this.bytesTotal += script.bytesTotal;
  }

  // Iterate over all Scripts and nested Funktions.
  forEach(fn) {
    this.scripts.forEach(script => script.forEach(fn));
  }

  getScripts() {
    return this.scripts;
  }
}


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

class NestingDistribution {
  constructor(funktions) {
    // Stores the nof bytes per function nesting level.
    this.accumulator = [0, 0, 0, 0, 0];
    // Max nof bytes encountered at any nesting level.
    this.max = 0;
    // avg bytes per nesting level.
    this.avg = 0;
    this.totalBytes = 0;

    funktions.forEach(each => each.accumulateNestingLevel(this.accumulator));
    this.max = this.accumulator.reduce((max, each) => Math.max(max, each), 0);
    this.totalBytes = this.accumulator.reduce((sum, each) => sum + each, 0);
    for (let i = 0; i < this.accumulator.length; i++) {
      this.avg += this.accumulator[i] * i;
    }
    this.avg /= this.totalBytes;
  }

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

  toString() {
    let ticks = " ▁▂▃▄▅▆▇█";
    let accString = this.accumulator.reduce((str, each) => {
      let index = Math.round(each / this.max * (ticks.length - 1));
      return str + ticks[index];
    }, '');
    let percent0 = this.accumulator[0]
    let percent1 = this.accumulator[1];
    let percent2plus = this.accumulator.slice(2)
      .reduce((sum, each) => sum + each, 0);
    return "  - nesting level:      " +
      ' avg=' + formatNumber(this.avg) +
      ' l0=' + PERCENT(percent0, this.totalBytes) +
      ' l1=' + PERCENT(percent1, this.totalBytes) +
      ' l2+=' + PERCENT(percent2plus, this.totalBytes) +
      ' distribution=[' + accString + ']';

  }

  setMetrics(dict) {}
}

class ExecutionCost {
  constructor(prefix, funktions, time_fn) {
    this.prefix = prefix;
    // Time spent on executed functions.
    this.executedCost = 0
    // Time spent on not executed functions.
    this.nonExecutedCost = 0;

    this.executedCost = funktions.reduce((sum, each) => {
      return sum + (each.hasBeenExecuted() ? time_fn(each) : 0)
    }, 0);
    this.nonExecutedCost = funktions.reduce((sum, each) => {
      return sum + (each.hasBeenExecuted() ? 0 : time_fn(each))
    }, 0);

  }

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

  toString() {
    return ('  - ' + this.prefix + '-time:').padEnd(24) +
      (" executed=" + formatNumber(this.executedCost) + 'ms').padEnd(20) +
      " non-executed=" + formatNumber(this.nonExecutedCost) + 'ms';
  }

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

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

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

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

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

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

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

679 680
  containsPosition(position) {
    return this.start <= position && position <= this.end;
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 722 723
  }

  accumulateNestingLevel(accumulator) {
    let value = accumulator[this.nestingLevel] || 0;
    accumulator[this.nestingLevel] = value + this.getOwnBytes();
  }

  addNestedFunktion(child) {
    if (this.script != child.script) throw "Incompatible script";
    if (child == null) throw "Nesting non child";
    this.nested.push(child);
    if (this.nested.length > 1) {
      // Make sure the nested children don't overlap and have been inserted in
      // byte start position order.
      let last = this.nested[this.nested.length - 2];
      if (last.end > child.start || last.start > child.start ||
        last.end > child.end || last.start > child.end) {
        throw "Wrongly nested child added";
      }
    }
    child.nestingLevel = this.nestingLevel + 1;
    return child.nestingLevel;
  }

  getBytes() {
    return this.end - this.start;
  }

  getOwnBytes() {
    return this.ownBytes;
  }

  didMetricChange(time, delta, name) {
    let value = this[name + 'Timestamp'];
    return (time - delta) <= value && value <= (time + delta);
  }

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

  toString(details = true) {
    let result = 'function' + (this.name ? ' ' + this.name : '') +
724
        `() range=${this.start}-${this.end}`;
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
    if (details) result += ` script=${this.script ? this.script.id : 'X'}`;
    return result;
  }
}


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

const kTimestampFactor = 1000;
const kSecondsToMillis = 1000;

function toTimestamp(microseconds) {
  return microseconds / kTimestampFactor
}

function startOf(timestamp, time) {
  let result = toTimestamp(timestamp) - time;
  if (result < 0) throw "start timestamp cannnot be negative";
  return result;
}


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

    this.idToScript = new Map();
    this.fileToScript = new Map();
    this.nameToFunction = new Map();
    this.scripts = [];
    this.totalScript = new TotalScript();
811 812 813
    this.firstEventTimestamp = -1;
    this.lastParseEventTimestamp = -1;
    this.lastEventTimestamp = -1;
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
  }

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

  processString(string) {
    let end = string.length;
    let current = 0;
    let next = 0;
    let line;
    let i = 0;
    let entry;
    while (current < end) {
      next = string.indexOf("\n", current);
      if (next === -1) break;
      i++;
      line = string.substring(current, next);
      current = next + 1;
      this.processLogLine(line);
    }
    this.postProcess();
  }

  processLogFile(fileName) {
    this.collectEntries = true
    this.lastLogFileName_ = fileName;
    var line;
    while (line = readline()) {
      this.processLogLine(line);
    }
    this.postProcess();
  }

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

853 854 855 856
    this.scripts.forEach(script => {
      script.finalize();
      script.calculateMetrics(false)
    });
857 858 859

    this.scripts.forEach(script => this.totalScript.addAllFunktions(script));
    this.totalScript.calculateMetrics(true);
860 861 862 863 864 865 866 867

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

868 869 870 871 872 873 874 875 876 877
    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);
878 879
    this.totalScript.getAccumulatedTimeMetrics(
        metrics, 0, this.lastEventTimestamp, 10);
880 881 882
  }

  processFunctionEvent(
883 884
      eventName, scriptId, startPosition, endPosition, duration, timestamp,
      functionName) {
885 886 887 888 889
    let handlerFn = this.functionEventDispatchTable_[eventName];
    if (handlerFn === undefined) {
      console.error('Couldn\'t find handler for function event:' + eventName);
    }
    handlerFn(
890
        scriptId, startPosition, endPosition, duration, timestamp,
891 892
        functionName);
  }
893 894 895 896 897

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

898 899
  lookupScript(id) {
    return this.idToScript.get(id);
900 901
  }

902 903 904
  getOrCreateFunction(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    if (scriptId == -1) {
905 906
      return this.lookupFunktionByRange(startPosition, endPosition);
    }
907
    let script = this.lookupScript(scriptId);
908
    let funktion = script.getFunktionAtStartPosition(startPosition);
909 910 911 912 913 914
    if (funktion === void 0) {
      funktion = new Funktion(functionName, startPosition, endPosition, script);
    }
    return funktion;
  }

915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
  // 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];
  }

933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966
  processScriptEvent(eventName, scriptId, timestamp) {
    let script = this.idToScript.get(scriptId);
    switch (eventName) {
      case 'create':
      case 'reserve-id':
      case 'deserialize': {
        if (script !== undefined) return;
        script = new Script(scriptId);
        this.idToScript.set(scriptId, script);
        if (eventName == 'deserialize') {
          script.deserializationTimestamp = toTimestamp(timestamp);
        }
        return;
      }
      case 'background-compile':
        if (script.isBackgroundCompiled) {
          throw 'Cannot background-compile twice';
        }
        script.isBackgroundCompiled = true;
        // TODO(cbruni): remove once backwards compatibility is no longer needed.
        script.isStreamingCompiled = true;
        // TODO(cbruni): fix parse events for background compilation scripts
        script.preparseTimestamp = toTimestamp(timestamp);
        return;
      case 'streaming-compile':
        if (script.isStreamingCompiled) throw 'Cannot stream-compile twice';
        // TODO(cbruni): remove once backwards compatibility is no longer needed.
        script.isBackgroundCompiled = true;
        script.isStreamingCompiled = true;
        // TODO(cbruni): fix parse events for background compilation scripts
        script.preparseTimestamp = toTimestamp(timestamp);
        return;
      default:
        console.error('Unhandled script event: ' + eventName);
967 968 969 970 971 972 973 974
    }
  }

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

975 976 977 978 979
  processScriptSource(scriptId, url, source) {
    let script = this.lookupScript(scriptId);
    script.source = source;
  }

980
  processParseEval(
981
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
982 983 984 985
    if (startPosition != 0 && startPosition != -1) {
      console.error('Invalid start position for parse-eval', arguments);
    }
    let script = this.processParseScript(...arguments);
986 987 988
    script.isEval = true;
  }

989 990
  processFull(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
991 992
    if (startPosition == 0) {
      // This should only happen for eval.
993
      let script = this.lookupScript(scriptId);
994 995 996
      script.isEval = true;
      return;
    }
997
    let funktion = this.getOrCreateFunction(...arguments);
998 999 1000
    // TODO(cbruni): this should never happen, emit differen event from the
    // parser.
    if (funktion.parseTimestamp > 0) return;
1001
    funktion.parseTimestamp = startOf(timestamp, duration);
1002
    funktion.parseDuration = duration;
1003 1004
  }

1005 1006 1007
  processParseFunction(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    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 1023 1024
  processPreparseResolution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
1025 1026 1027
    // TODO(cbruni): this should never happen, emit different event from the
    // parser.
    if (funktion.resolutionTimestamp > 0) return;
1028
    funktion.resolutionTimestamp = startOf(timestamp, duration);
1029
    funktion.resolutionDuration = duration;
1030 1031
  }

1032 1033 1034
  processPreparseNoResolution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
1035
    funktion.preparseTimestamp = startOf(timestamp, duration);
1036
    funktion.preparseDuration = duration;
1037 1038
  }

1039 1040 1041
  processFirstExecution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let script = this.lookupScript(scriptId);
1042 1043 1044 1045 1046 1047
    if (startPosition === 0) {
      // undefined = eval fn execution
      if (script) {
        script.executionTimestamp = toTimestamp(timestamp);
      }
    } else {
1048
      let funktion = script.getFunktionAtStartPosition(startPosition);
1049 1050
      if (funktion) {
        funktion.executionTimestamp = toTimestamp(timestamp);
1051 1052
      } else {
        // TODO(cbruni): handle funktions from  compilation-cache hits.
1053 1054 1055 1056
      }
    }
  }

1057 1058 1059
  processCompileLazy(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
1060
    funktion.lazyCompileTimestamp = startOf(timestamp, duration);
1061
    funktion.lazyCompileDuration = duration;
1062 1063
  }

1064 1065 1066
  processCompile(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    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 1075
      if (funktion === undefined) {
        // This should not happen since any funktion has to be parsed first.
1076 1077
        console.error('processCompile funktion not found', ...arguments);
        return;
1078 1079
      }
      funktion.compileTimestamp = startOf(timestamp, duration);
1080
      funktion.compileDuration = duration;
1081
      return funktion;
1082 1083
    }
  }
1084

1085 1086
  processCompileEval(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1087 1088
    let compilationUnit = this.processCompile(...arguments);
    compilationUnit.isEval = true;
1089 1090
  }

1091 1092
  processOptimizeLazy(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104
    let compilationUnit = this.lookupScript(scriptId);
    if (startPosition > 0) {
      compilationUnit =
          compilationUnit.getFunktionAtStartPosition(startPosition);
      if (compilationUnit === undefined) {
        // This should not happen since any funktion has to be parsed first.
        console.error('processOptimizeLazy funktion not found', ...arguments);
        return;
      }
    }
    compilationUnit.optimizationTimestamp = startOf(timestamp, duration);
    compilationUnit.optimizationDuration = duration;
1105 1106
  }

1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127
  processDeserialize(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let compilationUnit = this.lookupScript(scriptId);
    if (startPosition === 0) {
      compilationUnit.bytesTotal = endPosition;
    } else {
      compilationUnit = this.getOrCreateFunction(...arguments);
    }
    compilationUnit.deserializationTimestamp = startOf(timestamp, duration);
    compilationUnit.deserializationDuration = duration;
  }

  processCompilationCacheEvent(
      eventType, cacheType, scriptId, startPosition, endPosition, timestamp) {
    if (eventType !== 'hit') return;
    let compilationUnit = this.lookupScript(scriptId);
    if (startPosition > 0) {
      compilationUnit =
          compilationUnit.getFunktionAtStartPosition(startPosition);
    }
    compilationUnit.addCompilationCacheHit(toTimestamp(timestamp));
1128 1129
  }

1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
}


class ArgumentsProcessor extends BaseArgumentsProcessor {
  getArgsDispatch() {
    return {};
  }

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