1// Copyright 2017 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5"use strict"
6
7function $(id) {
8  return document.getElementById(id);
9}
10
11let components;
12function createViews() {
13  components = [
14    new CallTreeView(),
15    new TimelineView(),
16    new HelpView(),
17    new SummaryView(),
18    new ModeBarView(),
19  ];
20}
21
22function emptyState() {
23  return {
24    file : null,
25    mode : null,
26    currentCodeId : null,
27    start : 0,
28    end : Infinity,
29    timelineSize : {
30      width : 0,
31      height : 0
32    },
33    callTree : {
34      attribution : "js-exclude-bc",
35      categories : "code-type",
36      sort : "time"
37    }
38  };
39}
40
41function setCallTreeState(state, callTreeState) {
42  state = Object.assign({}, state);
43  state.callTree = callTreeState;
44  return state;
45}
46
47let main = {
48  currentState : emptyState(),
49
50  setMode(mode) {
51    if (mode !== main.currentState.mode) {
52
53      function setCallTreeModifiers(attribution, categories, sort) {
54        let callTreeState = Object.assign({}, main.currentState.callTree);
55        callTreeState.attribution = attribution;
56        callTreeState.categories = categories;
57        callTreeState.sort = sort;
58        return callTreeState;
59      }
60
61      let state = Object.assign({}, main.currentState);
62
63      switch (mode) {
64        case "bottom-up":
65          state.callTree =
66              setCallTreeModifiers("js-exclude-bc", "code-type", "time");
67          break;
68        case "top-down":
69          state.callTree =
70              setCallTreeModifiers("js-exclude-bc", "none", "time");
71          break;
72        case "function-list":
73          state.callTree =
74              setCallTreeModifiers("js-exclude-bc", "code-type", "own-time");
75          break;
76      }
77
78      state.mode = mode;
79
80      main.currentState = state;
81      main.delayRender();
82    }
83  },
84
85  setCallTreeAttribution(attribution) {
86    if (attribution !== main.currentState.attribution) {
87      let callTreeState = Object.assign({}, main.currentState.callTree);
88      callTreeState.attribution = attribution;
89      main.currentState = setCallTreeState(main.currentState,  callTreeState);
90      main.delayRender();
91    }
92  },
93
94  setCallTreeSort(sort) {
95    if (sort !== main.currentState.sort) {
96      let callTreeState = Object.assign({}, main.currentState.callTree);
97      callTreeState.sort = sort;
98      main.currentState = setCallTreeState(main.currentState,  callTreeState);
99      main.delayRender();
100    }
101  },
102
103  setCallTreeCategories(categories) {
104    if (categories !== main.currentState.categories) {
105      let callTreeState = Object.assign({}, main.currentState.callTree);
106      callTreeState.categories = categories;
107      main.currentState = setCallTreeState(main.currentState,  callTreeState);
108      main.delayRender();
109    }
110  },
111
112  setViewInterval(start, end) {
113    if (start !== main.currentState.start ||
114        end !== main.currentState.end) {
115      main.currentState = Object.assign({}, main.currentState);
116      main.currentState.start = start;
117      main.currentState.end = end;
118      main.delayRender();
119    }
120  },
121
122  setFile(file) {
123    if (file !== main.currentState.file) {
124      let lastMode = main.currentState.mode || "summary";
125      main.currentState = emptyState();
126      main.currentState.file = file;
127      main.setMode(lastMode);
128      main.delayRender();
129    }
130  },
131
132  setCurrentCode(codeId) {
133    if (codeId !== main.currentState.currentCodeId) {
134      main.currentState = Object.assign({}, main.currentState);
135      main.currentState.currentCodeId = codeId;
136      main.delayRender();
137    }
138  },
139
140  onResize() {
141    main.delayRender();
142  },
143
144  onLoad() {
145    function loadHandler(evt) {
146      let f = evt.target.files[0];
147      if (f) {
148        let reader = new FileReader();
149        reader.onload = function(event) {
150          main.setFile(JSON.parse(event.target.result));
151        };
152        reader.onerror = function(event) {
153          console.error(
154              "File could not be read! Code " + event.target.error.code);
155        };
156        reader.readAsText(f);
157      } else {
158        main.setFile(null);
159      }
160    }
161    $("fileinput").addEventListener(
162        "change", loadHandler, false);
163    createViews();
164  },
165
166  delayRender()  {
167    Promise.resolve().then(() => {
168      for (let c of components) {
169        c.render(main.currentState);
170      }
171    });
172  }
173};
174
175const CATEGORY_COLOR = "#f5f5f5";
176const bucketDescriptors =
177    [ { kinds : [ "JSOPT" ],
178        color : "#64dd17",
179        backgroundColor : "#80e27e",
180        text : "JS Optimized" },
181      { kinds : [ "JSUNOPT", "BC" ],
182        color : "#dd2c00",
183        backgroundColor : "#ff9e80",
184        text : "JS Unoptimized" },
185      { kinds : [ "IC" ],
186        color : "#ff6d00",
187        backgroundColor : "#ffab40",
188        text : "IC" },
189      { kinds : [ "STUB", "BUILTIN", "REGEXP" ],
190        color : "#ffd600",
191        backgroundColor : "#ffea00",
192        text : "Other generated" },
193      { kinds : [ "CPP", "LIB" ],
194        color : "#304ffe",
195        backgroundColor : "#6ab7ff",
196        text : "C++" },
197      { kinds : [ "CPPEXT" ],
198        color : "#003c8f",
199        backgroundColor : "#c0cfff",
200        text : "C++/external" },
201      { kinds : [ "CPPPARSE" ],
202        color : "#aa00ff",
203        backgroundColor : "#ffb2ff",
204        text : "C++/Parser" },
205      { kinds : [ "CPPCOMPBC" ],
206        color : "#43a047",
207        backgroundColor : "#88c399",
208        text : "C++/Bytecode compiler" },
209      { kinds : [ "CPPCOMP" ],
210        color : "#00e5ff",
211        backgroundColor : "#6effff",
212        text : "C++/Compiler" },
213      { kinds : [ "CPPGC" ],
214        color : "#6200ea",
215        backgroundColor : "#e1bee7",
216        text : "C++/GC" },
217      { kinds : [ "UNKNOWN" ],
218        color : "#bdbdbd",
219        backgroundColor : "#efefef",
220        text : "Unknown" }
221    ];
222
223let kindToBucketDescriptor = {};
224for (let i = 0; i < bucketDescriptors.length; i++) {
225  let bucket = bucketDescriptors[i];
226  for (let j = 0; j < bucket.kinds.length; j++) {
227    kindToBucketDescriptor[bucket.kinds[j]] = bucket;
228  }
229}
230
231function bucketFromKind(kind) {
232  for (let i = 0; i < bucketDescriptors.length; i++) {
233    let bucket = bucketDescriptors[i];
234    for (let j = 0; j < bucket.kinds.length; j++) {
235      if (bucket.kinds[j] === kind) {
236        return bucket;
237      }
238    }
239  }
240  return null;
241}
242
243function codeTypeToText(type) {
244  switch (type) {
245    case "UNKNOWN":
246      return "Unknown";
247    case "CPPPARSE":
248      return "C++ Parser";
249    case "CPPCOMPBC":
250      return "C++ Bytecode Compiler)";
251    case "CPPCOMP":
252      return "C++ Compiler";
253    case "CPPGC":
254      return "C++ GC";
255    case "CPPEXT":
256      return "C++ External";
257    case "CPP":
258      return "C++";
259    case "LIB":
260      return "Library";
261    case "IC":
262      return "IC";
263    case "BC":
264      return "Bytecode";
265    case "STUB":
266      return "Stub";
267    case "BUILTIN":
268      return "Builtin";
269    case "REGEXP":
270      return "RegExp";
271    case "JSOPT":
272      return "JS opt";
273    case "JSUNOPT":
274      return "JS unopt";
275  }
276  console.error("Unknown type: " + type);
277}
278
279function createTypeNode(type) {
280  if (type === "CAT") {
281    return document.createTextNode("");
282  }
283  let span = document.createElement("span");
284  span.classList.add("code-type-chip");
285  span.textContent = codeTypeToText(type);
286
287  return span;
288}
289
290function filterFromFilterId(id) {
291  switch (id) {
292    case "full-tree":
293      return (type, kind) => true;
294    case "js-funs":
295      return (type, kind) => type !== 'CODE';
296    case "js-exclude-bc":
297      return (type, kind) =>
298          type !== 'CODE' || kind !== "BytecodeHandler";
299  }
300}
301
302function createIndentNode(indent) {
303  let div = document.createElement("div");
304  div.style.display = "inline-block";
305  div.style.width = (indent + 0.5) + "em";
306  return div;
307}
308
309function createArrowNode() {
310  let span = document.createElement("span");
311  span.classList.add("tree-row-arrow");
312  return span;
313}
314
315function createFunctionNode(name, codeId) {
316  let nameElement = document.createElement("span");
317  nameElement.appendChild(document.createTextNode(name));
318  nameElement.classList.add("tree-row-name");
319  if (codeId !== -1) {
320    nameElement.classList.add("codeid-link");
321    nameElement.onclick = (event) => {
322      main.setCurrentCode(codeId);
323      // Prevent the click from bubbling to the row and causing it to
324      // collapse/expand.
325      event.stopPropagation();
326    };
327  }
328  return nameElement;
329}
330
331const COLLAPSED_ARROW = "\u25B6";
332const EXPANDED_ARROW = "\u25BC";
333
334class CallTreeView {
335  constructor() {
336    this.element = $("calltree");
337    this.treeElement = $("calltree-table");
338    this.selectAttribution = $("calltree-attribution");
339    this.selectCategories = $("calltree-categories");
340    this.selectSort = $("calltree-sort");
341
342    this.selectAttribution.onchange = () => {
343      main.setCallTreeAttribution(this.selectAttribution.value);
344    };
345
346    this.selectCategories.onchange = () => {
347      main.setCallTreeCategories(this.selectCategories.value);
348    };
349
350    this.selectSort.onchange = () => {
351      main.setCallTreeSort(this.selectSort.value);
352    };
353
354    this.currentState = null;
355  }
356
357  sortFromId(id) {
358    switch (id) {
359      case "time":
360        return (c1, c2) => {
361          if (c1.ticks < c2.ticks) return 1;
362          else if (c1.ticks > c2.ticks) return -1;
363          return c2.ownTicks - c1.ownTicks;
364        };
365      case "own-time":
366        return (c1, c2) => {
367          if (c1.ownTicks < c2.ownTicks) return 1;
368          else if (c1.ownTicks > c2.ownTicks) return -1;
369          return c2.ticks - c1.ticks;
370        };
371      case "category-time":
372        return (c1, c2) => {
373          if (c1.type === c2.type) return c2.ticks - c1.ticks;
374          if (c1.type < c2.type) return 1;
375          return -1;
376        };
377      case "category-own-time":
378        return (c1, c2) => {
379          if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks;
380          if (c1.type < c2.type) return 1;
381          return -1;
382        };
383    }
384  }
385
386  expandTree(tree, indent) {
387    let index = 0;
388    let id = "R/";
389    let row = tree.row;
390
391    if (row) {
392      index = row.rowIndex;
393      id = row.id;
394
395      tree.arrow.textContent = EXPANDED_ARROW;
396      // Collapse the children when the row is clicked again.
397      let expandHandler = row.onclick;
398      row.onclick = () => {
399        this.collapseRow(tree, expandHandler);
400      }
401    }
402
403    // Collect the children, and sort them by ticks.
404    let children = [];
405    let filter =
406        filterFromFilterId(this.currentState.callTree.attribution);
407    for (let childId in tree.children) {
408      let child = tree.children[childId];
409      if (child.ticks > 0) {
410        children.push(child);
411        if (child.delayedExpansion) {
412          expandTreeNode(this.currentState.file, child, filter);
413        }
414      }
415    }
416    children.sort(this.sortFromId(this.currentState.callTree.sort));
417
418    for (let i = 0; i < children.length; i++) {
419      let node = children[i];
420      let row = this.rows.insertRow(index);
421      row.id = id + i + "/";
422
423      if (node.type === "CAT") {
424        row.style.backgroundColor = CATEGORY_COLOR;
425      } else {
426        row.style.backgroundColor = bucketFromKind(node.type).backgroundColor;
427      }
428
429      // Inclusive time % cell.
430      let c = row.insertCell();
431      c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%";
432      c.style.textAlign = "right";
433      // Percent-of-parent cell.
434      c = row.insertCell();
435      c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%";
436      c.style.textAlign = "right";
437      // Exclusive time % cell.
438      if (this.currentState.mode !== "bottom-up") {
439        c = row.insertCell(-1);
440        c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%";
441        c.style.textAlign = "right";
442      }
443
444      // Create the name cell.
445      let nameCell = row.insertCell();
446      nameCell.appendChild(createIndentNode(indent + 1));
447      let arrow = createArrowNode();
448      nameCell.appendChild(arrow);
449      nameCell.appendChild(createTypeNode(node.type));
450      nameCell.appendChild(createFunctionNode(node.name, node.codeId));
451
452      // Inclusive ticks cell.
453      c = row.insertCell();
454      c.textContent = node.ticks;
455      c.style.textAlign = "right";
456      if (this.currentState.mode !== "bottom-up") {
457        // Exclusive ticks cell.
458        c = row.insertCell(-1);
459        c.textContent = node.ownTicks;
460        c.style.textAlign = "right";
461      }
462      if (node.children.length > 0) {
463        arrow.textContent = COLLAPSED_ARROW;
464        row.onclick = () => { this.expandTree(node, indent + 1); };
465      }
466
467      node.row = row;
468      node.arrow = arrow;
469
470      index++;
471    }
472  }
473
474  collapseRow(tree, expandHandler) {
475    let row = tree.row;
476    let id = row.id;
477    let index = row.rowIndex;
478    while (row.rowIndex < this.rows.rows.length &&
479        this.rows.rows[index].id.startsWith(id)) {
480      this.rows.deleteRow(index);
481    }
482
483    tree.arrow.textContent = COLLAPSED_ARROW;
484    row.onclick = expandHandler;
485  }
486
487  fillSelects(mode, calltree) {
488    function addOptions(e, values, current) {
489      while (e.options.length > 0) {
490        e.remove(0);
491      }
492      for (let i = 0; i < values.length; i++) {
493        let option = document.createElement("option");
494        option.value = values[i].value;
495        option.textContent = values[i].text;
496        e.appendChild(option);
497      }
498      e.value = current;
499    }
500
501    let attributions = [
502        { value : "js-exclude-bc",
503          text : "Attribute bytecode handlers to caller" },
504        { value : "full-tree",
505          text : "Count each code object separately" },
506        { value : "js-funs",
507          text : "Attribute non-functions to JS functions"  }
508    ];
509
510    switch (mode) {
511      case "bottom-up":
512        addOptions(this.selectAttribution, attributions, calltree.attribution);
513        addOptions(this.selectCategories, [
514            { value : "code-type", text : "Code type" },
515            { value : "none", text : "None" }
516        ], calltree.categories);
517        addOptions(this.selectSort, [
518            { value : "time", text : "Time (including children)" },
519            { value : "category-time", text : "Code category, time" },
520        ], calltree.sort);
521        return;
522      case "top-down":
523        addOptions(this.selectAttribution, attributions, calltree.attribution);
524        addOptions(this.selectCategories, [
525            { value : "none", text : "None" },
526            { value : "rt-entry", text : "Runtime entries" }
527        ], calltree.categories);
528        addOptions(this.selectSort, [
529            { value : "time", text : "Time (including children)" },
530            { value : "own-time", text : "Own time" },
531            { value : "category-time", text : "Code category, time" },
532            { value : "category-own-time", text : "Code category, own time"}
533        ], calltree.sort);
534        return;
535      case "function-list":
536        addOptions(this.selectAttribution, attributions, calltree.attribution);
537        addOptions(this.selectCategories, [
538            { value : "code-type", text : "Code type" },
539            { value : "none", text : "None" }
540        ], calltree.categories);
541        addOptions(this.selectSort, [
542            { value : "own-time", text : "Own time" },
543            { value : "time", text : "Time (including children)" },
544            { value : "category-own-time", text : "Code category, own time"},
545            { value : "category-time", text : "Code category, time" },
546        ], calltree.sort);
547        return;
548    }
549    console.error("Unexpected mode");
550  }
551
552  static isCallTreeMode(mode) {
553    switch (mode) {
554      case "bottom-up":
555      case "top-down":
556      case "function-list":
557        return true;
558      default:
559        return false;
560    }
561  }
562
563  render(newState) {
564    let oldState = this.currentState;
565    if (!newState.file || !CallTreeView.isCallTreeMode(newState.mode)) {
566      this.element.style.display = "none";
567      this.currentState = null;
568      return;
569    }
570
571    this.currentState = newState;
572    if (oldState) {
573      if (newState.file === oldState.file &&
574          newState.start === oldState.start &&
575          newState.end === oldState.end &&
576          newState.mode === oldState.mode &&
577          newState.callTree.attribution === oldState.callTree.attribution &&
578          newState.callTree.categories === oldState.callTree.categories &&
579          newState.callTree.sort === oldState.callTree.sort) {
580        // No change => just return.
581        return;
582      }
583    }
584
585    this.element.style.display = "inherit";
586
587    let mode = this.currentState.mode;
588    if (!oldState || mode !== oldState.mode) {
589      // Technically, we should also call this if attribution, categories or
590      // sort change, but the selection is already highlighted by the combobox
591      // itself, so we do need to do anything here.
592      this.fillSelects(newState.mode, newState.callTree);
593    }
594
595    let ownTimeClass = (mode === "bottom-up") ? "numeric-hidden" : "numeric";
596    let ownTimeTh = $(this.treeElement.id + "-own-time-header");
597    ownTimeTh.classList = ownTimeClass;
598    let ownTicksTh = $(this.treeElement.id + "-own-ticks-header");
599    ownTicksTh.classList = ownTimeClass;
600
601    // Build the tree.
602    let stackProcessor;
603    let filter = filterFromFilterId(this.currentState.callTree.attribution);
604    if (mode === "top-down") {
605      if (this.currentState.callTree.categories === "rt-entry") {
606        stackProcessor =
607            new RuntimeCallTreeProcessor();
608      } else {
609        stackProcessor =
610            new PlainCallTreeProcessor(filter, false);
611      }
612    } else if (mode === "function-list") {
613      stackProcessor = new FunctionListTree(
614          filter, this.currentState.callTree.categories === "code-type");
615
616    } else {
617      console.assert(mode === "bottom-up");
618      if (this.currentState.callTree.categories === "none") {
619        stackProcessor =
620            new PlainCallTreeProcessor(filter, true);
621      } else {
622        console.assert(this.currentState.callTree.categories === "code-type");
623        stackProcessor =
624            new CategorizedCallTreeProcessor(filter, true);
625      }
626    }
627    this.tickCount =
628        generateTree(this.currentState.file,
629                     this.currentState.start,
630                     this.currentState.end,
631                     stackProcessor);
632    // TODO(jarin) Handle the case when tick count is negative.
633
634    this.tree = stackProcessor.tree;
635
636    // Remove old content of the table, replace with new one.
637    let oldRows = this.treeElement.getElementsByTagName("tbody");
638    let newRows = document.createElement("tbody");
639    this.rows = newRows;
640
641    // Populate the table.
642    this.expandTree(this.tree, 0);
643
644    // Swap in the new rows.
645    this.treeElement.replaceChild(newRows, oldRows[0]);
646  }
647}
648
649class TimelineView {
650  constructor() {
651    this.element = $("timeline");
652    this.canvas = $("timeline-canvas");
653    this.legend = $("timeline-legend");
654    this.currentCode = $("timeline-currentCode");
655
656    this.canvas.onmousedown = this.onMouseDown.bind(this);
657    this.canvas.onmouseup = this.onMouseUp.bind(this);
658    this.canvas.onmousemove = this.onMouseMove.bind(this);
659
660    this.selectionStart = null;
661    this.selectionEnd = null;
662    this.selecting = false;
663
664    this.fontSize = 12;
665    this.imageOffset = Math.round(this.fontSize * 1.2);
666    this.functionTimelineHeight = 24;
667    this.functionTimelineTickHeight = 16;
668
669    this.currentState = null;
670  }
671
672  onMouseDown(e) {
673    this.selectionStart =
674        e.clientX - this.canvas.getBoundingClientRect().left;
675    this.selectionEnd = this.selectionStart + 1;
676    this.selecting = true;
677  }
678
679  onMouseMove(e) {
680    if (this.selecting) {
681      this.selectionEnd =
682          e.clientX - this.canvas.getBoundingClientRect().left;
683      this.drawSelection();
684    }
685  }
686
687  onMouseUp(e) {
688    if (this.selectionStart !== null) {
689      let x = e.clientX - this.canvas.getBoundingClientRect().left;
690      if (Math.abs(x - this.selectionStart) < 10) {
691        this.selectionStart = null;
692        this.selectionEnd = null;
693        let ctx = this.canvas.getContext("2d");
694        ctx.drawImage(this.buffer, 0, this.imageOffset);
695      } else {
696        this.selectionEnd = x;
697        this.drawSelection();
698      }
699      let file = this.currentState.file;
700      if (file) {
701        let start = this.selectionStart === null ? 0 : this.selectionStart;
702        let end = this.selectionEnd === null ? Infinity : this.selectionEnd;
703        let firstTime = file.ticks[0].tm;
704        let lastTime = file.ticks[file.ticks.length - 1].tm;
705
706        let width = this.buffer.width;
707
708        start = (start / width) * (lastTime - firstTime) + firstTime;
709        end = (end / width) * (lastTime - firstTime) + firstTime;
710
711        if (end < start) {
712          let temp = start;
713          start = end;
714          end = temp;
715        }
716
717        main.setViewInterval(start, end);
718      }
719    }
720    this.selecting = false;
721  }
722
723  drawSelection() {
724    let ctx = this.canvas.getContext("2d");
725
726    // Draw the timeline image.
727    ctx.drawImage(this.buffer, 0, this.imageOffset);
728
729    // Draw the current interval highlight.
730    let left;
731    let right;
732    if (this.selectionStart !== null && this.selectionEnd !== null) {
733      ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
734      left = Math.min(this.selectionStart, this.selectionEnd);
735      right = Math.max(this.selectionStart, this.selectionEnd);
736      let height = this.buffer.height - this.functionTimelineHeight;
737      ctx.fillRect(0, this.imageOffset, left, height);
738      ctx.fillRect(right, this.imageOffset, this.buffer.width - right, height);
739    } else {
740      left = 0;
741      right = this.buffer.width;
742    }
743
744    // Draw the scale text.
745    let file = this.currentState.file;
746    ctx.fillStyle = "white";
747    ctx.fillRect(0, 0, this.canvas.width, this.imageOffset);
748    if (file && file.ticks.length > 0) {
749      let firstTime = file.ticks[0].tm;
750      let lastTime = file.ticks[file.ticks.length - 1].tm;
751
752      let leftTime =
753          firstTime + left / this.canvas.width * (lastTime - firstTime);
754      let rightTime =
755          firstTime + right / this.canvas.width * (lastTime - firstTime);
756
757      let leftText = (leftTime / 1000000).toFixed(3) + "s";
758      let rightText = (rightTime / 1000000).toFixed(3) + "s";
759
760      ctx.textBaseline = 'top';
761      ctx.font = this.fontSize + "px Arial";
762      ctx.fillStyle = "black";
763
764      let leftWidth = ctx.measureText(leftText).width;
765      let rightWidth = ctx.measureText(rightText).width;
766
767      let leftStart = left - leftWidth / 2;
768      let rightStart = right - rightWidth / 2;
769
770      if (leftStart < 0) leftStart = 0;
771      if (rightStart + rightWidth > this.canvas.width) {
772        rightStart = this.canvas.width - rightWidth;
773      }
774      if (leftStart + leftWidth > rightStart) {
775        if (leftStart > this.canvas.width - (rightStart - rightWidth)) {
776          rightStart = leftStart + leftWidth;
777
778        } else {
779          leftStart = rightStart - leftWidth;
780        }
781      }
782
783      ctx.fillText(leftText, leftStart, 0);
784      ctx.fillText(rightText, rightStart, 0);
785    }
786  }
787
788  render(newState) {
789    let oldState = this.currentState;
790
791    if (!newState.file) {
792      this.element.style.display = "none";
793      return;
794    }
795
796    let width = Math.round(window.innerWidth - 20);
797    let height = Math.round(window.innerHeight / 5);
798
799    if (oldState) {
800      if (width === oldState.timelineSize.width &&
801          height === oldState.timelineSize.height &&
802          newState.file === oldState.file &&
803          newState.currentCodeId === oldState.currentCodeId &&
804          newState.start === oldState.start &&
805          newState.end === oldState.end) {
806        // No change, nothing to do.
807        return;
808      }
809    }
810    this.currentState = newState;
811    this.currentState.timelineSize.width = width;
812    this.currentState.timelineSize.height = height;
813
814    this.element.style.display = "inherit";
815
816    let file = this.currentState.file;
817
818    const minPixelsPerBucket = 10;
819    const minTicksPerBucket = 8;
820    let maxBuckets = Math.round(file.ticks.length / minTicksPerBucket);
821    let bucketCount = Math.min(
822        Math.round(width / minPixelsPerBucket), maxBuckets);
823
824    // Make sure the canvas has the right dimensions.
825    this.canvas.width = width;
826    this.canvas.height  = height;
827
828    // Make space for the selection text.
829    height -= this.imageOffset;
830
831    let currentCodeId = this.currentState.currentCodeId;
832
833    let firstTime = file.ticks[0].tm;
834    let lastTime = file.ticks[file.ticks.length - 1].tm;
835    let start = Math.max(this.currentState.start, firstTime);
836    let end = Math.min(this.currentState.end, lastTime);
837
838    this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width;
839    this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width;
840
841    let stackProcessor = new CategorySampler(file, bucketCount);
842    generateTree(file, 0, Infinity, stackProcessor);
843    let codeIdProcessor = new FunctionTimelineProcessor(
844      currentCodeId,
845      filterFromFilterId(this.currentState.callTree.attribution));
846    generateTree(file, 0, Infinity, codeIdProcessor);
847
848    let buffer = document.createElement("canvas");
849
850    buffer.width = width;
851    buffer.height = height;
852
853    // Calculate the bar heights for each bucket.
854    let graphHeight = height - this.functionTimelineHeight;
855    let buckets = stackProcessor.buckets;
856    let bucketsGraph = [];
857    for (let i = 0; i < buckets.length; i++) {
858      let sum = 0;
859      let bucketData = [];
860      let total = buckets[i].total;
861      if (total > 0) {
862        for (let j = 0; j < bucketDescriptors.length; j++) {
863          let desc = bucketDescriptors[j];
864          for (let k = 0; k < desc.kinds.length; k++) {
865            sum += buckets[i][desc.kinds[k]];
866          }
867          bucketData.push(Math.round(graphHeight * sum / total));
868        }
869      } else {
870        // No ticks fell into this bucket. Fill with "Unknown."
871        for (let j = 0; j < bucketDescriptors.length; j++) {
872          let desc = bucketDescriptors[j];
873          bucketData.push(desc.text === "Unknown" ? graphHeight : 0);
874        }
875      }
876      bucketsGraph.push(bucketData);
877    }
878
879    // Draw the category graph into the buffer.
880    let bucketWidth = width / (bucketsGraph.length - 1);
881    let ctx = buffer.getContext('2d');
882    for (let i = 0; i < bucketsGraph.length - 1; i++) {
883      let bucketData = bucketsGraph[i];
884      let nextBucketData = bucketsGraph[i + 1];
885      let x1 = Math.round(i * bucketWidth);
886      let x2 = Math.round((i + 1) * bucketWidth);
887      for (let j = 0; j < bucketData.length; j++) {
888        ctx.beginPath();
889        ctx.moveTo(x1, j > 0 ? bucketData[j - 1] : 0);
890        ctx.lineTo(x2, j > 0 ? nextBucketData[j - 1] : 0);
891        ctx.lineTo(x2, nextBucketData[j]);
892        ctx.lineTo(x1, bucketData[j]);
893        ctx.closePath();
894        ctx.fillStyle = bucketDescriptors[j].color;
895        ctx.fill();
896      }
897    }
898
899    // Draw the function ticks.
900    let functionTimelineYOffset = graphHeight;
901    let functionTimelineTickHeight = this.functionTimelineTickHeight;
902    let functionTimelineHalfHeight =
903        Math.round(functionTimelineTickHeight / 2);
904    let timestampScaler = width / (lastTime - firstTime);
905    let timestampToX = (t) => Math.round((t - firstTime) * timestampScaler);
906    ctx.fillStyle = "white";
907    ctx.fillRect(
908      0,
909      functionTimelineYOffset,
910      buffer.width,
911      this.functionTimelineHeight);
912    for (let i = 0; i < codeIdProcessor.blocks.length; i++) {
913      let block = codeIdProcessor.blocks[i];
914      let bucket = kindToBucketDescriptor[block.kind];
915      ctx.fillStyle = bucket.color;
916      ctx.fillRect(
917        timestampToX(block.start),
918        functionTimelineYOffset,
919        Math.max(1, Math.round((block.end - block.start) * timestampScaler)),
920        block.topOfStack ?
921            functionTimelineTickHeight : functionTimelineHalfHeight);
922    }
923    ctx.strokeStyle = "black";
924    ctx.lineWidth = "1";
925    ctx.beginPath();
926    ctx.moveTo(0, functionTimelineYOffset + 0.5);
927    ctx.lineTo(buffer.width, functionTimelineYOffset + 0.5);
928    ctx.stroke();
929    ctx.strokeStyle = "rgba(0,0,0,0.2)";
930    ctx.lineWidth = "1";
931    ctx.beginPath();
932    ctx.moveTo(0, functionTimelineYOffset + functionTimelineHalfHeight - 0.5);
933    ctx.lineTo(buffer.width,
934        functionTimelineYOffset + functionTimelineHalfHeight - 0.5);
935    ctx.stroke();
936
937    // Draw marks for optimizations and deoptimizations in the function
938    // timeline.
939    if (currentCodeId && currentCodeId >= 0 &&
940        file.code[currentCodeId].func) {
941      let y = Math.round(functionTimelineYOffset + functionTimelineTickHeight +
942          (this.functionTimelineHeight - functionTimelineTickHeight) / 2);
943      let func = file.functions[file.code[currentCodeId].func];
944      for (let i = 0; i < func.codes.length; i++) {
945        let code = file.code[func.codes[i]];
946        if (code.kind === "Opt") {
947          if (code.deopt) {
948            // Draw deoptimization mark.
949            let x = timestampToX(code.deopt.tm);
950            ctx.lineWidth = 0.7;
951            ctx.strokeStyle = "red";
952            ctx.beginPath();
953            ctx.moveTo(x - 3, y - 3);
954            ctx.lineTo(x + 3, y + 3);
955            ctx.stroke();
956            ctx.beginPath();
957            ctx.moveTo(x - 3, y + 3);
958            ctx.lineTo(x + 3, y - 3);
959            ctx.stroke();
960          }
961          // Draw optimization mark.
962          let x = timestampToX(code.tm);
963          ctx.lineWidth = 0.7;
964          ctx.strokeStyle = "blue";
965          ctx.beginPath();
966          ctx.moveTo(x - 3, y - 3);
967          ctx.lineTo(x, y);
968          ctx.stroke();
969          ctx.beginPath();
970          ctx.moveTo(x - 3, y + 3);
971          ctx.lineTo(x, y);
972          ctx.stroke();
973        } else {
974          // Draw code creation mark.
975          let x = Math.round(timestampToX(code.tm));
976          ctx.beginPath();
977          ctx.fillStyle = "black";
978          ctx.arc(x, y, 3, 0, 2 * Math.PI);
979          ctx.fill();
980        }
981      }
982    }
983
984    // Remember stuff for later.
985    this.buffer = buffer;
986
987    // Draw the buffer.
988    this.drawSelection();
989
990    // (Re-)Populate the graph legend.
991    while (this.legend.cells.length > 0) {
992      this.legend.deleteCell(0);
993    }
994    let cell = this.legend.insertCell(-1);
995    cell.textContent = "Legend: ";
996    cell.style.padding = "1ex";
997    for (let i = 0; i < bucketDescriptors.length; i++) {
998      let cell = this.legend.insertCell(-1);
999      cell.style.padding = "1ex";
1000      let desc = bucketDescriptors[i];
1001      let div = document.createElement("div");
1002      div.style.display = "inline-block";
1003      div.style.width = "0.6em";
1004      div.style.height = "1.2ex";
1005      div.style.backgroundColor = desc.color;
1006      div.style.borderStyle = "solid";
1007      div.style.borderWidth = "1px";
1008      div.style.borderColor = "Black";
1009      cell.appendChild(div);
1010      cell.appendChild(document.createTextNode(" " + desc.text));
1011    }
1012
1013    while (this.currentCode.firstChild) {
1014      this.currentCode.removeChild(this.currentCode.firstChild);
1015    }
1016    if (currentCodeId) {
1017      let currentCode = file.code[currentCodeId];
1018      this.currentCode.appendChild(document.createTextNode(currentCode.name));
1019    } else {
1020      this.currentCode.appendChild(document.createTextNode("<none>"));
1021    }
1022  }
1023}
1024
1025class ModeBarView {
1026  constructor() {
1027    let modeBar = this.element = $("mode-bar");
1028
1029    function addMode(id, text, active) {
1030      let div = document.createElement("div");
1031      div.classList = "mode-button" + (active ? " active-mode-button" : "");
1032      div.id = "mode-" + id;
1033      div.textContent = text;
1034      div.onclick = () => {
1035        if (main.currentState.mode === id) return;
1036        let old = $("mode-" + main.currentState.mode);
1037        old.classList = "mode-button";
1038        div.classList = "mode-button active-mode-button";
1039        main.setMode(id);
1040      };
1041      modeBar.appendChild(div);
1042    }
1043
1044    addMode("summary", "Summary", true);
1045    addMode("bottom-up", "Bottom up");
1046    addMode("top-down", "Top down");
1047    addMode("function-list", "Functions");
1048  }
1049
1050  render(newState) {
1051    if (!newState.file) {
1052      this.element.style.display = "none";
1053      return;
1054    }
1055
1056    this.element.style.display = "inherit";
1057  }
1058}
1059
1060class SummaryView {
1061  constructor() {
1062    this.element = $("summary");
1063    this.currentState = null;
1064  }
1065
1066  render(newState) {
1067    let oldState = this.currentState;
1068
1069    if (!newState.file || newState.mode !== "summary") {
1070      this.element.style.display = "none";
1071      this.currentState = null;
1072      return;
1073    }
1074
1075    this.currentState = newState;
1076    if (oldState) {
1077      if (newState.file === oldState.file &&
1078          newState.start === oldState.start &&
1079          newState.end === oldState.end) {
1080        // No change, nothing to do.
1081        return;
1082      }
1083    }
1084
1085    this.element.style.display = "inherit";
1086
1087    while (this.element.firstChild) {
1088      this.element.removeChild(this.element.firstChild);
1089    }
1090
1091    let stats = computeOptimizationStats(
1092        this.currentState.file, newState.start, newState.end);
1093
1094    let table = document.createElement("table");
1095    let rows = document.createElement("tbody");
1096
1097    function addRow(text, number, indent) {
1098      let row = rows.insertRow(-1);
1099      let textCell = row.insertCell(-1);
1100      textCell.textContent = text;
1101      let numberCell = row.insertCell(-1);
1102      numberCell.textContent = number;
1103      if (indent) {
1104        textCell.style.textIndent = indent + "em";
1105        numberCell.style.textIndent = indent + "em";
1106      }
1107      return row;
1108    }
1109
1110    function makeCollapsible(row, arrow) {
1111      arrow.textContent = EXPANDED_ARROW;
1112      let expandHandler = row.onclick;
1113      row.onclick = () => {
1114        let id = row.id;
1115        let index = row.rowIndex + 1;
1116        while (index < rows.rows.length &&
1117          rows.rows[index].id.startsWith(id)) {
1118          rows.deleteRow(index);
1119        }
1120        arrow.textContent = COLLAPSED_ARROW;
1121        row.onclick = expandHandler;
1122      }
1123    }
1124
1125    function expandDeoptInstances(row, arrow, instances, indent, kind) {
1126      let index = row.rowIndex;
1127      for (let i = 0; i < instances.length; i++) {
1128        let childRow = rows.insertRow(index + 1);
1129        childRow.id = row.id + i + "/";
1130
1131        let deopt = instances[i].deopt;
1132
1133        let textCell = childRow.insertCell(-1);
1134        textCell.appendChild(document.createTextNode(deopt.posText));
1135        textCell.style.textIndent = indent + "em";
1136        let reasonCell = childRow.insertCell(-1);
1137        reasonCell.appendChild(
1138            document.createTextNode("Reason: " + deopt.reason));
1139        reasonCell.style.textIndent = indent + "em";
1140      }
1141      makeCollapsible(row, arrow);
1142    }
1143
1144    function expandDeoptFunctionList(row, arrow, list, indent, kind) {
1145      let index = row.rowIndex;
1146      for (let i = 0; i < list.length; i++) {
1147        let childRow = rows.insertRow(index + 1);
1148        childRow.id = row.id + i + "/";
1149
1150        let textCell = childRow.insertCell(-1);
1151        textCell.appendChild(createIndentNode(indent));
1152        let childArrow = createArrowNode();
1153        textCell.appendChild(childArrow);
1154        textCell.appendChild(
1155            createFunctionNode(list[i].f.name, list[i].f.codes[0]));
1156
1157        let numberCell = childRow.insertCell(-1);
1158        numberCell.textContent = list[i].instances.length;
1159        numberCell.style.textIndent = indent + "em";
1160
1161        childArrow.textContent = COLLAPSED_ARROW;
1162        childRow.onclick = () => {
1163          expandDeoptInstances(
1164              childRow, childArrow, list[i].instances, indent + 1);
1165        };
1166      }
1167      makeCollapsible(row, arrow);
1168    }
1169
1170    function expandOptimizedFunctionList(row, arrow, list, indent, kind) {
1171      let index = row.rowIndex;
1172      for (let i = 0; i < list.length; i++) {
1173        let childRow = rows.insertRow(index + 1);
1174        childRow.id = row.id + i + "/";
1175
1176        let textCell = childRow.insertCell(-1);
1177        textCell.appendChild(
1178            createFunctionNode(list[i].f.name, list[i].f.codes[0]));
1179        textCell.style.textIndent = indent + "em";
1180
1181        let numberCell = childRow.insertCell(-1);
1182        numberCell.textContent = list[i].instances.length;
1183        numberCell.style.textIndent = indent + "em";
1184      }
1185      makeCollapsible(row, arrow);
1186    }
1187
1188    function addExpandableRow(text, list, indent, kind) {
1189      let row = rows.insertRow(-1);
1190
1191      row.id = "opt-table/" + kind + "/";
1192      row.style.backgroundColor = CATEGORY_COLOR;
1193
1194      let textCell = row.insertCell(-1);
1195      textCell.appendChild(createIndentNode(indent));
1196      let arrow = createArrowNode();
1197      textCell.appendChild(arrow);
1198      textCell.appendChild(document.createTextNode(text));
1199
1200      let numberCell = row.insertCell(-1);
1201      numberCell.textContent = list.count;
1202      if (indent) {
1203        numberCell.style.textIndent = indent + "em";
1204      }
1205
1206      if (list.count > 0) {
1207        arrow.textContent = COLLAPSED_ARROW;
1208        if (kind === "opt") {
1209          row.onclick = () => {
1210            expandOptimizedFunctionList(
1211                row, arrow, list.functions, indent + 1, kind);
1212          };
1213        } else {
1214          row.onclick = () => {
1215            expandDeoptFunctionList(
1216                row, arrow, list.functions, indent + 1, kind);
1217          };
1218        }
1219      }
1220      return row;
1221    }
1222
1223    addRow("Total function count:", stats.functionCount);
1224    addRow("Optimized function count:", stats.optimizedFunctionCount, 1);
1225    addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2);
1226
1227    addExpandableRow("Optimization count:", stats.optimizations, 0, "opt");
1228    let deoptCount = stats.eagerDeoptimizations.count +
1229        stats.softDeoptimizations.count + stats.lazyDeoptimizations.count;
1230    addRow("Deoptimization count:", deoptCount);
1231    addExpandableRow("Eager:", stats.eagerDeoptimizations, 1, "eager");
1232    addExpandableRow("Lazy:", stats.lazyDeoptimizations, 1, "lazy");
1233    addExpandableRow("Soft:", stats.softDeoptimizations, 1, "soft");
1234
1235    table.appendChild(rows);
1236    this.element.appendChild(table);
1237  }
1238}
1239
1240class HelpView {
1241  constructor() {
1242    this.element = $("help");
1243  }
1244
1245  render(newState) {
1246    this.element.style.display = newState.file ? "none" : "inherit";
1247  }
1248}
1249