• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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>
10html, body {
11  font-family: sans-serif;
12  padding: 0px;
13  margin: 0px;
14}
15h1, 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// =========================================================================
239const kChunkHeight = 250;
240const kChunkWidth = 10;
241
242class 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
288function $(id) {
289  return document.getElementById(id)
290}
291
292function removeAllChildren(node) {
293  while (node.lastChild) {
294    node.removeChild(node.lastChild);
295  }
296}
297
298function 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
308function 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
320function table(className) {
321  let node = document.createElement("table")
322  if (className) node.classList.add(className)
323  return node;
324}
325function td(text) {
326  let node = document.createElement("td");
327  node.innerText = text;
328  return node;
329}
330function tr() {
331  let node = document.createElement("tr");
332  return node;
333}
334
335function define(prototype, name, fn) {
336  Object.defineProperty(prototype, name, {value:fn, enumerable:false});
337}
338
339define(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})
348define(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
366define(Array.prototype, "first", function() { return this[0] });
367define(Array.prototype, "last", function() { return this[this.length - 1] });
368
369// =========================================================================
370// EventHandlers
371function 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
389function 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
399function handleLoadText(text) {
400  let mapProcessor = new MapProcessor();
401  document.state.timeline = mapProcessor.processString(text);
402}
403
404function 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};
437document.onkeydown = handleKeyDown;
438
439function 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
448Object.defineProperty(Edge.prototype, 'getColor', { value:function() {
449  return transitionTypeToColor(this.type);
450}});
451
452class 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
522class 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
863class 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
995class 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
1059function 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 =========================================================
1072customElements.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