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