• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
11function removeAllChildren(element) {
12  while (element.firstChild) {
13    element.removeChild(element.firstChild);
14  }
15}
16
17let components;
18function createViews() {
19  components = [
20    new CallTreeView(),
21    new TimelineView(),
22    new HelpView(),
23    new SummaryView(),
24    new ModeBarView(),
25    new ScriptSourceView(),
26  ];
27}
28
29function emptyState() {
30  return {
31    file : null,
32    mode : null,
33    currentCodeId : null,
34    viewingSource: false,
35    start : 0,
36    end : Infinity,
37    timelineSize : {
38      width : 0,
39      height : 0
40    },
41    callTree : {
42      attribution : "js-exclude-bc",
43      categories : "code-type",
44      sort : "time"
45    },
46    sourceData: null
47  };
48}
49
50function setCallTreeState(state, callTreeState) {
51  state = Object.assign({}, state);
52  state.callTree = callTreeState;
53  return state;
54}
55
56let main = {
57  currentState : emptyState(),
58  renderPending : false,
59
60  setMode(mode) {
61    if (mode !== main.currentState.mode) {
62
63      function setCallTreeModifiers(attribution, categories, sort) {
64        let callTreeState = Object.assign({}, main.currentState.callTree);
65        callTreeState.attribution = attribution;
66        callTreeState.categories = categories;
67        callTreeState.sort = sort;
68        return callTreeState;
69      }
70
71      let state = Object.assign({}, main.currentState);
72
73      switch (mode) {
74        case "bottom-up":
75          state.callTree =
76              setCallTreeModifiers("js-exclude-bc", "code-type", "time");
77          break;
78        case "top-down":
79          state.callTree =
80              setCallTreeModifiers("js-exclude-bc", "none", "time");
81          break;
82        case "function-list":
83          state.callTree =
84              setCallTreeModifiers("js-exclude-bc", "code-type", "own-time");
85          break;
86      }
87
88      state.mode = mode;
89
90      main.currentState = state;
91      main.delayRender();
92    }
93  },
94
95  setCallTreeAttribution(attribution) {
96    if (attribution !== main.currentState.attribution) {
97      let callTreeState = Object.assign({}, main.currentState.callTree);
98      callTreeState.attribution = attribution;
99      main.currentState = setCallTreeState(main.currentState,  callTreeState);
100      main.delayRender();
101    }
102  },
103
104  setCallTreeSort(sort) {
105    if (sort !== main.currentState.sort) {
106      let callTreeState = Object.assign({}, main.currentState.callTree);
107      callTreeState.sort = sort;
108      main.currentState = setCallTreeState(main.currentState,  callTreeState);
109      main.delayRender();
110    }
111  },
112
113  setCallTreeCategories(categories) {
114    if (categories !== main.currentState.categories) {
115      let callTreeState = Object.assign({}, main.currentState.callTree);
116      callTreeState.categories = categories;
117      main.currentState = setCallTreeState(main.currentState,  callTreeState);
118      main.delayRender();
119    }
120  },
121
122  setViewInterval(start, end) {
123    if (start !== main.currentState.start ||
124        end !== main.currentState.end) {
125      main.currentState = Object.assign({}, main.currentState);
126      main.currentState.start = start;
127      main.currentState.end = end;
128      main.delayRender();
129    }
130  },
131
132  updateSources(file) {
133    let statusDiv = $("source-status");
134    if (!file) {
135      statusDiv.textContent = "";
136      return;
137    }
138    if (!file.scripts || file.scripts.length === 0) {
139      statusDiv.textContent =
140          "Script source not available. Run profiler with --log-source-code.";
141      return;
142    }
143    statusDiv.textContent = "Script source is available.";
144    main.currentState.sourceData = new SourceData(file);
145  },
146
147  setFile(file) {
148    if (file !== main.currentState.file) {
149      let lastMode = main.currentState.mode || "summary";
150      main.currentState = emptyState();
151      main.currentState.file = file;
152      main.updateSources(file);
153      main.setMode(lastMode);
154      main.delayRender();
155    }
156  },
157
158  setCurrentCode(codeId) {
159    if (codeId !== main.currentState.currentCodeId) {
160      main.currentState = Object.assign({}, main.currentState);
161      main.currentState.currentCodeId = codeId;
162      main.delayRender();
163    }
164  },
165
166  setViewingSource(value) {
167    if (main.currentState.viewingSource !== value) {
168      main.currentState = Object.assign({}, main.currentState);
169      main.currentState.viewingSource = value;
170      main.delayRender();
171    }
172  },
173
174  onResize() {
175    main.delayRender();
176  },
177
178  onLoad() {
179    function loadHandler(evt) {
180      let f = evt.target.files[0];
181      if (f) {
182        let reader = new FileReader();
183        reader.onload = function(event) {
184          main.setFile(JSON.parse(event.target.result));
185        };
186        reader.onerror = function(event) {
187          console.error(
188              "File could not be read! Code " + event.target.error.code);
189        };
190        reader.readAsText(f);
191      } else {
192        main.setFile(null);
193      }
194    }
195    $("fileinput").addEventListener(
196        "change", loadHandler, false);
197    createViews();
198  },
199
200  delayRender()  {
201    if (main.renderPending) return;
202    main.renderPending = true;
203
204    window.requestAnimationFrame(() => {
205      main.renderPending = false;
206      for (let c of components) {
207        c.render(main.currentState);
208      }
209    });
210  }
211};
212
213const CATEGORY_COLOR = "#f5f5f5";
214const bucketDescriptors =
215  [{
216    kinds: ["JS_OPT"],
217    color: "#64dd17",
218    backgroundColor: "#80e27e",
219    text: "JS Optimized"
220  },
221  {
222    kinds: ["JS_TURBOPROP"],
223    color: "#693eb8",
224    backgroundColor: "#a6c452",
225    text: "JS Turboprop"
226  },
227  {
228    kinds: ["JS_BASELINE"],
229    color: "#b3005b",
230    backgroundColor: "#ff9e80",
231    text: "JS Baseline"
232  },
233  {
234    kinds: ["JS_UNOPT", "BC"],
235    color: "#dd2c00",
236    backgroundColor: "#ff9e80",
237    text: "JS Unoptimized"
238  },
239  {
240    kinds: ["IC"],
241    color: "#ff6d00",
242    backgroundColor: "#ffab40",
243    text: "IC"
244  },
245  {
246    kinds: ["STUB", "BUILTIN", "REGEXP"],
247    color: "#ffd600",
248    backgroundColor: "#ffea00",
249    text: "Other generated"
250  },
251  {
252    kinds: ["CPP", "LIB"],
253    color: "#304ffe",
254    backgroundColor: "#6ab7ff",
255    text: "C++"
256  },
257  {
258    kinds: ["CPP_EXT"],
259    color: "#003c8f",
260    backgroundColor: "#c0cfff",
261    text: "C++/external"
262  },
263  {
264    kinds: ["CPP_PARSE"],
265    color: "#aa00ff",
266    backgroundColor: "#ffb2ff",
267    text: "C++/Parser"
268  },
269  {
270    kinds: ["CPP_COMP_BC"],
271    color: "#43a047",
272    backgroundColor: "#88c399",
273    text: "C++/Bytecode compiler"
274  },
275  {
276    kinds: ["CPP_COMP_BASELINE"],
277    color: "#43a047",
278    backgroundColor: "#5a8000",
279    text: "C++/Baseline compiler"
280  },
281  {
282    kinds: ["CPP_COMP"],
283    color: "#00e5ff",
284    backgroundColor: "#6effff",
285    text: "C++/Compiler"
286  },
287  {
288    kinds: ["CPP_GC"],
289    color: "#6200ea",
290    backgroundColor: "#e1bee7",
291    text: "C++/GC"
292  },
293  {
294    kinds: ["UNKNOWN"],
295    color: "#bdbdbd",
296    backgroundColor: "#efefef",
297    text: "Unknown"
298  }
299  ];
300
301let kindToBucketDescriptor = {};
302for (let i = 0; i < bucketDescriptors.length; i++) {
303  let bucket = bucketDescriptors[i];
304  for (let j = 0; j < bucket.kinds.length; j++) {
305    kindToBucketDescriptor[bucket.kinds[j]] = bucket;
306  }
307}
308
309function bucketFromKind(kind) {
310  for (let i = 0; i < bucketDescriptors.length; i++) {
311    let bucket = bucketDescriptors[i];
312    for (let j = 0; j < bucket.kinds.length; j++) {
313      if (bucket.kinds[j] === kind) {
314        return bucket;
315      }
316    }
317  }
318  return null;
319}
320
321function codeTypeToText(type) {
322  switch (type) {
323    case "UNKNOWN":
324      return "Unknown";
325    case "CPP_PARSE":
326      return "C++ Parser";
327    case "CPP_COMP_BASELINE":
328      return "C++ Baseline Compiler";
329    case "CPP_COMP_BC":
330      return "C++ Bytecode Compiler";
331    case "CPP_COMP":
332      return "C++ Compiler";
333    case "CPP_GC":
334      return "C++ GC";
335    case "CPP_EXT":
336      return "C++ External";
337    case "CPP":
338      return "C++";
339    case "LIB":
340      return "Library";
341    case "IC":
342      return "IC";
343    case "BC":
344      return "Bytecode";
345    case "STUB":
346      return "Stub";
347    case "BUILTIN":
348      return "Builtin";
349    case "REGEXP":
350      return "RegExp";
351    case "JS_OPT":
352      return "JS opt";
353    case "JS_TURBOPROP":
354      return "JS Turboprop";
355    case "JS_BASELINE":
356      return "JS Baseline";
357    case "JS_UNOPT":
358      return "JS unopt";
359  }
360  console.error("Unknown type: " + type);
361}
362
363function createTypeNode(type) {
364  if (type === "CAT") {
365    return document.createTextNode("");
366  }
367  let span = document.createElement("span");
368  span.classList.add("code-type-chip");
369  span.textContent = codeTypeToText(type);
370
371  return span;
372}
373
374function filterFromFilterId(id) {
375  switch (id) {
376    case "full-tree":
377      return (type, kind) => true;
378    case "js-funs":
379      return (type, kind) => type !== 'CODE';
380    case "js-exclude-bc":
381      return (type, kind) =>
382          type !== 'CODE' || kind !== "BytecodeHandler";
383  }
384}
385
386function createIndentNode(indent) {
387  let div = document.createElement("div");
388  div.style.display = "inline-block";
389  div.style.width = (indent + 0.5) + "em";
390  return div;
391}
392
393function createArrowNode() {
394  let span = document.createElement("span");
395  span.classList.add("tree-row-arrow");
396  return span;
397}
398
399function createFunctionNode(name, codeId) {
400  let nameElement = document.createElement("span");
401  nameElement.appendChild(document.createTextNode(name));
402  nameElement.classList.add("tree-row-name");
403  if (codeId !== -1) {
404    nameElement.classList.add("codeid-link");
405    nameElement.onclick = (event) => {
406      main.setCurrentCode(codeId);
407      // Prevent the click from bubbling to the row and causing it to
408      // collapse/expand.
409      event.stopPropagation();
410    };
411  }
412  return nameElement;
413}
414
415function createViewSourceNode(codeId) {
416  let linkElement = document.createElement("span");
417  linkElement.appendChild(document.createTextNode("View source"));
418  linkElement.classList.add("view-source-link");
419  linkElement.onclick = (event) => {
420    main.setCurrentCode(codeId);
421    main.setViewingSource(true);
422    // Prevent the click from bubbling to the row and causing it to
423    // collapse/expand.
424    event.stopPropagation();
425  };
426  return linkElement;
427}
428
429const COLLAPSED_ARROW = "\u25B6";
430const EXPANDED_ARROW = "\u25BC";
431
432class CallTreeView {
433  constructor() {
434    this.element = $("calltree");
435    this.treeElement = $("calltree-table");
436    this.selectAttribution = $("calltree-attribution");
437    this.selectCategories = $("calltree-categories");
438    this.selectSort = $("calltree-sort");
439
440    this.selectAttribution.onchange = () => {
441      main.setCallTreeAttribution(this.selectAttribution.value);
442    };
443
444    this.selectCategories.onchange = () => {
445      main.setCallTreeCategories(this.selectCategories.value);
446    };
447
448    this.selectSort.onchange = () => {
449      main.setCallTreeSort(this.selectSort.value);
450    };
451
452    this.currentState = null;
453  }
454
455  sortFromId(id) {
456    switch (id) {
457      case "time":
458        return (c1, c2) => {
459          if (c1.ticks < c2.ticks) return 1;
460          else if (c1.ticks > c2.ticks) return -1;
461          return c2.ownTicks - c1.ownTicks;
462        };
463      case "own-time":
464        return (c1, c2) => {
465          if (c1.ownTicks < c2.ownTicks) return 1;
466          else if (c1.ownTicks > c2.ownTicks) return -1;
467          return c2.ticks - c1.ticks;
468        };
469      case "category-time":
470        return (c1, c2) => {
471          if (c1.type === c2.type) return c2.ticks - c1.ticks;
472          if (c1.type < c2.type) return 1;
473          return -1;
474        };
475      case "category-own-time":
476        return (c1, c2) => {
477          if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks;
478          if (c1.type < c2.type) return 1;
479          return -1;
480        };
481    }
482  }
483
484  expandTree(tree, indent) {
485    let index = 0;
486    let id = "R/";
487    let row = tree.row;
488
489    if (row) {
490      index = row.rowIndex;
491      id = row.id;
492
493      tree.arrow.textContent = EXPANDED_ARROW;
494      // Collapse the children when the row is clicked again.
495      let expandHandler = row.onclick;
496      row.onclick = () => {
497        this.collapseRow(tree, expandHandler);
498      }
499    }
500
501    // Collect the children, and sort them by ticks.
502    let children = [];
503    let filter =
504        filterFromFilterId(this.currentState.callTree.attribution);
505    for (let childId in tree.children) {
506      let child = tree.children[childId];
507      if (child.ticks > 0) {
508        children.push(child);
509        if (child.delayedExpansion) {
510          expandTreeNode(this.currentState.file, child, filter);
511        }
512      }
513    }
514    children.sort(this.sortFromId(this.currentState.callTree.sort));
515
516    for (let i = 0; i < children.length; i++) {
517      let node = children[i];
518      let row = this.rows.insertRow(index);
519      row.id = id + i + "/";
520
521      if (node.type === "CAT") {
522        row.style.backgroundColor = CATEGORY_COLOR;
523      } else {
524        row.style.backgroundColor = bucketFromKind(node.type).backgroundColor;
525      }
526
527      // Inclusive time % cell.
528      let c = row.insertCell();
529      c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%";
530      c.style.textAlign = "right";
531      // Percent-of-parent cell.
532      c = row.insertCell();
533      c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%";
534      c.style.textAlign = "right";
535      // Exclusive time % cell.
536      if (this.currentState.mode !== "bottom-up") {
537        c = row.insertCell(-1);
538        c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%";
539        c.style.textAlign = "right";
540      }
541
542      // Create the name cell.
543      let nameCell = row.insertCell();
544      nameCell.appendChild(createIndentNode(indent + 1));
545      let arrow = createArrowNode();
546      nameCell.appendChild(arrow);
547      nameCell.appendChild(createTypeNode(node.type));
548      nameCell.appendChild(createFunctionNode(node.name, node.codeId));
549      if (main.currentState.sourceData &&
550          node.codeId >= 0 &&
551          main.currentState.sourceData.hasSource(
552              this.currentState.file.code[node.codeId].func)) {
553        nameCell.appendChild(createViewSourceNode(node.codeId));
554      }
555
556      // Inclusive ticks cell.
557      c = row.insertCell();
558      c.textContent = node.ticks;
559      c.style.textAlign = "right";
560      if (this.currentState.mode !== "bottom-up") {
561        // Exclusive ticks cell.
562        c = row.insertCell(-1);
563        c.textContent = node.ownTicks;
564        c.style.textAlign = "right";
565      }
566      if (node.children.length > 0) {
567        arrow.textContent = COLLAPSED_ARROW;
568        row.onclick = () => { this.expandTree(node, indent + 1); };
569      }
570
571      node.row = row;
572      node.arrow = arrow;
573
574      index++;
575    }
576  }
577
578  collapseRow(tree, expandHandler) {
579    let row = tree.row;
580    let id = row.id;
581    let index = row.rowIndex;
582    while (row.rowIndex < this.rows.rows.length &&
583        this.rows.rows[index].id.startsWith(id)) {
584      this.rows.deleteRow(index);
585    }
586
587    tree.arrow.textContent = COLLAPSED_ARROW;
588    row.onclick = expandHandler;
589  }
590
591  fillSelects(mode, calltree) {
592    function addOptions(e, values, current) {
593      while (e.options.length > 0) {
594        e.remove(0);
595      }
596      for (let i = 0; i < values.length; i++) {
597        let option = document.createElement("option");
598        option.value = values[i].value;
599        option.textContent = values[i].text;
600        e.appendChild(option);
601      }
602      e.value = current;
603    }
604
605    let attributions = [
606        { value : "js-exclude-bc",
607          text : "Attribute bytecode handlers to caller" },
608        { value : "full-tree",
609          text : "Count each code object separately" },
610        { value : "js-funs",
611          text : "Attribute non-functions to JS functions"  }
612    ];
613
614    switch (mode) {
615      case "bottom-up":
616        addOptions(this.selectAttribution, attributions, calltree.attribution);
617        addOptions(this.selectCategories, [
618            { value : "code-type", text : "Code type" },
619            { value : "none", text : "None" }
620        ], calltree.categories);
621        addOptions(this.selectSort, [
622            { value : "time", text : "Time (including children)" },
623            { value : "category-time", text : "Code category, time" },
624        ], calltree.sort);
625        return;
626      case "top-down":
627        addOptions(this.selectAttribution, attributions, calltree.attribution);
628        addOptions(this.selectCategories, [
629            { value : "none", text : "None" },
630            { value : "rt-entry", text : "Runtime entries" }
631        ], calltree.categories);
632        addOptions(this.selectSort, [
633            { value : "time", text : "Time (including children)" },
634            { value : "own-time", text : "Own time" },
635            { value : "category-time", text : "Code category, time" },
636            { value : "category-own-time", text : "Code category, own time"}
637        ], calltree.sort);
638        return;
639      case "function-list":
640        addOptions(this.selectAttribution, attributions, calltree.attribution);
641        addOptions(this.selectCategories, [
642            { value : "code-type", text : "Code type" },
643            { value : "none", text : "None" }
644        ], calltree.categories);
645        addOptions(this.selectSort, [
646            { value : "own-time", text : "Own time" },
647            { value : "time", text : "Time (including children)" },
648            { value : "category-own-time", text : "Code category, own time"},
649            { value : "category-time", text : "Code category, time" },
650        ], calltree.sort);
651        return;
652    }
653    console.error("Unexpected mode");
654  }
655
656  static isCallTreeMode(mode) {
657    switch (mode) {
658      case "bottom-up":
659      case "top-down":
660      case "function-list":
661        return true;
662      default:
663        return false;
664    }
665  }
666
667  render(newState) {
668    let oldState = this.currentState;
669    if (!newState.file || !CallTreeView.isCallTreeMode(newState.mode)) {
670      this.element.style.display = "none";
671      this.currentState = null;
672      return;
673    }
674
675    this.currentState = newState;
676    if (oldState) {
677      if (newState.file === oldState.file &&
678          newState.start === oldState.start &&
679          newState.end === oldState.end &&
680          newState.mode === oldState.mode &&
681          newState.callTree.attribution === oldState.callTree.attribution &&
682          newState.callTree.categories === oldState.callTree.categories &&
683          newState.callTree.sort === oldState.callTree.sort) {
684        // No change => just return.
685        return;
686      }
687    }
688
689    this.element.style.display = "inherit";
690
691    let mode = this.currentState.mode;
692    if (!oldState || mode !== oldState.mode) {
693      // Technically, we should also call this if attribution, categories or
694      // sort change, but the selection is already highlighted by the combobox
695      // itself, so we do need to do anything here.
696      this.fillSelects(newState.mode, newState.callTree);
697    }
698
699    let ownTimeClass = (mode === "bottom-up") ? "numeric-hidden" : "numeric";
700    let ownTimeTh = $(this.treeElement.id + "-own-time-header");
701    ownTimeTh.classList = ownTimeClass;
702    let ownTicksTh = $(this.treeElement.id + "-own-ticks-header");
703    ownTicksTh.classList = ownTimeClass;
704
705    // Build the tree.
706    let stackProcessor;
707    let filter = filterFromFilterId(this.currentState.callTree.attribution);
708    if (mode === "top-down") {
709      if (this.currentState.callTree.categories === "rt-entry") {
710        stackProcessor =
711            new RuntimeCallTreeProcessor();
712      } else {
713        stackProcessor =
714            new PlainCallTreeProcessor(filter, false);
715      }
716    } else if (mode === "function-list") {
717      stackProcessor = new FunctionListTree(
718          filter, this.currentState.callTree.categories === "code-type");
719
720    } else {
721      console.assert(mode === "bottom-up");
722      if (this.currentState.callTree.categories === "none") {
723        stackProcessor =
724            new PlainCallTreeProcessor(filter, true);
725      } else {
726        console.assert(this.currentState.callTree.categories === "code-type");
727        stackProcessor =
728            new CategorizedCallTreeProcessor(filter, true);
729      }
730    }
731    this.tickCount =
732        generateTree(this.currentState.file,
733                     this.currentState.start,
734                     this.currentState.end,
735                     stackProcessor);
736    // TODO(jarin) Handle the case when tick count is negative.
737
738    this.tree = stackProcessor.tree;
739
740    // Remove old content of the table, replace with new one.
741    let oldRows = this.treeElement.getElementsByTagName("tbody");
742    let newRows = document.createElement("tbody");
743    this.rows = newRows;
744
745    // Populate the table.
746    this.expandTree(this.tree, 0);
747
748    // Swap in the new rows.
749    this.treeElement.replaceChild(newRows, oldRows[0]);
750  }
751}
752
753class TimelineView {
754  constructor() {
755    this.element = $("timeline");
756    this.canvas = $("timeline-canvas");
757    this.legend = $("timeline-legend");
758    this.currentCode = $("timeline-currentCode");
759
760    this.canvas.onmousedown = this.onMouseDown.bind(this);
761    this.canvas.onmouseup = this.onMouseUp.bind(this);
762    this.canvas.onmousemove = this.onMouseMove.bind(this);
763
764    this.selectionStart = null;
765    this.selectionEnd = null;
766    this.selecting = false;
767
768    this.fontSize = 12;
769    this.imageOffset = Math.round(this.fontSize * 1.2);
770    this.functionTimelineHeight = 24;
771    this.functionTimelineTickHeight = 16;
772
773    this.currentState = null;
774  }
775
776  onMouseDown(e) {
777    this.selectionStart =
778        e.clientX - this.canvas.getBoundingClientRect().left;
779    this.selectionEnd = this.selectionStart + 1;
780    this.selecting = true;
781  }
782
783  onMouseMove(e) {
784    if (this.selecting) {
785      this.selectionEnd =
786          e.clientX - this.canvas.getBoundingClientRect().left;
787      this.drawSelection();
788    }
789  }
790
791  onMouseUp(e) {
792    if (this.selectionStart !== null) {
793      let x = e.clientX - this.canvas.getBoundingClientRect().left;
794      if (Math.abs(x - this.selectionStart) < 10) {
795        this.selectionStart = null;
796        this.selectionEnd = null;
797        let ctx = this.canvas.getContext("2d");
798        ctx.drawImage(this.buffer, 0, this.imageOffset);
799      } else {
800        this.selectionEnd = x;
801        this.drawSelection();
802      }
803      let file = this.currentState.file;
804      if (file) {
805        let start = this.selectionStart === null ? 0 : this.selectionStart;
806        let end = this.selectionEnd === null ? Infinity : this.selectionEnd;
807        let firstTime = file.ticks[0].tm;
808        let lastTime = file.ticks[file.ticks.length - 1].tm;
809
810        let width = this.buffer.width;
811
812        start = (start / width) * (lastTime - firstTime) + firstTime;
813        end = (end / width) * (lastTime - firstTime) + firstTime;
814
815        if (end < start) {
816          let temp = start;
817          start = end;
818          end = temp;
819        }
820
821        main.setViewInterval(start, end);
822      }
823    }
824    this.selecting = false;
825  }
826
827  drawSelection() {
828    let ctx = this.canvas.getContext("2d");
829
830    // Draw the timeline image.
831    ctx.drawImage(this.buffer, 0, this.imageOffset);
832
833    // Draw the current interval highlight.
834    let left;
835    let right;
836    if (this.selectionStart !== null && this.selectionEnd !== null) {
837      ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
838      left = Math.min(this.selectionStart, this.selectionEnd);
839      right = Math.max(this.selectionStart, this.selectionEnd);
840      let height = this.buffer.height - this.functionTimelineHeight;
841      ctx.fillRect(0, this.imageOffset, left, height);
842      ctx.fillRect(right, this.imageOffset, this.buffer.width - right, height);
843    } else {
844      left = 0;
845      right = this.buffer.width;
846    }
847
848    // Draw the scale text.
849    let file = this.currentState.file;
850    ctx.fillStyle = "white";
851    ctx.fillRect(0, 0, this.canvas.width, this.imageOffset);
852    if (file && file.ticks.length > 0) {
853      let firstTime = file.ticks[0].tm;
854      let lastTime = file.ticks[file.ticks.length - 1].tm;
855
856      let leftTime =
857          firstTime + left / this.canvas.width * (lastTime - firstTime);
858      let rightTime =
859          firstTime + right / this.canvas.width * (lastTime - firstTime);
860
861      let leftText = (leftTime / 1000000).toFixed(3) + "s";
862      let rightText = (rightTime / 1000000).toFixed(3) + "s";
863
864      ctx.textBaseline = 'top';
865      ctx.font = this.fontSize + "px Arial";
866      ctx.fillStyle = "black";
867
868      let leftWidth = ctx.measureText(leftText).width;
869      let rightWidth = ctx.measureText(rightText).width;
870
871      let leftStart = left - leftWidth / 2;
872      let rightStart = right - rightWidth / 2;
873
874      if (leftStart < 0) leftStart = 0;
875      if (rightStart + rightWidth > this.canvas.width) {
876        rightStart = this.canvas.width - rightWidth;
877      }
878      if (leftStart + leftWidth > rightStart) {
879        if (leftStart > this.canvas.width - (rightStart - rightWidth)) {
880          rightStart = leftStart + leftWidth;
881
882        } else {
883          leftStart = rightStart - leftWidth;
884        }
885      }
886
887      ctx.fillText(leftText, leftStart, 0);
888      ctx.fillText(rightText, rightStart, 0);
889    }
890  }
891
892  render(newState) {
893    let oldState = this.currentState;
894
895    if (!newState.file) {
896      this.element.style.display = "none";
897      return;
898    }
899
900    let width = Math.round(document.documentElement.clientWidth - 20);
901    let height = Math.round(document.documentElement.clientHeight / 5);
902
903    if (oldState) {
904      if (width === oldState.timelineSize.width &&
905          height === oldState.timelineSize.height &&
906          newState.file === oldState.file &&
907          newState.currentCodeId === oldState.currentCodeId &&
908          newState.start === oldState.start &&
909          newState.end === oldState.end) {
910        // No change, nothing to do.
911        return;
912      }
913    }
914    this.currentState = newState;
915    this.currentState.timelineSize.width = width;
916    this.currentState.timelineSize.height = height;
917
918    this.element.style.display = "inherit";
919
920    let file = this.currentState.file;
921
922    const minPixelsPerBucket = 10;
923    const minTicksPerBucket = 8;
924    let maxBuckets = Math.round(file.ticks.length / minTicksPerBucket);
925    let bucketCount = Math.min(
926        Math.round(width / minPixelsPerBucket), maxBuckets);
927
928    // Make sure the canvas has the right dimensions.
929    this.canvas.width = width;
930    this.canvas.height  = height;
931
932    // Make space for the selection text.
933    height -= this.imageOffset;
934
935    let currentCodeId = this.currentState.currentCodeId;
936
937    let firstTime = file.ticks[0].tm;
938    let lastTime = file.ticks[file.ticks.length - 1].tm;
939    let start = Math.max(this.currentState.start, firstTime);
940    let end = Math.min(this.currentState.end, lastTime);
941
942    this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width;
943    this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width;
944
945    let stackProcessor = new CategorySampler(file, bucketCount);
946    generateTree(file, 0, Infinity, stackProcessor);
947    let codeIdProcessor = new FunctionTimelineProcessor(
948      currentCodeId,
949      filterFromFilterId(this.currentState.callTree.attribution));
950    generateTree(file, 0, Infinity, codeIdProcessor);
951
952    let buffer = document.createElement("canvas");
953
954    buffer.width = width;
955    buffer.height = height;
956
957    // Calculate the bar heights for each bucket.
958    let graphHeight = height - this.functionTimelineHeight;
959    let buckets = stackProcessor.buckets;
960    let bucketsGraph = [];
961    for (let i = 0; i < buckets.length; i++) {
962      let sum = 0;
963      let bucketData = [];
964      let total = buckets[i].total;
965      if (total > 0) {
966        for (let j = 0; j < bucketDescriptors.length; j++) {
967          let desc = bucketDescriptors[j];
968          for (let k = 0; k < desc.kinds.length; k++) {
969            sum += buckets[i][desc.kinds[k]];
970          }
971          bucketData.push(Math.round(graphHeight * sum / total));
972        }
973      } else {
974        // No ticks fell into this bucket. Fill with "Unknown."
975        for (let j = 0; j < bucketDescriptors.length; j++) {
976          let desc = bucketDescriptors[j];
977          bucketData.push(desc.text === "Unknown" ? graphHeight : 0);
978        }
979      }
980      bucketsGraph.push(bucketData);
981    }
982
983    // Draw the category graph into the buffer.
984    let bucketWidth = width / (bucketsGraph.length - 1);
985    let ctx = buffer.getContext('2d');
986    for (let i = 0; i < bucketsGraph.length - 1; i++) {
987      let bucketData = bucketsGraph[i];
988      let nextBucketData = bucketsGraph[i + 1];
989      let x1 = Math.round(i * bucketWidth);
990      let x2 = Math.round((i + 1) * bucketWidth);
991      for (let j = 0; j < bucketData.length; j++) {
992        ctx.beginPath();
993        ctx.moveTo(x1, j > 0 ? bucketData[j - 1] : 0);
994        ctx.lineTo(x2, j > 0 ? nextBucketData[j - 1] : 0);
995        ctx.lineTo(x2, nextBucketData[j]);
996        ctx.lineTo(x1, bucketData[j]);
997        ctx.closePath();
998        ctx.fillStyle = bucketDescriptors[j].color;
999        ctx.fill();
1000      }
1001    }
1002
1003    // Draw the function ticks.
1004    let functionTimelineYOffset = graphHeight;
1005    let functionTimelineTickHeight = this.functionTimelineTickHeight;
1006    let functionTimelineHalfHeight =
1007        Math.round(functionTimelineTickHeight / 2);
1008    let timestampScaler = width / (lastTime - firstTime);
1009    let timestampToX = (t) => Math.round((t - firstTime) * timestampScaler);
1010    ctx.fillStyle = "white";
1011    ctx.fillRect(
1012      0,
1013      functionTimelineYOffset,
1014      buffer.width,
1015      this.functionTimelineHeight);
1016    for (let i = 0; i < codeIdProcessor.blocks.length; i++) {
1017      let block = codeIdProcessor.blocks[i];
1018      let bucket = kindToBucketDescriptor[block.kind];
1019      ctx.fillStyle = bucket.color;
1020      ctx.fillRect(
1021        timestampToX(block.start),
1022        functionTimelineYOffset,
1023        Math.max(1, Math.round((block.end - block.start) * timestampScaler)),
1024        block.topOfStack ?
1025            functionTimelineTickHeight : functionTimelineHalfHeight);
1026    }
1027    ctx.strokeStyle = "black";
1028    ctx.lineWidth = "1";
1029    ctx.beginPath();
1030    ctx.moveTo(0, functionTimelineYOffset + 0.5);
1031    ctx.lineTo(buffer.width, functionTimelineYOffset + 0.5);
1032    ctx.stroke();
1033    ctx.strokeStyle = "rgba(0,0,0,0.2)";
1034    ctx.lineWidth = "1";
1035    ctx.beginPath();
1036    ctx.moveTo(0, functionTimelineYOffset + functionTimelineHalfHeight - 0.5);
1037    ctx.lineTo(buffer.width,
1038        functionTimelineYOffset + functionTimelineHalfHeight - 0.5);
1039    ctx.stroke();
1040
1041    // Draw marks for optimizations and deoptimizations in the function
1042    // timeline.
1043    if (currentCodeId && currentCodeId >= 0 &&
1044        file.code[currentCodeId].func) {
1045      let y = Math.round(functionTimelineYOffset + functionTimelineTickHeight +
1046          (this.functionTimelineHeight - functionTimelineTickHeight) / 2);
1047      let func = file.functions[file.code[currentCodeId].func];
1048      for (let i = 0; i < func.codes.length; i++) {
1049        let code = file.code[func.codes[i]];
1050        if (code.kind === "Opt") {
1051          if (code.deopt) {
1052            // Draw deoptimization mark.
1053            let x = timestampToX(code.deopt.tm);
1054            ctx.lineWidth = 0.7;
1055            ctx.strokeStyle = "red";
1056            ctx.beginPath();
1057            ctx.moveTo(x - 3, y - 3);
1058            ctx.lineTo(x + 3, y + 3);
1059            ctx.stroke();
1060            ctx.beginPath();
1061            ctx.moveTo(x - 3, y + 3);
1062            ctx.lineTo(x + 3, y - 3);
1063            ctx.stroke();
1064          }
1065          // Draw optimization mark.
1066          let x = timestampToX(code.tm);
1067          ctx.lineWidth = 0.7;
1068          ctx.strokeStyle = "blue";
1069          ctx.beginPath();
1070          ctx.moveTo(x - 3, y - 3);
1071          ctx.lineTo(x, y);
1072          ctx.stroke();
1073          ctx.beginPath();
1074          ctx.moveTo(x - 3, y + 3);
1075          ctx.lineTo(x, y);
1076          ctx.stroke();
1077        } else {
1078          // Draw code creation mark.
1079          let x = Math.round(timestampToX(code.tm));
1080          ctx.beginPath();
1081          ctx.fillStyle = "black";
1082          ctx.arc(x, y, 3, 0, 2 * Math.PI);
1083          ctx.fill();
1084        }
1085      }
1086    }
1087
1088    // Remember stuff for later.
1089    this.buffer = buffer;
1090
1091    // Draw the buffer.
1092    this.drawSelection();
1093
1094    // (Re-)Populate the graph legend.
1095    while (this.legend.cells.length > 0) {
1096      this.legend.deleteCell(0);
1097    }
1098    let cell = this.legend.insertCell(-1);
1099    cell.textContent = "Legend: ";
1100    cell.style.padding = "1ex";
1101    for (let i = 0; i < bucketDescriptors.length; i++) {
1102      let cell = this.legend.insertCell(-1);
1103      cell.style.padding = "1ex";
1104      let desc = bucketDescriptors[i];
1105      let div = document.createElement("div");
1106      div.style.display = "inline-block";
1107      div.style.width = "0.6em";
1108      div.style.height = "1.2ex";
1109      div.style.backgroundColor = desc.color;
1110      div.style.borderStyle = "solid";
1111      div.style.borderWidth = "1px";
1112      div.style.borderColor = "Black";
1113      cell.appendChild(div);
1114      cell.appendChild(document.createTextNode(" " + desc.text));
1115    }
1116
1117    removeAllChildren(this.currentCode);
1118    if (currentCodeId) {
1119      let currentCode = file.code[currentCodeId];
1120      this.currentCode.appendChild(document.createTextNode(currentCode.name));
1121    } else {
1122      this.currentCode.appendChild(document.createTextNode("<none>"));
1123    }
1124  }
1125}
1126
1127class ModeBarView {
1128  constructor() {
1129    let modeBar = this.element = $("mode-bar");
1130
1131    function addMode(id, text, active) {
1132      let div = document.createElement("div");
1133      div.classList = "mode-button" + (active ? " active-mode-button" : "");
1134      div.id = "mode-" + id;
1135      div.textContent = text;
1136      div.onclick = () => {
1137        if (main.currentState.mode === id) return;
1138        let old = $("mode-" + main.currentState.mode);
1139        old.classList = "mode-button";
1140        div.classList = "mode-button active-mode-button";
1141        main.setMode(id);
1142      };
1143      modeBar.appendChild(div);
1144    }
1145
1146    addMode("summary", "Summary", true);
1147    addMode("bottom-up", "Bottom up");
1148    addMode("top-down", "Top down");
1149    addMode("function-list", "Functions");
1150  }
1151
1152  render(newState) {
1153    if (!newState.file) {
1154      this.element.style.display = "none";
1155      return;
1156    }
1157
1158    this.element.style.display = "inherit";
1159  }
1160}
1161
1162class SummaryView {
1163  constructor() {
1164    this.element = $("summary");
1165    this.currentState = null;
1166  }
1167
1168  render(newState) {
1169    let oldState = this.currentState;
1170
1171    if (!newState.file || newState.mode !== "summary") {
1172      this.element.style.display = "none";
1173      this.currentState = null;
1174      return;
1175    }
1176
1177    this.currentState = newState;
1178    if (oldState) {
1179      if (newState.file === oldState.file &&
1180          newState.start === oldState.start &&
1181          newState.end === oldState.end) {
1182        // No change, nothing to do.
1183        return;
1184      }
1185    }
1186
1187    this.element.style.display = "inherit";
1188    removeAllChildren(this.element);
1189
1190    let stats = computeOptimizationStats(
1191        this.currentState.file, newState.start, newState.end);
1192
1193    let table = document.createElement("table");
1194    let rows = document.createElement("tbody");
1195
1196    function addRow(text, number, indent) {
1197      let row = rows.insertRow(-1);
1198      let textCell = row.insertCell(-1);
1199      textCell.textContent = text;
1200      let numberCell = row.insertCell(-1);
1201      numberCell.textContent = number;
1202      if (indent) {
1203        textCell.style.textIndent = indent + "em";
1204        numberCell.style.textIndent = indent + "em";
1205      }
1206      return row;
1207    }
1208
1209    function makeCollapsible(row, arrow) {
1210      arrow.textContent = EXPANDED_ARROW;
1211      let expandHandler = row.onclick;
1212      row.onclick = () => {
1213        let id = row.id;
1214        let index = row.rowIndex + 1;
1215        while (index < rows.rows.length &&
1216          rows.rows[index].id.startsWith(id)) {
1217          rows.deleteRow(index);
1218        }
1219        arrow.textContent = COLLAPSED_ARROW;
1220        row.onclick = expandHandler;
1221      }
1222    }
1223
1224    function expandDeoptInstances(row, arrow, instances, indent, kind) {
1225      let index = row.rowIndex;
1226      for (let i = 0; i < instances.length; i++) {
1227        let childRow = rows.insertRow(index + 1);
1228        childRow.id = row.id + i + "/";
1229
1230        let deopt = instances[i].deopt;
1231
1232        let textCell = childRow.insertCell(-1);
1233        textCell.appendChild(document.createTextNode(deopt.posText));
1234        textCell.style.textIndent = indent + "em";
1235        let reasonCell = childRow.insertCell(-1);
1236        reasonCell.appendChild(
1237            document.createTextNode("Reason: " + deopt.reason));
1238        reasonCell.style.textIndent = indent + "em";
1239      }
1240      makeCollapsible(row, arrow);
1241    }
1242
1243    function expandDeoptFunctionList(row, arrow, list, indent, kind) {
1244      let index = row.rowIndex;
1245      for (let i = 0; i < list.length; i++) {
1246        let childRow = rows.insertRow(index + 1);
1247        childRow.id = row.id + i + "/";
1248
1249        let textCell = childRow.insertCell(-1);
1250        textCell.appendChild(createIndentNode(indent));
1251        let childArrow = createArrowNode();
1252        textCell.appendChild(childArrow);
1253        textCell.appendChild(
1254            createFunctionNode(list[i].f.name, list[i].f.codes[0]));
1255
1256        let numberCell = childRow.insertCell(-1);
1257        numberCell.textContent = list[i].instances.length;
1258        numberCell.style.textIndent = indent + "em";
1259
1260        childArrow.textContent = COLLAPSED_ARROW;
1261        childRow.onclick = () => {
1262          expandDeoptInstances(
1263              childRow, childArrow, list[i].instances, indent + 1);
1264        };
1265      }
1266      makeCollapsible(row, arrow);
1267    }
1268
1269    function expandOptimizedFunctionList(row, arrow, list, indent, kind) {
1270      let index = row.rowIndex;
1271      for (let i = 0; i < list.length; i++) {
1272        let childRow = rows.insertRow(index + 1);
1273        childRow.id = row.id + i + "/";
1274
1275        let textCell = childRow.insertCell(-1);
1276        textCell.appendChild(
1277            createFunctionNode(list[i].f.name, list[i].f.codes[0]));
1278        textCell.style.textIndent = indent + "em";
1279
1280        let numberCell = childRow.insertCell(-1);
1281        numberCell.textContent = list[i].instances.length;
1282        numberCell.style.textIndent = indent + "em";
1283      }
1284      makeCollapsible(row, arrow);
1285    }
1286
1287    function addExpandableRow(text, list, indent, kind) {
1288      let row = rows.insertRow(-1);
1289
1290      row.id = "opt-table/" + kind + "/";
1291      row.style.backgroundColor = CATEGORY_COLOR;
1292
1293      let textCell = row.insertCell(-1);
1294      textCell.appendChild(createIndentNode(indent));
1295      let arrow = createArrowNode();
1296      textCell.appendChild(arrow);
1297      textCell.appendChild(document.createTextNode(text));
1298
1299      let numberCell = row.insertCell(-1);
1300      numberCell.textContent = list.count;
1301      if (indent) {
1302        numberCell.style.textIndent = indent + "em";
1303      }
1304
1305      if (list.count > 0) {
1306        arrow.textContent = COLLAPSED_ARROW;
1307        if (kind === "opt") {
1308          row.onclick = () => {
1309            expandOptimizedFunctionList(
1310                row, arrow, list.functions, indent + 1, kind);
1311          };
1312        } else {
1313          row.onclick = () => {
1314            expandDeoptFunctionList(
1315                row, arrow, list.functions, indent + 1, kind);
1316          };
1317        }
1318      }
1319      return row;
1320    }
1321
1322    addRow("Total function count:", stats.functionCount);
1323    addRow("Optimized function count:", stats.optimizedFunctionCount, 1);
1324    if (stats.turbopropOptimizedFunctionCount != 0) {
1325      addRow("Turboprop optimized function count:", stats.turbopropOptimizedFunctionCount, 1);
1326    }
1327    addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2);
1328
1329    addExpandableRow("Optimization count:", stats.optimizations, 0, "opt");
1330    if (stats.turbopropOptimizedFunctionCount != 0) {
1331      addExpandableRow("Turboprop Optimization count:", stats.turbopropOptimizations, 0, "tp");
1332    }
1333    let deoptCount = stats.eagerDeoptimizations.count +
1334        stats.softDeoptimizations.count + stats.lazyDeoptimizations.count;
1335    addRow("Deoptimization count:", deoptCount);
1336    addExpandableRow("Eager:", stats.eagerDeoptimizations, 1, "eager");
1337    addExpandableRow("Lazy:", stats.lazyDeoptimizations, 1, "lazy");
1338    addExpandableRow("Soft:", stats.softDeoptimizations, 1, "soft");
1339    if (stats.softBailouts.count != 0) {
1340      addExpandableRow("SoftBailout:", stats.softBailouts, 1, "softbailout");
1341    }
1342    if (stats.eagerBailouts.count != 0) {
1343      addExpandableRow("EagerBailout:", stats.eagerBailouts, 1, "eagerbailout");
1344    }
1345
1346    table.appendChild(rows);
1347    this.element.appendChild(table);
1348  }
1349}
1350
1351class ScriptSourceView {
1352  constructor() {
1353    this.table = $("source-viewer");
1354    this.hideButton = $("source-viewer-hide-button");
1355    this.hideButton.onclick = () => {
1356      main.setViewingSource(false);
1357    };
1358  }
1359
1360  render(newState) {
1361    let oldState = this.currentState;
1362    if (!newState.file || !newState.viewingSource) {
1363      this.table.style.display = "none";
1364      this.hideButton.style.display = "none";
1365      this.currentState = null;
1366      return;
1367    }
1368    if (oldState) {
1369      if (newState.file === oldState.file &&
1370          newState.currentCodeId === oldState.currentCodeId &&
1371          newState.viewingSource === oldState.viewingSource) {
1372        // No change, nothing to do.
1373        return;
1374      }
1375    }
1376    this.currentState = newState;
1377
1378    this.table.style.display = "inline-block";
1379    this.hideButton.style.display = "inline";
1380    removeAllChildren(this.table);
1381
1382    let functionId =
1383        this.currentState.file.code[this.currentState.currentCodeId].func;
1384    let sourceView =
1385        this.currentState.sourceData.generateSourceView(functionId);
1386    for (let i = 0; i < sourceView.source.length; i++) {
1387      let sampleCount = sourceView.lineSampleCounts[i] || 0;
1388      let sampleProportion = sourceView.samplesTotal > 0 ?
1389                             sampleCount / sourceView.samplesTotal : 0;
1390      let heatBucket;
1391      if (sampleProportion === 0) {
1392        heatBucket = "line-none";
1393      } else if (sampleProportion < 0.2) {
1394        heatBucket = "line-cold";
1395      } else if (sampleProportion < 0.4) {
1396        heatBucket = "line-mediumcold";
1397      } else if (sampleProportion < 0.6) {
1398        heatBucket = "line-mediumhot";
1399      } else if (sampleProportion < 0.8) {
1400        heatBucket = "line-hot";
1401      } else {
1402        heatBucket = "line-superhot";
1403      }
1404
1405      let row = this.table.insertRow(-1);
1406
1407      let lineNumberCell = row.insertCell(-1);
1408      lineNumberCell.classList.add("source-line-number");
1409      lineNumberCell.textContent = i + sourceView.firstLineNumber;
1410
1411      let sampleCountCell = row.insertCell(-1);
1412      sampleCountCell.classList.add(heatBucket);
1413      sampleCountCell.textContent = sampleCount;
1414
1415      let sourceLineCell = row.insertCell(-1);
1416      sourceLineCell.classList.add(heatBucket);
1417      sourceLineCell.textContent = sourceView.source[i];
1418    }
1419
1420    $("timeline-currentCode").scrollIntoView();
1421  }
1422}
1423
1424class SourceData {
1425  constructor(file) {
1426    this.scripts = new Map();
1427    for (let i = 0; i < file.scripts.length; i++) {
1428      const scriptBlock = file.scripts[i];
1429      if (scriptBlock === null) continue; // Array may be sparse.
1430      if (scriptBlock.source === undefined) continue;
1431      let source = scriptBlock.source.split("\n");
1432      this.scripts.set(i, source);
1433    }
1434
1435    this.functions = new Map();
1436    for (let codeId = 0; codeId < file.code.length; ++codeId) {
1437      let codeBlock = file.code[codeId];
1438      if (codeBlock.source && codeBlock.func !== undefined) {
1439        let data = this.functions.get(codeBlock.func);
1440        if (!data) {
1441          data = new FunctionSourceData(codeBlock.source.script,
1442                                        codeBlock.source.start,
1443                                        codeBlock.source.end);
1444          this.functions.set(codeBlock.func, data);
1445        }
1446        data.addSourceBlock(codeId, codeBlock.source);
1447      }
1448    }
1449
1450    for (let tick of file.ticks) {
1451      let stack = tick.s;
1452      for (let i = 0; i < stack.length; i += 2) {
1453        let codeId = stack[i];
1454        if (codeId < 0) continue;
1455        let functionId = file.code[codeId].func;
1456        if (this.functions.has(functionId)) {
1457          let codeOffset = stack[i + 1];
1458          this.functions.get(functionId).addOffsetSample(codeId, codeOffset);
1459        }
1460      }
1461    }
1462  }
1463
1464  getScript(scriptId) {
1465    return this.scripts.get(scriptId);
1466  }
1467
1468  getLineForScriptOffset(script, scriptOffset) {
1469    let line = 0;
1470    let charsConsumed = 0;
1471    for (; line < script.length; ++line) {
1472      charsConsumed += script[line].length + 1; // Add 1 for newline.
1473      if (charsConsumed > scriptOffset) break;
1474    }
1475    return line;
1476  }
1477
1478  hasSource(functionId) {
1479    return this.functions.has(functionId);
1480  }
1481
1482  generateSourceView(functionId) {
1483    console.assert(this.hasSource(functionId));
1484    let data = this.functions.get(functionId);
1485    let scriptId = data.scriptId;
1486    let script = this.getScript(scriptId);
1487    let firstLineNumber =
1488        this.getLineForScriptOffset(script, data.startScriptOffset);
1489    let lastLineNumber =
1490        this.getLineForScriptOffset(script, data.endScriptOffset);
1491    let lines = script.slice(firstLineNumber, lastLineNumber + 1);
1492    normalizeLeadingWhitespace(lines);
1493
1494    let samplesTotal = 0;
1495    let lineSampleCounts = [];
1496    for (let [codeId, block] of data.codes) {
1497      block.offsets.forEach((sampleCount, codeOffset) => {
1498        let sourceOffset = block.positionTable.getScriptOffset(codeOffset);
1499        let lineNumber =
1500            this.getLineForScriptOffset(script, sourceOffset) - firstLineNumber;
1501        samplesTotal += sampleCount;
1502        lineSampleCounts[lineNumber] =
1503            (lineSampleCounts[lineNumber] || 0) + sampleCount;
1504      });
1505    }
1506
1507    return {
1508      source: lines,
1509      lineSampleCounts: lineSampleCounts,
1510      samplesTotal: samplesTotal,
1511      firstLineNumber: firstLineNumber + 1  // Source code is 1-indexed.
1512    };
1513  }
1514}
1515
1516class FunctionSourceData {
1517  constructor(scriptId, startScriptOffset, endScriptOffset) {
1518    this.scriptId = scriptId;
1519    this.startScriptOffset = startScriptOffset;
1520    this.endScriptOffset = endScriptOffset;
1521
1522    this.codes = new Map();
1523  }
1524
1525  addSourceBlock(codeId, source) {
1526    this.codes.set(codeId, {
1527      positionTable: new SourcePositionTable(source.positions),
1528      offsets: []
1529    });
1530  }
1531
1532  addOffsetSample(codeId, codeOffset) {
1533    let codeIdOffsets = this.codes.get(codeId).offsets;
1534    codeIdOffsets[codeOffset] = (codeIdOffsets[codeOffset] || 0) + 1;
1535  }
1536}
1537
1538class SourcePositionTable {
1539  constructor(encodedTable) {
1540    this.offsetTable = [];
1541    let offsetPairRegex = /C([0-9]+)O([0-9]+)/g;
1542    while (true) {
1543      let regexResult = offsetPairRegex.exec(encodedTable);
1544      if (!regexResult) break;
1545      let codeOffset = parseInt(regexResult[1]);
1546      let scriptOffset = parseInt(regexResult[2]);
1547      if (isNaN(codeOffset) || isNaN(scriptOffset)) continue;
1548      this.offsetTable.push(codeOffset, scriptOffset);
1549    }
1550  }
1551
1552  getScriptOffset(codeOffset) {
1553    console.assert(codeOffset >= 0);
1554    for (let i = this.offsetTable.length - 2; i >= 0; i -= 2) {
1555      if (this.offsetTable[i] <= codeOffset) {
1556        return this.offsetTable[i + 1];
1557      }
1558    }
1559    return this.offsetTable[1];
1560  }
1561}
1562
1563class HelpView {
1564  constructor() {
1565    this.element = $("help");
1566  }
1567
1568  render(newState) {
1569    this.element.style.display = newState.file ? "none" : "inherit";
1570  }
1571}
1572