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