Home | History | Annotate | Download | only in tools
      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 = "&#x25CF;"
    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