ui.js (18635B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 var stroke = { 6 gcslice: "rgb(255,100,0)", 7 minor: "rgb(0,255,100)", 8 initialMajor: "rgb(180,60,255)", 9 }; 10 11 var numSamples = 500; 12 13 var tests = new Map(); 14 15 var gHistogram = new Map(); // {ms: count} 16 var gHistory = new FrameHistory(numSamples); 17 var gPerf = new PerfTracker(); 18 19 var latencyGraph; 20 var memoryGraph; 21 var ctx; 22 var memoryCtx; 23 24 var loadState = "(init)"; // One of '(active)', '(inactive)', '(N/A)' 25 var testState = "idle"; // One of 'idle' or 'running'. 26 var enabled = { trackingSizes: false }; 27 28 var gMemory = performance.mozMemory?.gc || performance.mozMemory || {}; 29 30 var Firefox = class extends Host { 31 start_turn() { 32 // Handled by Gecko. 33 } 34 35 end_turn() { 36 // Handled by Gecko. 37 } 38 39 suspend(duration) { 40 // Not used; requestAnimationFrame takes its place. 41 throw new Error("unimplemented"); 42 } 43 44 get minorGCCount() { 45 return gMemory.minorGCCount; 46 } 47 get majorGCCount() { 48 return gMemory.majorGCCount; 49 } 50 get GCSliceCount() { 51 return gMemory.sliceCount; 52 } 53 get gcBytes() { 54 return gMemory.zone.gcBytes; 55 } 56 get mallocBytes() { 57 return gMemory.zone.mallocBytes; 58 } 59 get gcAllocTrigger() { 60 return gMemory.zone.gcAllocTrigger; 61 } 62 get mallocTrigger() { 63 return gMemory.zone.mallocTriggerBytes; 64 } 65 66 features = { 67 haveMemorySizes: 'gcBytes' in gMemory, 68 haveGCCounts: 'majorGCCount' in gMemory, 69 }; 70 }; 71 72 var gHost = new Firefox(); 73 74 function parse_units(v) { 75 if (!v.length) { 76 return NaN; 77 } 78 var lastChar = v[v.length - 1].toLowerCase(); 79 if (!isNaN(parseFloat(lastChar))) { 80 return parseFloat(v); 81 } 82 var units = parseFloat(v.substr(0, v.length - 1)); 83 if (lastChar == "k") { 84 return units * 1e3; 85 } 86 if (lastChar == "m") { 87 return units * 1e6; 88 } 89 if (lastChar == "g") { 90 return units * 1e9; 91 } 92 return NaN; 93 } 94 95 var Graph = class { 96 constructor(canvas) { 97 this.ctx = canvas.getContext('2d'); 98 99 // Adjust scale for high-DPI displays. 100 this.scale = window.devicePixelRatio || 1; 101 let rect = canvas.getBoundingClientRect(); 102 canvas.width = Math.floor(rect.width * this.scale); 103 canvas.height = Math.floor(rect.height * this.scale); 104 canvas.style.width = rect.width; 105 canvas.style.height = rect.height; 106 107 // Record canvas size to draw into. 108 this.width = canvas.width; 109 this.height = canvas.height; 110 111 this.layout = { 112 xAxisLabel_Y: this.height - 20 * this.scale, 113 }; 114 } 115 116 xpos(index) { 117 return (index / numSamples) * (this.width - 100 * this.scale); 118 } 119 120 clear() { 121 this.ctx.clearRect(0, 0, this.width, this.height); 122 } 123 124 drawScale(delay) { 125 this.drawHBar(delay, `${delay}ms`, "rgb(150,150,150)"); 126 } 127 128 draw60fps() { 129 this.drawHBar(1000 / 60, "60fps", "#00cf61", 25); 130 } 131 132 draw30fps() { 133 this.drawHBar(1000 / 30, "30fps", "#cf0061", 25); 134 } 135 136 drawAxisLabels(x_label, y_label) { 137 const ctx = this.ctx; 138 139 ctx.font = `${10 * this.scale}px sans-serif`; 140 141 ctx.fillText(x_label, this.width / 2, this.layout.xAxisLabel_Y); 142 143 ctx.save(); 144 ctx.rotate(Math.PI / 2); 145 var start = this.height / 2 - ctx.measureText(y_label).width / 2; 146 ctx.fillText(y_label, start, -this.width + 20 * this.scale); 147 ctx.restore(); 148 } 149 150 drawFrame() { 151 const ctx = this.ctx; 152 const width = this.width; 153 const height = this.height; 154 155 // Draw frame to show size 156 ctx.strokeStyle = "rgb(0,0,0)"; 157 ctx.fillStyle = "rgb(0,0,0)"; 158 ctx.beginPath(); 159 ctx.moveTo(0, 0); 160 ctx.lineTo(width, 0); 161 ctx.lineTo(width, height); 162 ctx.lineTo(0, height); 163 ctx.closePath(); 164 ctx.stroke(); 165 } 166 }; 167 168 var LatencyGraph = class extends Graph { 169 constructor(ctx) { 170 super(ctx); 171 } 172 173 ypos(delay) { 174 return this.height + this.scale * (100 - Math.log(delay) * 64); 175 } 176 177 drawHBar(delay, label, color = "rgb(0,0,0)", label_offset = 0) { 178 const ctx = this.ctx; 179 180 let y = this.ypos(delay); 181 182 ctx.fillStyle = color; 183 ctx.strokeStyle = color; 184 ctx.fillText( 185 label, 186 this.xpos(numSamples) + 4 + label_offset, 187 this.ypos(delay) + 3 188 ); 189 190 ctx.beginPath(); 191 ctx.moveTo(this.xpos(0), this.ypos(delay)); 192 ctx.lineTo(this.xpos(numSamples) + label_offset, this.ypos(delay)); 193 ctx.stroke(); 194 ctx.strokeStyle = "rgb(0,0,0)"; 195 ctx.fillStyle = "rgb(0,0,0)"; 196 } 197 198 draw() { 199 const ctx = this.ctx; 200 201 this.clear(); 202 this.drawFrame(); 203 204 for (var delay of [10, 20, 30, 50, 100, 200, 400, 800]) { 205 this.drawScale(delay); 206 } 207 this.draw60fps(); 208 this.draw30fps(); 209 210 var worst = 0, 211 worstpos = 0; 212 ctx.beginPath(); 213 for (let i = 0; i < numSamples; i++) { 214 ctx.lineTo(this.xpos(i), this.ypos(gHistory.delays[i])); 215 if (gHistory.delays[i] >= worst) { 216 worst = gHistory.delays[i]; 217 worstpos = i; 218 } 219 } 220 ctx.stroke(); 221 222 // Draw vertical lines marking minor and major GCs 223 if (gHost.features.haveGCCounts) { 224 ctx.strokeStyle = stroke.gcslice; 225 let idx = sampleIndex % numSamples; 226 const count = { 227 major: gHistory.majorGCs[idx], 228 minor: 0, 229 slice: gHistory.slices[idx], 230 }; 231 for (let i = 0; i < numSamples; i++) { 232 idx = (sampleIndex + i) % numSamples; 233 const isMajorStart = count.major < gHistory.majorGCs[idx]; 234 if (count.slice < gHistory.slices[idx]) { 235 if (isMajorStart) { 236 ctx.strokeStyle = stroke.initialMajor; 237 } 238 ctx.beginPath(); 239 ctx.moveTo(this.xpos(idx), 0); 240 ctx.lineTo(this.xpos(idx), this.layout.xAxisLabel_Y); 241 ctx.stroke(); 242 if (isMajorStart) { 243 ctx.strokeStyle = stroke.gcslice; 244 } 245 } 246 count.major = gHistory.majorGCs[idx]; 247 count.slice = gHistory.slices[idx]; 248 } 249 250 ctx.strokeStyle = stroke.minor; 251 idx = sampleIndex % numSamples; 252 count.minor = gHistory.minorGCs[idx]; 253 for (let i = 0; i < numSamples; i++) { 254 idx = (sampleIndex + i) % numSamples; 255 if (count.minor < gHistory.minorGCs[idx]) { 256 ctx.beginPath(); 257 ctx.moveTo(this.xpos(idx), 0); 258 ctx.lineTo(this.xpos(idx), 20); 259 ctx.stroke(); 260 } 261 count.minor = gHistory.minorGCs[idx]; 262 } 263 } 264 265 ctx.fillStyle = "rgb(255,0,0)"; 266 if (worst) { 267 ctx.fillText( 268 `${worst.toFixed(2)}ms`, 269 this.xpos(worstpos) - 10, 270 this.ypos(worst) - 14 271 ); 272 } 273 274 // Mark and label the slowest frame 275 ctx.beginPath(); 276 var where = sampleIndex % numSamples; 277 ctx.arc( 278 this.xpos(where), 279 this.ypos(gHistory.delays[where]), 280 5, 281 0, 282 Math.PI * 2, 283 true 284 ); 285 ctx.fill(); 286 ctx.fillStyle = "rgb(0,0,0)"; 287 288 this.drawAxisLabels("Time", "Pause between frames (log scale)"); 289 } 290 }; 291 292 var MemoryGraph = class extends Graph { 293 constructor(ctx) { 294 super(ctx); 295 this.range = 1; 296 } 297 298 ypos(size) { 299 const percent = size / this.range; 300 return (1 - percent) * this.height * 0.9 + this.scale * 20; 301 } 302 303 drawHBarForBytes(size, name, color) { 304 this.drawHBar(size, `${format_bytes(size)} ${name}`, color) 305 } 306 307 drawHBar(size, label, color) { 308 const ctx = this.ctx; 309 310 const y = this.ypos(size); 311 312 ctx.fillStyle = color; 313 ctx.strokeStyle = color; 314 ctx.fillText(label, this.xpos(numSamples) + 4, y + 3); 315 316 ctx.beginPath(); 317 ctx.moveTo(this.xpos(0), y); 318 ctx.lineTo(this.xpos(numSamples), y); 319 ctx.stroke(); 320 ctx.strokeStyle = "rgb(0,0,0)"; 321 ctx.fillStyle = "rgb(0,0,0)"; 322 } 323 324 draw() { 325 const ctx = this.ctx; 326 327 this.clear(); 328 this.drawFrame(); 329 330 let gcMaxPos = 0; 331 let mallocMaxPos = 0; 332 let gcMax = 0; 333 let mallocMax = 0; 334 for (let i = 0; i < numSamples; i++) { 335 if (gHistory.gcBytes[i] >= gcMax) { 336 gcMax = gHistory.gcBytes[i]; 337 gcMaxPos = i; 338 } 339 if (gHistory.mallocBytes[i] >= mallocMax) { 340 mallocMax = gHistory.mallocBytes[i]; 341 mallocMaxPos = i; 342 } 343 } 344 345 this.range = Math.max(gcMax, mallocMax, gHost.gcAllocTrigger, gHost.mallocTrigger); 346 347 this.drawHBarForBytes(gcMax, "GC max", "#00cf61"); 348 this.drawHBarForBytes(mallocMax, "Malloc max", "#cc1111"); 349 this.drawHBarForBytes(gHost.gcAllocTrigger, "GC trigger", "#cc11cc"); 350 this.drawHBarForBytes(gHost.mallocTrigger, "Malloc trigger", "#cc11cc"); 351 352 ctx.fillStyle = "rgb(255,0,0)"; 353 354 if (gcMax !== 0) { 355 ctx.fillText( 356 format_bytes(gcMax), 357 this.xpos(gcMaxPos) - 10, 358 this.ypos(gcMax) - 14 359 ); 360 } 361 if (mallocMax !== 0) { 362 ctx.fillText( 363 format_bytes(mallocMax), 364 this.xpos(mallocMaxPos) - 10, 365 this.ypos(mallocMax) - 14 366 ); 367 } 368 369 const where = sampleIndex % numSamples; 370 371 ctx.beginPath(); 372 ctx.arc( 373 this.xpos(where), 374 this.ypos(gHistory.gcBytes[where]), 375 5, 376 0, 377 Math.PI * 2, 378 true 379 ); 380 ctx.fill(); 381 ctx.beginPath(); 382 ctx.arc( 383 this.xpos(where), 384 this.ypos(gHistory.mallocBytes[where]), 385 5, 386 0, 387 Math.PI * 2, 388 true 389 ); 390 ctx.fill(); 391 392 ctx.beginPath(); 393 for (let i = 0; i < numSamples; i++) { 394 let x = this.xpos(i); 395 let y = this.ypos(gHistory.gcBytes[i]); 396 if (i == (sampleIndex + 1) % numSamples) { 397 ctx.moveTo(x, y); 398 } else { 399 ctx.lineTo(x, y); 400 } 401 if (i == where) { 402 ctx.stroke(); 403 } 404 } 405 ctx.stroke(); 406 407 ctx.beginPath(); 408 for (let i = 0; i < numSamples; i++) { 409 let x = this.xpos(i); 410 let y = this.ypos(gHistory.mallocBytes[i]); 411 if (i == (sampleIndex + 1) % numSamples) { 412 ctx.moveTo(x, y); 413 } else { 414 ctx.lineTo(x, y); 415 } 416 if (i == where) { 417 ctx.stroke(); 418 } 419 } 420 ctx.stroke(); 421 422 ctx.fillStyle = "rgb(0,0,0)"; 423 424 this.drawAxisLabels("Time", "Heap Memory Usage"); 425 } 426 }; 427 428 function onUpdateDisplayChanged() { 429 const do_graph = document.getElementById("do-graph"); 430 if (do_graph.checked) { 431 window.requestAnimationFrame(handler); 432 gHistory.resume(); 433 } else { 434 gHistory.pause(); 435 } 436 update_load_state_indicator(); 437 } 438 439 function onDoLoadChange() { 440 const do_load = document.getElementById("do-load"); 441 gLoadMgr.paused = !do_load.checked; 442 console.log(`load paused: ${gLoadMgr.paused}`); 443 update_load_state_indicator(); 444 } 445 446 var previous = 0; 447 function handler(timestamp) { 448 if (gHistory.is_stopped()) { 449 return; 450 } 451 452 const completed = gLoadMgr.tick(timestamp); 453 if (completed) { 454 end_test(timestamp, gLoadMgr.lastActive); 455 if (!gLoadMgr.stopped()) { 456 start_test(); 457 } 458 update_load_display(); 459 } 460 461 if (testState == "running") { 462 document.getElementById("test-progress").textContent = 463 (gLoadMgr.currentLoadRemaining(timestamp) / 1000).toFixed(1) + " sec"; 464 } 465 466 const delay = gHistory.on_frame(timestamp); 467 468 update_histogram(gHistogram, delay); 469 470 latencyGraph.draw(); 471 if (memoryGraph) { 472 memoryGraph.draw(); 473 } 474 window.requestAnimationFrame(handler); 475 } 476 477 // For interactive debugging. 478 // 479 // ['a', 'b', 'b', 'b', 'c', 'c'] => ['a', 'b x 3', 'c x 2'] 480 function summarize(arr) { 481 if (!arr.length) { 482 return []; 483 } 484 485 var result = []; 486 var run_start = 0; 487 var prev = arr[0]; 488 for (let i = 1; i <= arr.length; i++) { 489 if (i == arr.length || arr[i] != prev) { 490 if (i == run_start + 1) { 491 result.push(arr[i]); 492 } else { 493 result.push(prev + " x " + (i - run_start)); 494 } 495 run_start = i; 496 } 497 if (i != arr.length) { 498 prev = arr[i]; 499 } 500 } 501 502 return result; 503 } 504 505 function reset_draw_state() { 506 gHistory.reset(); 507 } 508 509 function onunload() { 510 if (gLoadMgr) { 511 gLoadMgr.deactivateLoad(); 512 } 513 } 514 515 async function onload() { 516 // Collect all test loads into the `tests` Map. 517 let imports = []; 518 foreach_test_file(path => imports.push(import("./" + path))); 519 await Promise.all(imports); 520 521 // The order of `tests` is currently based on their asynchronous load 522 // order, rather than the listed order. Rearrange by extracting the test 523 // names from their filenames, which is kind of gross. 524 _tests = tests; 525 tests = new Map(); 526 foreach_test_file(fn => { 527 // "benchmarks/foo.js" => "foo" 528 const name = fn.split(/\//)[1].split(/\./)[0]; 529 tests.set(name, _tests.get(name)); 530 }); 531 _tests = undefined; 532 533 gLoadMgr = new AllocationLoadManager(tests); 534 535 // Load initial test duration. 536 duration_changed(); 537 538 // Load initial garbage size. 539 garbage_piles_changed(); 540 garbage_per_frame_changed(); 541 542 // Populate the test selection dropdown. 543 var select = document.getElementById("test-selection"); 544 for (var [name, test] of tests) { 545 test.name = name; 546 var option = document.createElement("option"); 547 option.id = name; 548 option.text = name; 549 option.title = test.description; 550 select.add(option); 551 } 552 553 // Load the initial test. 554 gLoadMgr.setActiveLoad(gLoadMgr.getByName("noAllocation")); 555 update_load_display(); 556 document.getElementById("test-selection").value = "noAllocation"; 557 558 // Polyfill rAF. 559 var requestAnimationFrame = 560 window.requestAnimationFrame || 561 window.mozRequestAnimationFrame || 562 window.webkitRequestAnimationFrame || 563 window.msRequestAnimationFrame; 564 window.requestAnimationFrame = requestAnimationFrame; 565 566 // Acquire our canvas. 567 var canvas = document.getElementById("graph"); 568 latencyGraph = new LatencyGraph(canvas); 569 570 if (!gHost.features.haveMemorySizes) { 571 document.getElementById("memgraph-disabled").style.display = "block"; 572 document.getElementById("track-sizes-div").style.display = "none"; 573 } 574 575 trackHeapSizes(document.getElementById("track-sizes").checked); 576 577 update_load_state_indicator(); 578 gHistory.start(); 579 580 // Start drawing. 581 reset_draw_state(); 582 window.requestAnimationFrame(handler); 583 } 584 585 function run_one_test() { 586 start_test_cycle([gLoadMgr.activeLoad().name]); 587 } 588 589 function run_all_tests() { 590 start_test_cycle([...tests.keys()]); 591 } 592 593 function start_test_cycle(tests_to_run) { 594 // Convert from an iterable to an array for pop. 595 const duration = gLoadMgr.testDurationMS / 1000; 596 const mutators = tests_to_run.map(name => new SingleMutatorSequencer(gLoadMgr.getByName(name), gPerf, duration)); 597 const sequencer = new ChainSequencer(mutators); 598 gLoadMgr.startSequencer(sequencer); 599 testState = "running"; 600 gHistogram.clear(); 601 reset_draw_state(); 602 } 603 604 function update_load_state_indicator() { 605 if ( 606 !gLoadMgr.load_running() || 607 gLoadMgr.activeLoad().name == "noAllocation" 608 ) { 609 loadState = "(none)"; 610 } else if (gHistory.is_stopped() || gLoadMgr.paused) { 611 loadState = "(inactive)"; 612 } else { 613 loadState = "(active)"; 614 } 615 document.getElementById("load-running").textContent = loadState; 616 } 617 618 function start_test() { 619 console.log(`Running test: ${gLoadMgr.activeLoad().name}`); 620 document.getElementById("test-selection").value = gLoadMgr.activeLoad().name; 621 update_load_state_indicator(); 622 } 623 624 function end_test(timestamp, load) { 625 document.getElementById("test-progress").textContent = "(not running)"; 626 report_test_result(load, gHistogram); 627 gHistogram.clear(); 628 console.log(`Ending test ${load.name}`); 629 if (gLoadMgr.stopped()) { 630 testState = "idle"; 631 } 632 update_load_state_indicator(); 633 reset_draw_state(); 634 } 635 636 function compute_test_spark_histogram(histogram) { 637 const percents = compute_spark_histogram_percents(histogram); 638 639 var sparks = "▁▂▃▄▅▆▇█"; 640 var colors = [ 641 "#aaaa00", 642 "#007700", 643 "#dd0000", 644 "#ff0000", 645 "#ff0000", 646 "#ff0000", 647 "#ff0000", 648 "#ff0000", 649 ]; 650 var line = ""; 651 for (let i = 0; i < percents.length; ++i) { 652 var spark = sparks.charAt(parseInt(percents[i] * sparks.length)); 653 line += `<span style="color:${colors[i]}">${spark}</span>`; 654 } 655 return line; 656 } 657 658 function report_test_result(load, histogram) { 659 var resultList = document.getElementById("results-display"); 660 var resultElem = document.createElement("div"); 661 var score = compute_test_score(histogram); 662 var sparks = compute_test_spark_histogram(histogram); 663 var params = `(${format_num(load.garbagePerFrame)},${format_num( 664 load.garbagePiles 665 )})`; 666 resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${ 667 load.name 668 }${params} - ${load.description}`; 669 resultList.appendChild(resultElem); 670 } 671 672 function update_load_display() { 673 const garbage = gLoadMgr.activeLoad() 674 ? gLoadMgr.activeLoad().garbagePerFrame 675 : parse_units(gDefaultGarbagePerFrame); 676 document.getElementById("garbage-per-frame").value = format_num(garbage); 677 const piles = gLoadMgr.activeLoad() 678 ? gLoadMgr.activeLoad().garbagePiles 679 : parse_units(gDefaultGarbagePiles); 680 document.getElementById("garbage-piles").value = format_num(piles); 681 update_load_state_indicator(); 682 } 683 684 function duration_changed() { 685 var durationInput = document.getElementById("test-duration"); 686 gLoadMgr.testDurationMS = parseInt(durationInput.value) * 1000; 687 console.log( 688 `Updated test duration to: ${gLoadMgr.testDurationMS / 1000} seconds` 689 ); 690 } 691 692 function onLoadChange() { 693 var select = document.getElementById("test-selection"); 694 console.log(`Switching to test: ${select.value}`); 695 gLoadMgr.setActiveLoad(gLoadMgr.getByName(select.value)); 696 update_load_display(); 697 gHistogram.clear(); 698 reset_draw_state(); 699 } 700 701 function garbage_piles_changed() { 702 const input = document.getElementById("garbage-piles"); 703 const value = parse_units(input.value); 704 if (isNaN(value)) { 705 update_load_display(); 706 return; 707 } 708 709 if (gLoadMgr.load_running()) { 710 gLoadMgr.change_garbagePiles(value); 711 console.log( 712 `Updated garbage-piles to ${gLoadMgr.activeLoad().garbagePiles} items` 713 ); 714 } 715 gHistogram.clear(); 716 reset_draw_state(); 717 } 718 719 function garbage_per_frame_changed() { 720 const input = document.getElementById("garbage-per-frame"); 721 var value = parse_units(input.value); 722 if (isNaN(value)) { 723 update_load_display(); 724 return; 725 } 726 if (gLoadMgr.load_running()) { 727 gLoadMgr.change_garbagePerFrame(value); 728 console.log( 729 `Updated garbage-per-frame to ${ 730 gLoadMgr.activeLoad().garbagePerFrame 731 } items` 732 ); 733 } 734 } 735 736 function trackHeapSizes(track) { 737 enabled.trackingSizes = track && gHost.features.haveMemorySizes; 738 739 var canvas = document.getElementById("memgraph"); 740 741 if (enabled.trackingSizes) { 742 canvas.style.display = "block"; 743 memoryGraph = new MemoryGraph(canvas); 744 } else { 745 canvas.style.display = "none"; 746 memoryGraph = null; 747 } 748 }