1 <!DOCTYPE html> 2 <html> 3 <!-- 4 Copyright 2017 the V8 project authors. All rights reserved. Use of this source 5 code is governed by a BSD-style license that can be found in the LICENSE file. 6 --> 7 <head> 8 <meta charset="UTF-8"> 9 <style> 10 html, body { 11 font-family: sans-serif; 12 padding: 0px; 13 margin: 0px; 14 } 15 h1, h2, h3, section { 16 padding-left: 15px; 17 } 18 #stats table { 19 display: inline-block; 20 padding-right: 50px; 21 } 22 #stats .transitionTable { 23 max-height: 200px; 24 overflow-y: scroll; 25 } 26 #timeline { 27 position: relative; 28 height: 300px; 29 overflow-y: hidden; 30 overflow-x: scroll; 31 user-select: none; 32 } 33 #timelineChunks { 34 height: 250px; 35 position: absolute; 36 margin-right: 100px; 37 } 38 #timelineCanvas { 39 height: 250px; 40 position: relative; 41 overflow: visible; 42 pointer-events: none; 43 } 44 .chunk { 45 width: 6px; 46 border: 0px white solid; 47 border-width: 0 2px 0 2px; 48 position: absolute; 49 background-size: 100% 100%; 50 image-rendering: pixelated; 51 bottom: 0px; 52 } 53 .timestamp { 54 height: 250px; 55 width: 100px; 56 border-left: 1px black dashed; 57 padding-left: 4px; 58 position: absolute; 59 pointer-events: none; 60 font-size: 10px; 61 opacity: 0.5; 62 } 63 #timelineOverview { 64 width: 100%; 65 height: 50px; 66 position: relative; 67 margin-top: -50px; 68 margin-bottom: 10px; 69 background-size: 100% 100%; 70 border: 1px black solid; 71 border-width: 1px 0 1px 0; 72 overflow: hidden; 73 } 74 #timelineOverviewIndicator { 75 height: 100%; 76 position: absolute; 77 box-shadow: 0px 2px 20px -5px black inset; 78 top: 0px; 79 cursor: ew-resize; 80 } 81 #timelineOverviewIndicator .leftMask, 82 #timelineOverviewIndicator .rightMask { 83 background-color: rgba(200, 200, 200, 0.5); 84 width: 10000px; 85 height: 100%; 86 position: absolute; 87 top: 0px; 88 } 89 #timelineOverviewIndicator .leftMask { 90 right: 100%; 91 } 92 #timelineOverviewIndicator .rightMask { 93 left: 100%; 94 } 95 #mapDetails { 96 font-family: monospace; 97 white-space: pre; 98 } 99 #transitionView { 100 overflow-x: scroll; 101 white-space: nowrap; 102 min-height: 50px; 103 max-height: 200px; 104 padding: 50px 0 0 0; 105 margin-top: -25px; 106 width: 100%; 107 } 108 .map { 109 width: 20px; 110 height: 20px; 111 display: inline-block; 112 border-radius: 50%; 113 background-color: black; 114 border: 4px solid white; 115 font-size: 10px; 116 text-align: center; 117 line-height: 18px; 118 color: white; 119 vertical-align: top; 120 margin-top: -13px; 121 /* raise z-index */ 122 position: relative; 123 z-index: 2; 124 cursor: pointer; 125 } 126 .map.selected { 127 border-color: black; 128 } 129 .transitions { 130 display: inline-block; 131 margin-left: -15px; 132 } 133 .transition { 134 min-height: 55px; 135 margin: 0 0 -2px 2px; 136 } 137 /* gray out deprecated transitions */ 138 .deprecated > .transitionEdge, 139 .deprecated > .map { 140 opacity: 0.5; 141 } 142 .deprecated > .transition { 143 border-color: rgba(0, 0, 0, 0.5); 144 } 145 /* Show a border for all but the first transition */ 146 .transition:nth-of-type(2), 147 .transition:nth-last-of-type(n+2) { 148 border-left: 2px solid; 149 margin-left: 0px; 150 } 151 /* special case for 2 transitions */ 152 .transition:nth-last-of-type(1) { 153 border-left: none; 154 } 155 /* topmost transitions are not related */ 156 #transitionView > .transition { 157 border-left: none; 158 } 159 /* topmost transition edge needs initial offset to be aligned */ 160 #transitionView > .transition > .transitionEdge { 161 margin-left: 13px; 162 } 163 .transitionEdge { 164 height: 2px; 165 width: 80px; 166 display: inline-block; 167 margin: 0 0 2px 0; 168 background-color: black; 169 vertical-align: top; 170 padding-left: 15px; 171 } 172 .transitionLabel { 173 color: black; 174 transform: rotate(-15deg); 175 transform-origin: top left; 176 margin-top: -10px; 177 font-size: 10px; 178 white-space: normal; 179 word-break: break-all; 180 background-color: rgba(255,255,255,0.5); 181 } 182 .red { 183 background-color: red; 184 } 185 .green { 186 background-color: green; 187 } 188 .yellow { 189 background-color: yellow; 190 color: black; 191 } 192 .blue { 193 background-color: blue; 194 } 195 .orange { 196 background-color: orange; 197 } 198 .violet { 199 background-color: violet; 200 color: black; 201 } 202 .showSubtransitions { 203 width: 0; 204 height: 0; 205 border-left: 6px solid transparent; 206 border-right: 6px solid transparent; 207 border-top: 10px solid black; 208 cursor: zoom-in; 209 margin: 4px 0 0 4px; 210 } 211 .showSubtransitions.opened { 212 border-top: none; 213 border-bottom: 10px solid black; 214 cursor: zoom-out; 215 } 216 #tooltip { 217 position: absolute; 218 width: 10px; 219 height: 10px; 220 background-color: red; 221 pointer-events: none; 222 z-index: 100; 223 display: none; 224 } 225 </style> 226 <script src="./splaytree.js"></script> 227 <script src="./codemap.js"></script> 228 <script src="./csvparser.js"></script> 229 <script src="./consarray.js"></script> 230 <script src="./profile.js"></script> 231 <script src="./profile_view.js"></script> 232 <script src="./logreader.js"></script> 233 <script src="./SourceMap.js"></script> 234 <script src="./arguments.js"></script> 235 <script src="./map-processor.js"></script> 236 <script> 237 "use strict" 238 // ========================================================================= 239 const kChunkHeight = 250; 240 const kChunkWidth = 10; 241 242 class State { 243 constructor() { 244 this._nofChunks = 400; 245 this._map = undefined; 246 this._timeline = undefined; 247 this._chunks = undefined; 248 this._view = new View(this); 249 this._navigation = new Navigation(this, this.view); 250 } 251 get timeline() { return this._timeline } 252 set timeline(value) { 253 this._timeline = value; 254 this.updateChunks(); 255 this.view.updateTimeline(); 256 this.view.updateStats(); 257 } 258 get chunks() { return this._chunks } 259 get nofChunks() { return this._nofChunks } 260 set nofChunks(count) { 261 this._nofChunks = count; 262 this.updateChunks(); 263 this.view.updateTimeline(); 264 } 265 get view() { return this._view } 266 get navigation() { return this._navigation } 267 get map() { return this._map } 268 set map(value) { 269 this._map = value; 270 this._navigation.updateUrl(); 271 this.view.updateMapDetails(); 272 this.view.redraw(); 273 } 274 updateChunks() { 275 this._chunks = this._timeline.chunks(this._nofChunks); 276 } 277 get entries() { 278 if (!this.map) return {}; 279 return { 280 map: this.map.id, 281 time: this.map.time 282 } 283 } 284 } 285 286 // ========================================================================= 287 // DOM Helper 288 function $(id) { 289 return document.getElementById(id) 290 } 291 292 function removeAllChildren(node) { 293 while (node.lastChild) { 294 node.removeChild(node.lastChild); 295 } 296 } 297 298 function selectOption(select, match) { 299 let options = select.options; 300 for (let i = 0; i < options.length; i++) { 301 if (match(i, options[i])) { 302 select.selectedIndex = i; 303 return; 304 } 305 } 306 } 307 308 function div(classes) { 309 let node = document.createElement('div'); 310 if (classes !== void 0) { 311 if (typeof classes == "string") { 312 node.classList.add(classes); 313 } else { 314 classes.forEach(cls => node.classList.add(cls)); 315 } 316 } 317 return node; 318 } 319 320 function table(className) { 321 let node = document.createElement("table") 322 if (className) node.classList.add(className) 323 return node; 324 } 325 function td(text) { 326 let node = document.createElement("td"); 327 node.innerText = text; 328 return node; 329 } 330 function tr() { 331 let node = document.createElement("tr"); 332 return node; 333 } 334 335 function define(prototype, name, fn) { 336 Object.defineProperty(prototype, name, {value:fn, enumerable:false}); 337 } 338 339 define(Array.prototype, "max", function(fn) { 340 if (this.length == 0) return undefined; 341 if (fn == undefined) fn = (each) => each; 342 let max = fn(this[0]); 343 for (let i = 1; i < this.length; i++) { 344 max = Math.max(max, fn(this[i])); 345 } 346 return max; 347 }) 348 define(Array.prototype, "histogram", function(mapFn) { 349 let histogram = []; 350 for (let i = 0; i < this.length; i++) { 351 let value = this[i]; 352 let index = Math.round(mapFn(value)) 353 let bucket = histogram[index]; 354 if (bucket !== undefined) { 355 bucket.push(value); 356 } else { 357 histogram[index] = [value]; 358 } 359 } 360 for (let i = 0; i < histogram.length; i++) { 361 histogram[i] = histogram[i] || []; 362 } 363 return histogram; 364 }); 365 366 define(Array.prototype, "first", function() { return this[0] }); 367 define(Array.prototype, "last", function() { return this[this.length - 1] }); 368 369 // ========================================================================= 370 // EventHandlers 371 function handleBodyLoad() { 372 let upload = $('uploadInput'); 373 upload.onclick = (e) => { e.target.value = null }; 374 upload.onchange = (e) => { handleLoadFile(e.target) }; 375 upload.focus(); 376 377 document.state = new State(); 378 $("transitionView").addEventListener("mousemove", e => { 379 let tooltip = $("tooltip"); 380 tooltip.style.left = e.pageX + "px"; 381 tooltip.style.top = e.pageY + "px"; 382 let map = e.target.map; 383 if (map) { 384 $("tooltipContents").innerText = map.description.join("\n"); 385 } 386 }); 387 } 388 389 function handleLoadFile(upload) { 390 let files = upload.files; 391 let file = files[0]; 392 let reader = new FileReader(); 393 reader.onload = function(evt) { 394 handleLoadText(this.result); 395 } 396 reader.readAsText(file); 397 } 398 399 function handleLoadText(text) { 400 let mapProcessor = new MapProcessor(); 401 document.state.timeline = mapProcessor.processString(text); 402 } 403 404 function handleKeyDown(event) { 405 let nav = document.state.navigation; 406 switch(event.key) { 407 case "ArrowUp": 408 event.preventDefault(); 409 if (event.shiftKey) { 410 nav.selectPrevEdge(); 411 } else { 412 nav.moveInChunk(-1); 413 } 414 return false; 415 case "ArrowDown": 416 event.preventDefault(); 417 if (event.shiftKey) { 418 nav.selectNextEdge(); 419 } else { 420 nav.moveInChunk(1); 421 } 422 return false; 423 case "ArrowLeft": 424 nav.moveInChunks(false); 425 break; 426 case "ArrowRight": 427 nav.moveInChunks(true); 428 break; 429 case "+": 430 nav.increaseTimelineResolution(); 431 break; 432 case "-": 433 nav.decreaseTimelineResolution(); 434 break; 435 } 436 }; 437 document.onkeydown = handleKeyDown; 438 439 function handleTimelineIndicatorMove(event) { 440 if (event.buttons == 0) return; 441 let timelineTotalWidth = $("timelineCanvas").offsetWidth; 442 let factor = $("timelineOverview").offsetWidth / timelineTotalWidth; 443 $("timeline").scrollLeft += event.movementX / factor; 444 } 445 446 // ========================================================================= 447 448 Object.defineProperty(Edge.prototype, 'getColor', { value:function() { 449 return transitionTypeToColor(this.type); 450 }}); 451 452 class Navigation { 453 constructor(state, view) { 454 this.state = state; 455 this.view = view; 456 } 457 get map() { return this.state.map } 458 set map(value) { this.state.map = value } 459 get chunks() { return this.state.chunks } 460 461 increaseTimelineResolution() { 462 this.state.nofChunks *= 1.5; 463 } 464 465 decreaseTimelineResolution() { 466 this.state.nofChunks /= 1.5; 467 } 468 469 selectNextEdge() { 470 if (!this.map) return; 471 if (this.map.children.length != 1) return; 472 this.map = this.map.children[0].to; 473 } 474 475 selectPrevEdge() { 476 if (!this.map) return; 477 if (!this.map.parent()) return; 478 this.map = this.map.parent(); 479 } 480 481 selectDefaultMap() { 482 this.map = this.chunks[0].at(0); 483 } 484 moveInChunks(next) { 485 if (!this.map) return this.selectDefaultMap(); 486 let chunkIndex = this.map.chunkIndex(this.chunks); 487 let chunk = this.chunks[chunkIndex]; 488 let index = chunk.indexOf(this.map); 489 if (next) { 490 chunk = chunk.next(this.chunks); 491 } else { 492 chunk = chunk.prev(this.chunks); 493 } 494 if (!chunk) return; 495 index = Math.min(index, chunk.size()-1); 496 this.map = chunk.at(index); 497 } 498 499 moveInChunk(delta) { 500 if (!this.map) return this.selectDefaultMap(); 501 let chunkIndex = this.map.chunkIndex(this.chunks) 502 let chunk = this.chunks[chunkIndex]; 503 let index = chunk.indexOf(this.map) + delta; 504 let map; 505 if (index < 0) { 506 map = chunk.prev(this.chunks).last(); 507 } else if (index >= chunk.size()) { 508 map = chunk.next(this.chunks).first() 509 } else { 510 map = chunk.at(index); 511 } 512 this.map = map; 513 } 514 515 updateUrl() { 516 let entries = this.state.entries; 517 let params = new URLSearchParams(entries); 518 window.history.pushState(entries, "", "?" + params.toString()); 519 } 520 } 521 522 class View { 523 constructor(state) { 524 this.state = state; 525 setInterval(this.updateOverviewWindow, 50); 526 this.backgroundCanvas = document.createElement("canvas"); 527 this.transitionView = new TransitionView(state, $("transitionView")); 528 this.statsView = new StatsView(state, $("stats")); 529 this.isLocked = false; 530 } 531 get chunks() { return this.state.chunks } 532 get timeline() { return this.state.timeline } 533 get map() { return this.state.map } 534 535 updateStats() { 536 this.statsView.update(); 537 } 538 539 updateMapDetails() { 540 let details = ""; 541 if (this.map) { 542 details += "ID: " + this.map.id; 543 details += "\n" + this.map.description; 544 } 545 $("mapDetails").innerText = details; 546 this.transitionView.showMap(this.map); 547 } 548 549 updateTimeline() { 550 let chunksNode = $("timelineChunks"); 551 removeAllChildren(chunksNode); 552 let chunks = this.chunks; 553 let max = chunks.max(each => each.size()); 554 let start = this.timeline.startTime; 555 let end = this.timeline.endTime; 556 let duration = end - start; 557 const timeToPixel = chunks.length * kChunkWidth / duration; 558 let addTimestamp = (time, name) => { 559 let timeNode = div("timestamp"); 560 timeNode.innerText = name; 561 timeNode.style.left = ((time-start) * timeToPixel) + "px"; 562 chunksNode.appendChild(timeNode); 563 }; 564 for (let i = 0; i < chunks.length; i++) { 565 let chunk = chunks[i]; 566 let height = (chunk.size() / max * kChunkHeight); 567 chunk.height = height; 568 if (chunk.isEmpty()) continue; 569 let node = div(); 570 node.className = "chunk"; 571 node.style.left = (i * kChunkWidth) + "px"; 572 node.style.height = height + "px"; 573 node.chunk = chunk; 574 node.addEventListener("mousemove", e => this.handleChunkMouseMove(e)); 575 node.addEventListener("click", e => this.handleChunkClick(e)); 576 node.addEventListener("dblclick", e => this.handleChunkDoubleClick(e)); 577 this.setTimelineChunkBackground(chunk, node); 578 chunksNode.appendChild(node); 579 chunk.markers.forEach(marker => addTimestamp(marker.time, marker.name)); 580 } 581 // Put a time marker roughly every 20 chunks. 582 let expected = duration / chunks.length * 20; 583 let interval = (10 ** Math.floor(Math.log10(expected))); 584 let correction = Math.log10(expected / interval); 585 correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5; 586 interval *= correction; 587 588 let time = start; 589 while (time < end) { 590 addTimestamp(time, ((time-start) / 1000) + " ms"); 591 time += interval; 592 } 593 this.drawOverview(); 594 this.drawHistograms(); 595 this.redraw(); 596 } 597 598 handleChunkMouseMove(event) { 599 if (this.isLocked) return false; 600 let chunk = event.target.chunk; 601 if (!chunk) return; 602 // topmost map (at chunk.height) == map #0. 603 let relativeIndex = 604 Math.round(event.layerY / event.target.offsetHeight * chunk.size()); 605 let map = chunk.at(relativeIndex); 606 this.state.map = map; 607 } 608 609 handleChunkClick(event) { 610 this.isLocked = !this.isLocked; 611 } 612 613 handleChunkDoubleClick(event) { 614 this.isLocked = true; 615 let chunk = event.target.chunk; 616 if (!chunk) return; 617 this.transitionView.showMaps(chunk.getUniqueTransitions()); 618 } 619 620 setTimelineChunkBackground(chunk, node) { 621 // Render the types of transitions as bar charts 622 const kHeight = chunk.height; 623 const kWidth = 1; 624 this.backgroundCanvas.width = kWidth; 625 this.backgroundCanvas.height = kHeight; 626 let ctx = this.backgroundCanvas.getContext("2d"); 627 ctx.clearRect(0, 0, kWidth, kHeight); 628 let y = 0; 629 let total = chunk.size(); 630 let type, count; 631 if (true) { 632 chunk.getTransitionBreakdown().forEach(([type, count]) => { 633 ctx.fillStyle = transitionTypeToColor(type); 634 let height = count / total * kHeight; 635 ctx.fillRect(0, y, kWidth, y + height); 636 y += height; 637 }); 638 } else { 639 chunk.items.forEach(map => { 640 ctx.fillStyle = transitionTypeToColor(map.getType()); 641 let y = chunk.yOffset(map); 642 ctx.fillRect(0, y, kWidth, y + 1); 643 }); 644 } 645 646 let imageData = this.backgroundCanvas.toDataURL("image/png"); 647 node.style.backgroundImage = "url(" + imageData + ")"; 648 } 649 650 updateOverviewWindow() { 651 let indicator = $("timelineOverviewIndicator"); 652 let totalIndicatorWidth = $("timelineOverview").offsetWidth; 653 let div = $("timeline"); 654 let timelineTotalWidth = $("timelineCanvas").offsetWidth; 655 let factor = $("timelineOverview").offsetWidth / timelineTotalWidth; 656 let width = div.offsetWidth * factor; 657 let left = div.scrollLeft * factor; 658 indicator.style.width = width + "px"; 659 indicator.style.left = left + "px"; 660 } 661 662 drawOverview() { 663 const height = 50; 664 const kFactor = 2; 665 let canvas = this.backgroundCanvas; 666 canvas.height = height; 667 canvas.width = window.innerWidth; 668 let ctx = canvas.getContext("2d"); 669 670 let chunks = this.state.timeline.chunkSizes(canvas.width * kFactor); 671 let max = chunks.max(); 672 673 ctx.clearRect(0, 0, canvas.width, height); 674 ctx.strokeStyle = "black"; 675 ctx.fillStyle = "black"; 676 ctx.beginPath(); 677 ctx.moveTo(0,height); 678 for (let i = 0; i < chunks.length; i++) { 679 ctx.lineTo(i/kFactor, height - chunks[i]/max * height); 680 } 681 ctx.lineTo(chunks.length, height); 682 ctx.stroke(); 683 ctx.closePath(); 684 ctx.fill(); 685 let imageData = canvas.toDataURL("image/png"); 686 $("timelineOverview").style.backgroundImage = "url(" + imageData + ")"; 687 } 688 689 drawHistograms() { 690 $("mapsDepthHistogram").histogram = this.timeline.depthHistogram(); 691 $("mapsFanOutHistogram").histogram = this.timeline.fanOutHistogram(); 692 } 693 694 drawMapsDepthHistogram() { 695 let canvas = $("mapsDepthCanvas"); 696 let histogram = this.timeline.depthHistogram(); 697 this.drawHistogram(canvas, histogram, true); 698 } 699 700 drawMapsFanOutHistogram() { 701 let canvas = $("mapsFanOutCanvas"); 702 let histogram = this.timeline.fanOutHistogram(); 703 this.drawHistogram(canvas, histogram, true, true); 704 } 705 706 drawHistogram(canvas, histogram, logScaleX=false, logScaleY=false) { 707 let ctx = canvas.getContext("2d"); 708 let yMax = histogram.max(each => each.length); 709 if (logScaleY) yMax = Math.log(yMax); 710 let xMax = histogram.length; 711 if (logScaleX) xMax = Math.log(xMax); 712 ctx.clearRect(0, 0, canvas.width, canvas.height); 713 ctx.beginPath(); 714 ctx.moveTo(0,canvas.height); 715 for (let i = 0; i < histogram.length; i++) { 716 let x = i; 717 if (logScaleX) x = Math.log(x); 718 x = x / xMax * canvas.width; 719 let bucketLength = histogram[i].length; 720 if (logScaleY) bucketLength = Math.log(bucketLength); 721 let y = (1 - bucketLength / yMax) * canvas.height; 722 ctx.lineTo(x, y); 723 } 724 ctx.lineTo(canvas.width, canvas.height); 725 ctx.closePath; 726 ctx.stroke(); 727 ctx.fill(); 728 } 729 730 redraw() { 731 let canvas= $("timelineCanvas"); 732 canvas.width = (this.chunks.length+1) * kChunkWidth; 733 canvas.height = kChunkHeight; 734 let ctx = canvas.getContext("2d"); 735 ctx.clearRect(0, 0, canvas.width, kChunkHeight); 736 if (!this.state.map) return; 737 this.drawEdges(ctx); 738 } 739 740 setMapStyle(map, ctx) { 741 ctx.fillStyle = map.edge && map.edge.from ? "black" : "green"; 742 } 743 744 setEdgeStyle(edge, ctx) { 745 let color = edge.getColor(); 746 ctx.strokeStyle = color; 747 ctx.fillStyle = color; 748 } 749 750 markMap(ctx, map) { 751 let [x, y] = map.position(this.state.chunks); 752 ctx.beginPath(); 753 this.setMapStyle(map, ctx); 754 ctx.arc(x, y, 3, 0, 2 * Math.PI); 755 ctx.fill(); 756 ctx.beginPath(); 757 ctx.fillStyle = "white"; 758 ctx.arc(x, y, 2, 0, 2 * Math.PI); 759 ctx.fill(); 760 } 761 762 markSelectedMap(ctx, map) { 763 let [x, y] = map.position(this.state.chunks); 764 ctx.beginPath(); 765 this.setMapStyle(map, ctx); 766 ctx.arc(x, y, 6, 0, 2 * Math.PI); 767 ctx.stroke(); 768 } 769 770 drawEdges(ctx) { 771 // Draw the trace of maps in reverse order to make sure the outgoing 772 // transitions of previous maps aren't drawn over. 773 const kMaxOutgoingEdges = 100; 774 let nofEdges = 0; 775 let stack = []; 776 let current = this.state.map; 777 while (current && nofEdges < kMaxOutgoingEdges) { 778 nofEdges += current.children.length; 779 stack.push(current); 780 current = current.parent(); 781 } 782 ctx.save(); 783 this.drawOutgoingEdges(ctx, this.state.map, 3); 784 ctx.restore(); 785 786 let labelOffset = 15; 787 let xPrev = 0; 788 while (current = stack.pop()) { 789 if (current.edge) { 790 this.setEdgeStyle(current.edge, ctx); 791 let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset); 792 if (xTo == xPrev) { 793 labelOffset += 8; 794 } else { 795 labelOffset = 15 796 } 797 xPrev = xTo; 798 } 799 this.markMap(ctx, current); 800 current = current.parent(); 801 ctx.save(); 802 // this.drawOutgoingEdges(ctx, current, 1); 803 ctx.restore(); 804 } 805 // Mark selected map 806 this.markSelectedMap(ctx, this.state.map); 807 } 808 809 drawEdge(ctx, edge, showLabel=true, labelOffset=20) { 810 if (!edge.from || !edge.to) return [-1, -1]; 811 let [xFrom, yFrom] = edge.from.position(this.chunks); 812 let [xTo, yTo] = edge.to.position(this.chunks); 813 let sameChunk = xTo == xFrom; 814 if (sameChunk) labelOffset += 8; 815 816 ctx.beginPath(); 817 ctx.moveTo(xFrom, yFrom); 818 let offsetX = 20; 819 let offsetY = 20; 820 let midX = xFrom + (xTo- xFrom) / 2; 821 let midY = (yFrom + yTo) / 2 - 100; 822 if (!sameChunk) { 823 ctx.quadraticCurveTo(midX, midY, xTo, yTo); 824 } else { 825 ctx.lineTo(xTo, yTo); 826 } 827 if (!showLabel) { 828 ctx.stroke(); 829 } else { 830 let centerX, centerY; 831 if (!sameChunk) { 832 centerX = (xFrom/2 + midX + xTo/2)/2; 833 centerY = (yFrom/2 + midY + yTo/2)/2; 834 } else { 835 centerX = xTo; 836 centerY = yTo; 837 } 838 ctx.moveTo(centerX, centerY); 839 ctx.lineTo(centerX + offsetX, centerY - labelOffset); 840 ctx.stroke(); 841 ctx.textAlign = "left"; 842 ctx.fillText(edge.toString(), centerX + offsetX + 2, centerY - labelOffset) 843 } 844 return [xTo, yTo]; 845 } 846 847 drawOutgoingEdges(ctx, map, max=10, depth=0) { 848 if (!map) return; 849 if (depth >= max) return; 850 ctx.globalAlpha = 0.5 - depth * (0.3/max); 851 ctx.strokeStyle = "#666"; 852 853 const limit = Math.min(map.children.length, 100) 854 for (let i = 0; i < limit; i++) { 855 let edge = map.children[i]; 856 this.drawEdge(ctx, edge, true); 857 this.drawOutgoingEdges(ctx, edge.to, max, depth+1); 858 } 859 } 860 } 861 862 863 class TransitionView { 864 constructor(state, node) { 865 this.state = state; 866 this.container = node; 867 this.currentNode = node; 868 this.currentMap = undefined; 869 } 870 871 selectMap(map) { 872 this.currentMap = map; 873 this.state.map = map; 874 } 875 876 showMap(map) { 877 if (this.currentMap === map) return; 878 this.currentMap = map; 879 this._showMaps([map]); 880 } 881 882 showMaps(list, name) { 883 this.state.view.isLocked = true; 884 this._showMaps(list); 885 } 886 887 _showMaps(list, name) { 888 // Hide the container to avoid any layouts. 889 this.container.style.display = "none"; 890 removeAllChildren(this.container); 891 list.forEach(map => this.addMapAndParentTransitions(map)); 892 this.container.style.display = "" 893 } 894 895 addMapAndParentTransitions(map) { 896 if (map === void 0) return; 897 this.currentNode = this.container; 898 let parents = map.getParents(); 899 if (parents.length > 0) { 900 this.addTransitionTo(parents.pop()); 901 parents.reverse().forEach(each => this.addTransitionTo(each)); 902 } 903 let mapNode = this.addSubtransitions(map); 904 // Mark and show the selected map. 905 mapNode.classList.add("selected"); 906 if (this.selectedMap == map) { 907 setTimeout(() => mapNode.scrollIntoView({ 908 behavior: "smooth", block: "nearest", inline: "nearest" 909 }), 1); 910 } 911 } 912 913 addMapNode(map) { 914 let node = div("map"); 915 if (map.edge) node.classList.add(map.edge.getColor()); 916 node.map = map; 917 node.addEventListener("click", () => this.selectMap(map)); 918 if (map.children.length > 1) { 919 node.innerText = map.children.length; 920 let showSubtree = div("showSubtransitions"); 921 showSubtree.addEventListener("click", (e) => this.toggleSubtree(e, node)); 922 node.appendChild(showSubtree); 923 } else if (map.children.length == 0) { 924 node.innerHTML = "●" 925 } 926 this.currentNode.appendChild(node); 927 return node; 928 } 929 930 addSubtransitions(map) { 931 let mapNode = this.addTransitionTo(map); 932 // Draw outgoing linear transition line. 933 let current = map; 934 while (current.children.length == 1) { 935 current = current.children[0].to; 936 this.addTransitionTo(current); 937 } 938 return mapNode; 939 } 940 941 addTransitionEdge(map) { 942 let classes = ["transitionEdge", map.edge.getColor()]; 943 let edge = div(classes); 944 let labelNode = div("transitionLabel"); 945 labelNode.innerText = map.edge.toString(); 946 edge.appendChild(labelNode); 947 return edge; 948 } 949 950 addTransitionTo(map) { 951 // transition[ transitions[ transition[...], transition[...], ...]]; 952 953 let transition = div("transition"); 954 if (map.isDeprecated()) transition.classList.add("deprecated"); 955 if (map.edge) { 956 transition.appendChild(this.addTransitionEdge(map)); 957 } 958 let mapNode = this.addMapNode(map); 959 transition.appendChild(mapNode); 960 961 let subtree = div("transitions"); 962 transition.appendChild(subtree); 963 964 this.currentNode.appendChild(transition); 965 this.currentNode = subtree; 966 967 return mapNode; 968 969 } 970 971 toggleSubtree(event, node) { 972 let map = node.map; 973 event.target.classList.toggle("opened"); 974 let transitionsNode = node.parentElement.querySelector(".transitions"); 975 let subtransitionNodes = transitionsNode.children; 976 if (subtransitionNodes.length <= 1) { 977 // Add subtransitions excepth the one that's already shown. 978 let visibleTransitionMap = subtransitionNodes.length == 1 ? 979 transitionsNode.querySelector(".map").map : void 0; 980 map.children.forEach(edge => { 981 if (edge.to != visibleTransitionMap) { 982 this.currentNode = transitionsNode; 983 this.addSubtransitions(edge.to); 984 } 985 }); 986 } else { 987 // remove all but the first (currently selected) subtransition 988 for (let i = subtransitionNodes.length-1; i > 0; i--) { 989 transitionsNode.removeChild(subtransitionNodes[i]); 990 } 991 } 992 } 993 } 994 995 class StatsView { 996 constructor(state, node) { 997 this.state = state; 998 this.node = node; 999 } 1000 get timeline() { return this.state.timeline } 1001 get transitionView() { return this.state.view.transitionView; } 1002 update() { 1003 removeAllChildren(this.node); 1004 this.updateGeneralStats(); 1005 this.updateNamedTransitionsStats(); 1006 } 1007 updateGeneralStats() { 1008 let pairs = [ 1009 ["Maps", e => true], 1010 ["Transitions", e => e.edge && e.edge.isTransition()], 1011 ["Fast to Slow", e => e.edge && e.edge.isFastToSlow()], 1012 ["Slow to Fast", e => e.edge && e.edge.isSlowToFast()], 1013 ["Initial Map", e => e.edge && e.edge.isInitial()], 1014 ["Replace Descriptors", e => e.edge && e.edge.isReplaceDescriptors()], 1015 ["Copy as Prototype", e => e.edge && e.edge.isCopyAsPrototype()], 1016 ["Optimize as Prototype", e => e.edge && e.edge.isOptimizeAsPrototype()], 1017 ["Deprecated", e => e.isDeprecated()], 1018 ]; 1019 1020 let text = ""; 1021 let tableNode = table(); 1022 let name, filter; 1023 let total = this.timeline.size(); 1024 pairs.forEach(([name, filter]) => { 1025 let row = tr(); 1026 row.maps = this.timeline.filterUniqueTransitions(filter); 1027 row.addEventListener("click", 1028 e => this.transitionView.showMaps(e.target.parentNode.maps)); 1029 row.appendChild(td(name)); 1030 let count = this.timeline.count(filter); 1031 row.appendChild(td(count)); 1032 let percent = Math.round(count / total * 1000) / 10; 1033 row.appendChild(td(percent + "%")); 1034 tableNode.appendChild(row); 1035 }); 1036 this.node.appendChild(tableNode); 1037 }; 1038 updateNamedTransitionsStats() { 1039 let tableNode = table("transitionTable"); 1040 let nameMapPairs = Array.from(this.timeline.transitions.entries()); 1041 nameMapPairs 1042 .sort((a,b) => b[1].length - a[1].length) 1043 .forEach(([name, maps]) => { 1044 let row = tr(); 1045 row.maps = maps; 1046 row.addEventListener("click", 1047 e => this.transitionView.showMaps( 1048 e.target.parentNode.maps.map(map => map.to))); 1049 row.appendChild(td(name)); 1050 row.appendChild(td(maps.length)); 1051 tableNode.appendChild(row); 1052 }); 1053 this.node.appendChild(tableNode); 1054 } 1055 } 1056 1057 // ========================================================================= 1058 1059 function transitionTypeToColor(type) { 1060 switch(type) { 1061 case "new": return "green"; 1062 case "Normalize": return "violet"; 1063 case "map=SlowToFast": return "orange"; 1064 case "InitialMap": return "yellow"; 1065 case "Transition": return "black"; 1066 case "ReplaceDescriptors": return "red"; 1067 } 1068 return "black"; 1069 } 1070 1071 // ShadowDom elements ========================================================= 1072 customElements.define('x-histogram', class extends HTMLElement { 1073 constructor() { 1074 super(); 1075 let shadowRoot = this.attachShadow({mode: 'open'}); 1076 const t = document.querySelector('#x-histogram-template'); 1077 const instance = t.content.cloneNode(true); 1078 shadowRoot.appendChild(instance); 1079 this._histogram = undefined; 1080 this.mouseX = 0; 1081 this.mouseY = 0; 1082 this.canvas.addEventListener('mousemove', event => this.handleCanvasMove(event)); 1083 } 1084 setBoolAttribute(name, value) { 1085 if (value) { 1086 this.setAttribute(name, ""); 1087 } else { 1088 this.deleteAttribute(name); 1089 } 1090 } 1091 static get observedAttributes() { 1092 return ['title', 'xlog', 'ylog', 'xlabel', 'ylabel']; 1093 } 1094 $(query) { return this.shadowRoot.querySelector(query) } 1095 get h1() { return this.$("h2") } 1096 get canvas() { return this.$("canvas") } 1097 get xLabelDiv() { return this.$("#xLabel") } 1098 get yLabelDiv() { return this.$("#yLabel") } 1099 1100 get histogram() { 1101 return this._histogram; 1102 } 1103 set histogram(array) { 1104 this._histogram = array; 1105 if (this._histogram) { 1106 this.yMax = this._histogram.max(each => each.length); 1107 this.xMax = this._histogram.length; 1108 } 1109 this.draw(); 1110 } 1111 1112 get title() { return this.getAttribute("title") } 1113 set title(string) { this.setAttribute("title", string) } 1114 get xLabel() { return this.getAttribute("xlabel") } 1115 set xLabel(string) { this.setAttribute("xlabel", string)} 1116 get yLabel() { return this.getAttribute("ylabel") } 1117 set yLabel(string) { this.setAttribute("ylabel", string)} 1118 get xLog() { return this.hasAttribute("xlog") } 1119 set xLog(value) { this.setBoolAttribute("xlog", value) } 1120 get yLog() { return this.hasAttribute("ylog") } 1121 set yLog(value) { this.setBoolAttribute("ylog", value) } 1122 1123 attributeChangedCallback(name, oldValue, newValue) { 1124 if (name == "title") { 1125 this.h1.innerText = newValue; 1126 return; 1127 } 1128 if (name == "ylabel") { 1129 this.yLabelDiv.innerText = newValue; 1130 return; 1131 } 1132 if (name == "xlabel") { 1133 this.xLabelDiv.innerText = newValue; 1134 return; 1135 } 1136 this.draw(); 1137 } 1138 1139 handleCanvasMove(event) { 1140 this.mouseX = event.offsetX; 1141 this.mouseY = event.offsetY; 1142 this.draw(); 1143 } 1144 xPosition(i) { 1145 let x = i; 1146 if (this.xLog) x = Math.log(x); 1147 return x / this.xMax * this.canvas.width; 1148 } 1149 yPosition(i) { 1150 let bucketLength = this.histogram[i].length; 1151 if (this.yLog) { 1152 return (1 - Math.log(bucketLength) / Math.log(this.yMax)) * this.drawHeight + 10; 1153 } else { 1154 return (1 - bucketLength / this.yMax) * this.drawHeight + 10; 1155 } 1156 } 1157 1158 get drawHeight() { return this.canvas.height - 10 } 1159 1160 draw() { 1161 if (!this.histogram) return; 1162 let width = this.canvas.width; 1163 let height = this.drawHeight; 1164 let ctx = this.canvas.getContext("2d"); 1165 if (this.xLog) yMax = Math.log(yMax); 1166 let xMax = this.histogram.length; 1167 if (this.yLog) xMax = Math.log(xMax); 1168 ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 1169 ctx.beginPath(); 1170 ctx.moveTo(0, height); 1171 for (let i = 0; i < this.histogram.length; i++) { 1172 ctx.lineTo(this.xPosition(i), this.yPosition(i)); 1173 } 1174 ctx.lineTo(width, height); 1175 ctx.closePath; 1176 ctx.stroke(); 1177 ctx.fill(); 1178 if (!this.mouseX) return; 1179 ctx.beginPath(); 1180 let index = Math.round(this.mouseX); 1181 let yBucket = this.histogram[index]; 1182 let y = this.yPosition(index); 1183 if (this.yLog) y = Math.log(y); 1184 ctx.moveTo(0, y); 1185 ctx.lineTo(width-40, y); 1186 ctx.moveTo(this.mouseX, 0); 1187 ctx.lineTo(this.mouseX, height); 1188 ctx.stroke(); 1189 ctx.textAlign = "left"; 1190 ctx.fillText(yBucket.length, width-30, y); 1191 } 1192 }); 1193 1194 </script> 1195 </head> 1196 <template id="x-histogram-template"> 1197 <style> 1198 #yLabel { 1199 transform: rotate(90deg); 1200 } 1201 canvas, #yLabel, #info { float: left; } 1202 #xLabel { clear: both } 1203 </style> 1204 <h2></h2> 1205 <div id="yLabel"></div> 1206 <canvas height=50></canvas> 1207 <div id="info"> 1208 </div> 1209 <div id="xLabel"></div> 1210 </template> 1211 1212 <body onload="handleBodyLoad(event)" onkeypress="handleKeyDown(event)"> 1213 <h2>Data</h2> 1214 <section> 1215 <form name="fileForm"> 1216 <p> 1217 <input id="uploadInput" type="file" name="files"> 1218 </p> 1219 </form> 1220 </section> 1221 1222 <h2>Stats</h2> 1223 <section id="stats"></section> 1224 1225 <h2>Timeline</h2> 1226 <div id="timeline"> 1227 <div id=timelineChunks></div> 1228 <canvas id="timelineCanvas" ></canvas> 1229 </div> 1230 <div id="timelineOverview" 1231 onmousemove="handleTimelineIndicatorMove(event)" > 1232 <div id="timelineOverviewIndicator"> 1233 <div class="leftMask"></div> 1234 <div class="rightMask"></div> 1235 </div> 1236 </div> 1237 1238 <h2>Transitions</h2> 1239 <section id="transitionView"></section> 1240 <br/> 1241 1242 <h2>Selected Map</h2> 1243 <section id="mapDetails"></section> 1244 1245 <x-histogram id="mapsDepthHistogram" 1246 title="Maps Depth" xlabel="depth" ylabel="nof"></x-histogram> 1247 <x-histogram id="mapsFanOutHistogram" xlabel="fan-out" 1248 title="Maps Fan-out" ylabel="nof"></x-histogram> 1249 1250 <div id="tooltip"> 1251 <div id="tooltipContents"></div> 1252 </div> 1253 </body> 1254 </html> 1255