Import Cobalt 20.master.0.234144

Includes the following patches:
  https://cobalt-review.googlesource.com/c/cobalt/+/5590
  by n1214.hwang@samsung.com

  https://cobalt-review.googlesource.com/c/cobalt/+/5530
  by errong.leng@samsung.com

  https://cobalt-review.googlesource.com/c/cobalt/+/5570
  by devin.cai@mediatek.com
diff --git a/src/v8/tools/profview/index.html b/src/v8/tools/profview/index.html
index 32f7c7b..8695a41 100644
--- a/src/v8/tools/profview/index.html
+++ b/src/v8/tools/profview/index.html
@@ -22,7 +22,7 @@
   Chrome V8 profiling log processor
 </h3>
 
-<input type="file" id="fileinput" />
+<input type="file" id="fileinput" /><div id="source-status"></div>
 <br>
 <hr>
 
@@ -59,6 +59,10 @@
   </table>
   <div>
     Current code object: <span id="timeline-currentCode"></span>
+    <button id="source-viewer-hide-button">Hide source</button>
+  </div>
+  <div>
+    <table id="source-viewer"> </table>
   </div>
 </div>
 
@@ -108,7 +112,7 @@
 <br>
 <br>
 <br>
-Copyright the V8 Authors - Last change to this page: 2017/02/15
+Copyright the V8 Authors - Last change to this page: 2018/08/13
 </p>
 
 </body>
diff --git a/src/v8/tools/profview/profile-utils.js b/src/v8/tools/profview/profile-utils.js
index f5a85be..4be5589 100644
--- a/src/v8/tools/profview/profile-utils.js
+++ b/src/v8/tools/profview/profile-utils.js
@@ -93,9 +93,10 @@
 
 function createNodeFromStackEntry(code, codeId, vmState) {
   let name = code ? code.name : "UNKNOWN";
-
-  return { name, codeId, type : resolveCodeKindAndVmState(code, vmState),
-           children : [], ownTicks : 0, ticks : 0 };
+  let node = createEmptyNode(name);
+  node.codeId = codeId;
+  node.type = resolveCodeKindAndVmState(code, vmState);
+  return node;
 }
 
 function childIdFromCode(codeId, code) {
@@ -148,29 +149,30 @@
 }
 
 function addOrUpdateChildNode(parent, file, stackIndex, stackPos, ascending) {
-  let stack = file.ticks[stackIndex].s;
-  let vmState = file.ticks[stackIndex].vm;
-  let codeId = stack[stackPos];
-  let code = codeId >= 0 ? file.code[codeId] : undefined;
   if (stackPos === -1) {
     // We reached the end without finding the next step.
     // If we are doing top-down call tree, update own ticks.
     if (!ascending) {
       parent.ownTicks++;
     }
-  } else {
-    console.assert(stackPos >= 0 && stackPos < stack.length);
-    // We found a child node.
-    let childId = childIdFromCode(codeId, code);
-    let child = parent.children[childId];
-    if (!child) {
-      child = createNodeFromStackEntry(code, codeId, vmState);
-      child.delayedExpansion = { frameList : [], ascending };
-      parent.children[childId] = child;
-    }
-    child.ticks++;
-    addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos);
+    return;
   }
+
+  let stack = file.ticks[stackIndex].s;
+  console.assert(stackPos >= 0 && stackPos < stack.length);
+  let codeId = stack[stackPos];
+  let code = codeId >= 0 ? file.code[codeId] : undefined;
+  // We found a child node.
+  let childId = childIdFromCode(codeId, code);
+  let child = parent.children[childId];
+  if (!child) {
+    let vmState = file.ticks[stackIndex].vm;
+    child = createNodeFromStackEntry(code, codeId, vmState);
+    child.delayedExpansion = { frameList : [], ascending };
+    parent.children[childId] = child;
+  }
+  child.ticks++;
+  addFrameToFrameList(child.delayedExpansion.frameList, stackIndex, stackPos);
 }
 
 // This expands a tree node (direct children only).
@@ -314,13 +316,7 @@
       this.tree = root;
       this.categories = categories;
     } else {
-      this.tree = {
-        name : "root",
-        codeId: -1,
-        children : [],
-        ownTicks : 0,
-        ticks : 0
-      };
+      this.tree = createEmptyNode("root");
       this.categories = null;
     }
 
@@ -339,7 +335,7 @@
       let codeId = stack[i];
       if (codeId < 0 || this.codeVisited[codeId]) continue;
 
-      let code = codeId >= 0 ? file.code[codeId] : undefined;
+      let code = file.code[codeId];
       if (this.filter) {
         let type = code ? code.type : undefined;
         let kind = code ? code.kind : undefined;
@@ -601,3 +597,15 @@
     softDeoptimizations,
   };
 }
+
+function normalizeLeadingWhitespace(lines) {
+  let regex = /^\s*/;
+  let minimumLeadingWhitespaceChars = Infinity;
+  for (let line of lines) {
+    minimumLeadingWhitespaceChars =
+        Math.min(minimumLeadingWhitespaceChars, regex.exec(line)[0].length);
+  }
+  for (let i = 0; i < lines.length; i++) {
+    lines[i] = lines[i].substring(minimumLeadingWhitespaceChars);
+  }
+}
diff --git a/src/v8/tools/profview/profview.css b/src/v8/tools/profview/profview.css
index 106bfe2..ca39745 100644
--- a/src/v8/tools/profview/profview.css
+++ b/src/v8/tools/profview/profview.css
@@ -2,6 +2,11 @@
   width : 100%;
 }
 
+td {
+  padding-top: 0.1em;
+  padding-bottom: 0.1em;
+}
+
 .numeric {
   width : 12ex;
 }
@@ -14,31 +19,82 @@
   font-family: 'Roboto', sans-serif;
 }
 
-div.code-type-chip {
-  display : inline-block;
-  padding : 0.0em;
+#source-status {
+  display: inline-block;
 }
 
-span.code-type-chip {
+.tree-row-arrow {
+  margin-right: 0.2em;
+  text-align: right;
+}
+
+.code-type-chip {
   border-radius : 1em;
-  display : inline-block;
-  padding : 0.1em;
+  padding : 0.2em;
   background-color : #4040c0;
   color: #ffffff;
   font-size : small;
   box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
 }
 
-span.code-type-chip-space {
-  width : 0.5ex;
-  display : inline-block;
+.tree-row-name {
+  margin-left: 0.2em;
+  margin-right: 0.2em;
 }
 
-span.codeid-link {
+.codeid-link {
   text-decoration: underline;
   cursor: pointer;
 }
 
+.view-source-link {
+  text-decoration: underline;
+  cursor: pointer;
+  font-size: 10pt;
+  margin-left: 0.6em;
+  color: #555555;
+}
+
+#source-viewer {
+  border: 1px solid black;
+  padding: 0.2em;
+  font-family: 'Roboto Mono', monospace;
+  white-space: pre;
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
+
+#source-viewer td.line-none {
+  background-color: white;
+}
+
+#source-viewer td.line-cold {
+  background-color: #e1f5fe;
+}
+
+#source-viewer td.line-mediumcold {
+  background-color: #b2ebf2;
+}
+
+#source-viewer td.line-mediumhot {
+  background-color: #c5e1a5;
+}
+
+#source-viewer td.line-hot {
+  background-color: #dce775;
+}
+
+#source-viewer td.line-superhot {
+  background-color: #ffee58;
+}
+
+#source-viewer .source-line-number {
+  padding-left: 0.2em;
+  padding-right: 0.2em;
+  color: #003c8f;
+  background-color: #eceff1;
+}
+
 div.mode-button {
   padding: 1em 3em;
   display: inline-block;
diff --git a/src/v8/tools/profview/profview.js b/src/v8/tools/profview/profview.js
index d480cd4..210cec7 100644
--- a/src/v8/tools/profview/profview.js
+++ b/src/v8/tools/profview/profview.js
@@ -8,34 +8,42 @@
   return document.getElementById(id);
 }
 
-let components = [];
+function removeAllChildren(element) {
+  while (element.firstChild) {
+    element.removeChild(element.firstChild);
+  }
+}
 
+let components;
 function createViews() {
-  components.push(new CallTreeView());
-  components.push(new TimelineView());
-  components.push(new HelpView());
-  components.push(new SummaryView());
-  components.push(new ModeBarView());
-
-  main.setMode("summary");
+  components = [
+    new CallTreeView(),
+    new TimelineView(),
+    new HelpView(),
+    new SummaryView(),
+    new ModeBarView(),
+    new ScriptSourceView(),
+  ];
 }
 
 function emptyState() {
   return {
     file : null,
-    mode : "none",
+    mode : null,
     currentCodeId : null,
+    viewingSource: false,
     start : 0,
     end : Infinity,
-    timeLine : {
-      width : 100,
-      height : 100
+    timelineSize : {
+      width : 0,
+      height : 0
     },
     callTree : {
       attribution : "js-exclude-bc",
       categories : "code-type",
       sort : "time"
-    }
+    },
+    sourceData: null
   };
 }
 
@@ -47,6 +55,7 @@
 
 let main = {
   currentState : emptyState(),
+  renderPending : false,
 
   setMode(mode) {
     if (mode !== main.currentState.mode) {
@@ -120,22 +129,28 @@
     }
   },
 
-  setTimeLineDimensions(width, height) {
-    if (width !== main.currentState.timeLine.width ||
-        height !== main.currentState.timeLine.height) {
-      let timeLine = Object.assign({}, main.currentState.timeLine);
-      timeLine.width = width;
-      timeLine.height = height;
-      main.currentState = Object.assign({}, main.currentState);
-      main.currentState.timeLine = timeLine;
-      main.delayRender();
+  updateSources(file) {
+    let statusDiv = $("source-status");
+    if (!file) {
+      statusDiv.textContent = "";
+      return;
     }
+    if (!file.scripts || file.scripts.length === 0) {
+      statusDiv.textContent =
+          "Script source not available. Run profiler with --log-source-code.";
+      return;
+    }
+    statusDiv.textContent = "Script source is available.";
+    main.currentState.sourceData = new SourceData(file);
   },
 
   setFile(file) {
     if (file !== main.currentState.file) {
-      main.currentState = Object.assign({}, main.currentState);
+      let lastMode = main.currentState.mode || "summary";
+      main.currentState = emptyState();
       main.currentState.file = file;
+      main.updateSources(file);
+      main.setMode(lastMode);
       main.delayRender();
     }
   },
@@ -148,10 +163,16 @@
     }
   },
 
+  setViewingSource(value) {
+    if (main.currentState.viewingSource !== value) {
+      main.currentState = Object.assign({}, main.currentState);
+      main.currentState.viewingSource = value;
+      main.delayRender();
+    }
+  },
+
   onResize() {
-    main.setTimeLineDimensions(
-      Math.round(window.innerWidth - 20),
-      Math.round(window.innerHeight / 5));
+    main.delayRender();
   },
 
   onLoad() {
@@ -160,9 +181,7 @@
       if (f) {
         let reader = new FileReader();
         reader.onload = function(event) {
-          let profData = JSON.parse(event.target.result);
-          main.setViewInterval(0, Infinity);
-          main.setFile(profData);
+          main.setFile(JSON.parse(event.target.result));
         };
         reader.onerror = function(event) {
           console.error(
@@ -176,11 +195,14 @@
     $("fileinput").addEventListener(
         "change", loadHandler, false);
     createViews();
-    main.onResize();
   },
 
   delayRender()  {
-    Promise.resolve().then(() => {
+    if (main.renderPending) return;
+    main.renderPending = true;
+
+    window.requestAnimationFrame(() => {
+      main.renderPending = false;
       for (let c of components) {
         c.render(main.currentState);
       }
@@ -188,50 +210,51 @@
   }
 };
 
-let bucketDescriptors =
+const CATEGORY_COLOR = "#f5f5f5";
+const bucketDescriptors =
     [ { kinds : [ "JSOPT" ],
-        color : "#00ff00",
-        backgroundColor : "#c0ffc0",
+        color : "#64dd17",
+        backgroundColor : "#80e27e",
         text : "JS Optimized" },
       { kinds : [ "JSUNOPT", "BC" ],
-        color : "#ffb000",
-        backgroundColor : "#ffe0c0",
+        color : "#dd2c00",
+        backgroundColor : "#ff9e80",
         text : "JS Unoptimized" },
       { kinds : [ "IC" ],
-        color : "#ffff00",
-        backgroundColor : "#ffffc0",
+        color : "#ff6d00",
+        backgroundColor : "#ffab40",
         text : "IC" },
       { kinds : [ "STUB", "BUILTIN", "REGEXP" ],
-        color : "#ffb0b0",
-        backgroundColor : "#fff0f0",
+        color : "#ffd600",
+        backgroundColor : "#ffea00",
         text : "Other generated" },
       { kinds : [ "CPP", "LIB" ],
-        color : "#0000ff",
-        backgroundColor : "#c0c0ff",
+        color : "#304ffe",
+        backgroundColor : "#6ab7ff",
         text : "C++" },
       { kinds : [ "CPPEXT" ],
-        color : "#8080ff",
-        backgroundColor : "#e0e0ff",
+        color : "#003c8f",
+        backgroundColor : "#c0cfff",
         text : "C++/external" },
       { kinds : [ "CPPPARSE" ],
-        color : "#b890f7",
-        backgroundColor : "#ebdeff",
+        color : "#aa00ff",
+        backgroundColor : "#ffb2ff",
         text : "C++/Parser" },
       { kinds : [ "CPPCOMPBC" ],
-        color : "#52b0ce",
-        backgroundColor : "#a5c8d4",
+        color : "#43a047",
+        backgroundColor : "#88c399",
         text : "C++/Bytecode compiler" },
       { kinds : [ "CPPCOMP" ],
-        color : "#00ffff",
-        backgroundColor : "#c0ffff",
+        color : "#00e5ff",
+        backgroundColor : "#6effff",
         text : "C++/Compiler" },
       { kinds : [ "CPPGC" ],
-        color : "#ff00ff",
-        backgroundColor : "#ffc0ff",
+        color : "#6200ea",
+        backgroundColor : "#e1bee7",
         text : "C++/GC" },
       { kinds : [ "UNKNOWN" ],
-        color : "#f0f0f0",
-        backgroundColor : "#e0e0e0",
+        color : "#bdbdbd",
+        backgroundColor : "#efefef",
         text : "Unknown" }
     ];
 
@@ -260,13 +283,13 @@
     case "UNKNOWN":
       return "Unknown";
     case "CPPPARSE":
-      return "C++ (parser)";
+      return "C++ Parser";
     case "CPPCOMPBC":
-      return "C++ (bytecode compiler)";
+      return "C++ Bytecode Compiler)";
     case "CPPCOMP":
-      return "C++ (compiler)";
+      return "C++ Compiler";
     case "CPPGC":
-      return "C++";
+      return "C++ GC";
     case "CPPEXT":
       return "C++ External";
     case "CPP":
@@ -291,27 +314,15 @@
   console.error("Unknown type: " + type);
 }
 
-function createTypeDiv(type) {
+function createTypeNode(type) {
   if (type === "CAT") {
     return document.createTextNode("");
   }
-  let div = document.createElement("div");
-  div.classList.add("code-type-chip");
-
   let span = document.createElement("span");
   span.classList.add("code-type-chip");
   span.textContent = codeTypeToText(type);
-  div.appendChild(span);
 
-  span = document.createElement("span");
-  span.classList.add("code-type-chip-space");
-  div.appendChild(span);
-
-  return div;
-}
-
-function isBytecodeHandler(kind) {
-  return kind === "BytecodeHandler";
+  return span;
 }
 
 function filterFromFilterId(id) {
@@ -322,31 +333,56 @@
       return (type, kind) => type !== 'CODE';
     case "js-exclude-bc":
       return (type, kind) =>
-          type !== 'CODE' || !isBytecodeHandler(kind);
+          type !== 'CODE' || kind !== "BytecodeHandler";
   }
 }
 
-function createTableExpander(indent) {
+function createIndentNode(indent) {
   let div = document.createElement("div");
-  div.style.width = (indent + 0.5) + "em";
   div.style.display = "inline-block";
-  div.style.textAlign = "right";
+  div.style.width = (indent + 0.5) + "em";
   return div;
 }
 
+function createArrowNode() {
+  let span = document.createElement("span");
+  span.classList.add("tree-row-arrow");
+  return span;
+}
+
 function createFunctionNode(name, codeId) {
-  if (codeId === -1) {
-    return document.createTextNode(name);
-  }
   let nameElement = document.createElement("span");
-  nameElement.classList.add("codeid-link");
-  nameElement.onclick = function() {
-    main.setCurrentCode(codeId);
-  };
   nameElement.appendChild(document.createTextNode(name));
+  nameElement.classList.add("tree-row-name");
+  if (codeId !== -1) {
+    nameElement.classList.add("codeid-link");
+    nameElement.onclick = (event) => {
+      main.setCurrentCode(codeId);
+      // Prevent the click from bubbling to the row and causing it to
+      // collapse/expand.
+      event.stopPropagation();
+    };
+  }
   return nameElement;
 }
 
+function createViewSourceNode(codeId) {
+  let linkElement = document.createElement("span");
+  linkElement.appendChild(document.createTextNode("View source"));
+  linkElement.classList.add("view-source-link");
+  linkElement.onclick = (event) => {
+    main.setCurrentCode(codeId);
+    main.setViewingSource(true);
+    // Prevent the click from bubbling to the row and causing it to
+    // collapse/expand.
+    event.stopPropagation();
+  };
+  return linkElement;
+}
+
+const COLLAPSED_ARROW = "\u25B6";
+const EXPANDED_ARROW = "\u25BC";
+
 class CallTreeView {
   constructor() {
     this.element = $("calltree");
@@ -400,22 +436,19 @@
   }
 
   expandTree(tree, indent) {
-    let that = this;
     let index = 0;
     let id = "R/";
     let row = tree.row;
-    let expander = tree.expander;
 
     if (row) {
       index = row.rowIndex;
       id = row.id;
 
-      // Make sure we collapse the children when the row is clicked
-      // again.
-      expander.textContent = "\u25BE";
-      let expandHandler = expander.onclick;
-      expander.onclick = () => {
-        that.collapseRow(tree, expander, expandHandler);
+      tree.arrow.textContent = EXPANDED_ARROW;
+      // Collapse the children when the row is clicked again.
+      let expandHandler = row.onclick;
+      row.onclick = () => {
+        this.collapseRow(tree, expandHandler);
       }
     }
 
@@ -439,7 +472,9 @@
       let row = this.rows.insertRow(index);
       row.id = id + i + "/";
 
-      if (node.type !== "CAT") {
+      if (node.type === "CAT") {
+        row.style.backgroundColor = CATEGORY_COLOR;
+      } else {
         row.style.backgroundColor = bucketFromKind(node.type).backgroundColor;
       }
 
@@ -460,10 +495,17 @@
 
       // Create the name cell.
       let nameCell = row.insertCell();
-      let expander = createTableExpander(indent + 1);
-      nameCell.appendChild(expander);
-      nameCell.appendChild(createTypeDiv(node.type));
+      nameCell.appendChild(createIndentNode(indent + 1));
+      let arrow = createArrowNode();
+      nameCell.appendChild(arrow);
+      nameCell.appendChild(createTypeNode(node.type));
       nameCell.appendChild(createFunctionNode(node.name, node.codeId));
+      if (main.currentState.sourceData &&
+          node.codeId >= 0 &&
+          main.currentState.sourceData.hasSource(
+              this.currentState.file.code[node.codeId].func)) {
+        nameCell.appendChild(createViewSourceNode(node.codeId));
+      }
 
       // Inclusive ticks cell.
       c = row.insertCell();
@@ -476,18 +518,18 @@
         c.style.textAlign = "right";
       }
       if (node.children.length > 0) {
-        expander.textContent = "\u25B8";
-        expander.onclick = () => { that.expandTree(node, indent + 1); };
+        arrow.textContent = COLLAPSED_ARROW;
+        row.onclick = () => { this.expandTree(node, indent + 1); };
       }
 
       node.row = row;
-      node.expander = expander;
+      node.arrow = arrow;
 
       index++;
     }
   }
 
-  collapseRow(tree, expander, expandHandler) {
+  collapseRow(tree, expandHandler) {
     let row = tree.row;
     let id = row.id;
     let index = row.rowIndex;
@@ -496,8 +538,8 @@
       this.rows.deleteRow(index);
     }
 
-    expander.textContent = "\u25B8";
-    expander.onclick = expandHandler;
+    tree.arrow.textContent = COLLAPSED_ARROW;
+    row.onclick = expandHandler;
   }
 
   fillSelects(mode, calltree) {
@@ -809,10 +851,12 @@
       return;
     }
 
-    this.currentState = newState;
+    let width = Math.round(document.documentElement.clientWidth - 20);
+    let height = Math.round(document.documentElement.clientHeight / 5);
+
     if (oldState) {
-      if (newState.timeLine.width === oldState.timeLine.width &&
-          newState.timeLine.height === oldState.timeLine.height &&
+      if (width === oldState.timelineSize.width &&
+          height === oldState.timelineSize.height &&
           newState.file === oldState.file &&
           newState.currentCodeId === oldState.currentCodeId &&
           newState.start === oldState.start &&
@@ -821,21 +865,27 @@
         return;
       }
     }
+    this.currentState = newState;
+    this.currentState.timelineSize.width = width;
+    this.currentState.timelineSize.height = height;
 
     this.element.style.display = "inherit";
 
+    let file = this.currentState.file;
+
+    const minPixelsPerBucket = 10;
+    const minTicksPerBucket = 8;
+    let maxBuckets = Math.round(file.ticks.length / minTicksPerBucket);
+    let bucketCount = Math.min(
+        Math.round(width / minPixelsPerBucket), maxBuckets);
+
     // Make sure the canvas has the right dimensions.
-    let width = this.currentState.timeLine.width;
-    let height = this.currentState.timeLine.height;
     this.canvas.width = width;
     this.canvas.height  = height;
 
     // Make space for the selection text.
     height -= this.imageOffset;
 
-    let file = this.currentState.file;
-    if (!file) return;
-
     let currentCodeId = this.currentState.currentCodeId;
 
     let firstTime = file.ticks[0].tm;
@@ -846,13 +896,6 @@
     this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width;
     this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width;
 
-    let tickCount = file.ticks.length;
-
-    let minBucketPixels = 10;
-    let minBucketSamples = 30;
-    let bucketCount = Math.min(width / minBucketPixels,
-                               tickCount / minBucketSamples);
-
     let stackProcessor = new CategorySampler(file, bucketCount);
     generateTree(file, 0, Infinity, stackProcessor);
     let codeIdProcessor = new FunctionTimelineProcessor(
@@ -873,28 +916,36 @@
       let sum = 0;
       let bucketData = [];
       let total = buckets[i].total;
-      for (let j = 0; j < bucketDescriptors.length; j++) {
-        let desc = bucketDescriptors[j];
-        for (let k = 0; k < desc.kinds.length; k++) {
-          sum += buckets[i][desc.kinds[k]];
+      if (total > 0) {
+        for (let j = 0; j < bucketDescriptors.length; j++) {
+          let desc = bucketDescriptors[j];
+          for (let k = 0; k < desc.kinds.length; k++) {
+            sum += buckets[i][desc.kinds[k]];
+          }
+          bucketData.push(Math.round(graphHeight * sum / total));
         }
-        bucketData.push(Math.round(graphHeight * sum / total));
+      } else {
+        // No ticks fell into this bucket. Fill with "Unknown."
+        for (let j = 0; j < bucketDescriptors.length; j++) {
+          let desc = bucketDescriptors[j];
+          bucketData.push(desc.text === "Unknown" ? graphHeight : 0);
+        }
       }
       bucketsGraph.push(bucketData);
     }
 
     // Draw the category graph into the buffer.
-    let bucketWidth = width / bucketsGraph.length;
+    let bucketWidth = width / (bucketsGraph.length - 1);
     let ctx = buffer.getContext('2d');
     for (let i = 0; i < bucketsGraph.length - 1; i++) {
       let bucketData = bucketsGraph[i];
       let nextBucketData = bucketsGraph[i + 1];
+      let x1 = Math.round(i * bucketWidth);
+      let x2 = Math.round((i + 1) * bucketWidth);
       for (let j = 0; j < bucketData.length; j++) {
-        let x1 = Math.round(i * bucketWidth);
-        let x2 = Math.round((i + 1) * bucketWidth);
         ctx.beginPath();
-        ctx.moveTo(x1, j && bucketData[j - 1]);
-        ctx.lineTo(x2, j && nextBucketData[j - 1]);
+        ctx.moveTo(x1, j > 0 ? bucketData[j - 1] : 0);
+        ctx.lineTo(x2, j > 0 ? nextBucketData[j - 1] : 0);
         ctx.lineTo(x2, nextBucketData[j]);
         ctx.lineTo(x1, bucketData[j]);
         ctx.closePath();
@@ -1017,9 +1068,7 @@
       cell.appendChild(document.createTextNode(" " + desc.text));
     }
 
-    while (this.currentCode.firstChild) {
-      this.currentCode.removeChild(this.currentCode.firstChild);
-    }
+    removeAllChildren(this.currentCode);
     if (currentCodeId) {
       let currentCode = file.code[currentCodeId];
       this.currentCode.appendChild(document.createTextNode(currentCode.name));
@@ -1090,10 +1139,7 @@
     }
 
     this.element.style.display = "inherit";
-
-    while (this.element.firstChild) {
-      this.element.removeChild(this.element.firstChild);
-    }
+    removeAllChildren(this.element);
 
     let stats = computeOptimizationStats(
         this.currentState.file, newState.start, newState.end);
@@ -1114,22 +1160,22 @@
       return row;
     }
 
-    function makeCollapsible(row, expander) {
-      expander.textContent = "\u25BE";
-      let expandHandler = expander.onclick;
-      expander.onclick = () => {
+    function makeCollapsible(row, arrow) {
+      arrow.textContent = EXPANDED_ARROW;
+      let expandHandler = row.onclick;
+      row.onclick = () => {
         let id = row.id;
         let index = row.rowIndex + 1;
         while (index < rows.rows.length &&
           rows.rows[index].id.startsWith(id)) {
           rows.deleteRow(index);
         }
-        expander.textContent = "\u25B8";
-        expander.onclick = expandHandler;
+        arrow.textContent = COLLAPSED_ARROW;
+        row.onclick = expandHandler;
       }
     }
 
-    function expandDeoptInstances(row, expander, instances, indent, kind) {
+    function expandDeoptInstances(row, arrow, instances, indent, kind) {
       let index = row.rowIndex;
       for (let i = 0; i < instances.length; i++) {
         let childRow = rows.insertRow(index + 1);
@@ -1145,18 +1191,19 @@
             document.createTextNode("Reason: " + deopt.reason));
         reasonCell.style.textIndent = indent + "em";
       }
-      makeCollapsible(row, expander);
+      makeCollapsible(row, arrow);
     }
 
-    function expandDeoptFunctionList(row, expander, list, indent, kind) {
+    function expandDeoptFunctionList(row, arrow, list, indent, kind) {
       let index = row.rowIndex;
       for (let i = 0; i < list.length; i++) {
         let childRow = rows.insertRow(index + 1);
         childRow.id = row.id + i + "/";
 
         let textCell = childRow.insertCell(-1);
-        let expander = createTableExpander(indent);
-        textCell.appendChild(expander);
+        textCell.appendChild(createIndentNode(indent));
+        let childArrow = createArrowNode();
+        textCell.appendChild(childArrow);
         textCell.appendChild(
             createFunctionNode(list[i].f.name, list[i].f.codes[0]));
 
@@ -1164,16 +1211,16 @@
         numberCell.textContent = list[i].instances.length;
         numberCell.style.textIndent = indent + "em";
 
-        expander.textContent = "\u25B8";
-        expander.onclick = () => {
+        childArrow.textContent = COLLAPSED_ARROW;
+        childRow.onclick = () => {
           expandDeoptInstances(
-              childRow, expander, list[i].instances, indent + 1);
+              childRow, childArrow, list[i].instances, indent + 1);
         };
       }
-      makeCollapsible(row, expander);
+      makeCollapsible(row, arrow);
     }
 
-    function expandOptimizedFunctionList(row, expander, list, indent, kind) {
+    function expandOptimizedFunctionList(row, arrow, list, indent, kind) {
       let index = row.rowIndex;
       for (let i = 0; i < list.length; i++) {
         let childRow = rows.insertRow(index + 1);
@@ -1188,17 +1235,19 @@
         numberCell.textContent = list[i].instances.length;
         numberCell.style.textIndent = indent + "em";
       }
-      makeCollapsible(row, expander);
+      makeCollapsible(row, arrow);
     }
 
     function addExpandableRow(text, list, indent, kind) {
       let row = rows.insertRow(-1);
 
       row.id = "opt-table/" + kind + "/";
+      row.style.backgroundColor = CATEGORY_COLOR;
 
       let textCell = row.insertCell(-1);
-      let expander = createTableExpander(indent);
-      textCell.appendChild(expander);
+      textCell.appendChild(createIndentNode(indent));
+      let arrow = createArrowNode();
+      textCell.appendChild(arrow);
       textCell.appendChild(document.createTextNode(text));
 
       let numberCell = row.insertCell(-1);
@@ -1208,16 +1257,16 @@
       }
 
       if (list.count > 0) {
-        expander.textContent = "\u25B8";
+        arrow.textContent = COLLAPSED_ARROW;
         if (kind === "opt") {
-          expander.onclick = () => {
+          row.onclick = () => {
             expandOptimizedFunctionList(
-                row, expander, list.functions, indent + 1, kind);
+                row, arrow, list.functions, indent + 1, kind);
           };
         } else {
-          expander.onclick = () => {
+          row.onclick = () => {
             expandDeoptFunctionList(
-                row, expander, list.functions, indent + 1, kind);
+                row, arrow, list.functions, indent + 1, kind);
           };
         }
       }
@@ -1241,6 +1290,217 @@
   }
 }
 
+class ScriptSourceView {
+  constructor() {
+    this.table = $("source-viewer");
+    this.hideButton = $("source-viewer-hide-button");
+    this.hideButton.onclick = () => {
+      main.setViewingSource(false);
+    };
+  }
+
+  render(newState) {
+    let oldState = this.currentState;
+    if (!newState.file || !newState.viewingSource) {
+      this.table.style.display = "none";
+      this.hideButton.style.display = "none";
+      this.currentState = null;
+      return;
+    }
+    if (oldState) {
+      if (newState.file === oldState.file &&
+          newState.currentCodeId === oldState.currentCodeId &&
+          newState.viewingSource === oldState.viewingSource) {
+        // No change, nothing to do.
+        return;
+      }
+    }
+    this.currentState = newState;
+
+    this.table.style.display = "inline-block";
+    this.hideButton.style.display = "inline";
+    removeAllChildren(this.table);
+
+    let functionId =
+        this.currentState.file.code[this.currentState.currentCodeId].func;
+    let sourceView =
+        this.currentState.sourceData.generateSourceView(functionId);
+    for (let i = 0; i < sourceView.source.length; i++) {
+      let sampleCount = sourceView.lineSampleCounts[i] || 0;
+      let sampleProportion = sourceView.samplesTotal > 0 ?
+                             sampleCount / sourceView.samplesTotal : 0;
+      let heatBucket;
+      if (sampleProportion === 0) {
+        heatBucket = "line-none";
+      } else if (sampleProportion < 0.2) {
+        heatBucket = "line-cold";
+      } else if (sampleProportion < 0.4) {
+        heatBucket = "line-mediumcold";
+      } else if (sampleProportion < 0.6) {
+        heatBucket = "line-mediumhot";
+      } else if (sampleProportion < 0.8) {
+        heatBucket = "line-hot";
+      } else {
+        heatBucket = "line-superhot";
+      }
+
+      let row = this.table.insertRow(-1);
+
+      let lineNumberCell = row.insertCell(-1);
+      lineNumberCell.classList.add("source-line-number");
+      lineNumberCell.textContent = i + sourceView.firstLineNumber;
+
+      let sampleCountCell = row.insertCell(-1);
+      sampleCountCell.classList.add(heatBucket);
+      sampleCountCell.textContent = sampleCount;
+
+      let sourceLineCell = row.insertCell(-1);
+      sourceLineCell.classList.add(heatBucket);
+      sourceLineCell.textContent = sourceView.source[i];
+    }
+
+    $("timeline-currentCode").scrollIntoView();
+  }
+}
+
+class SourceData {
+  constructor(file) {
+    this.scripts = new Map();
+    for (let i = 0; i < file.scripts.length; i++) {
+      const scriptBlock = file.scripts[i];
+      if (scriptBlock === null) continue; // Array may be sparse.
+      let source = scriptBlock.source.split("\n");
+      this.scripts.set(i, source);
+    }
+
+    this.functions = new Map();
+    for (let codeId = 0; codeId < file.code.length; ++codeId) {
+      let codeBlock = file.code[codeId];
+      if (codeBlock.source && codeBlock.func !== undefined) {
+        let data = this.functions.get(codeBlock.func);
+        if (!data) {
+          data = new FunctionSourceData(codeBlock.source.script,
+                                        codeBlock.source.start,
+                                        codeBlock.source.end);
+          this.functions.set(codeBlock.func, data);
+        }
+        data.addSourceBlock(codeId, codeBlock.source);
+      }
+    }
+
+    for (let tick of file.ticks) {
+      let stack = tick.s;
+      for (let i = 0; i < stack.length; i += 2) {
+        let codeId = stack[i];
+        if (codeId < 0) continue;
+        let functionId = file.code[codeId].func;
+        if (this.functions.has(functionId)) {
+          let codeOffset = stack[i + 1];
+          this.functions.get(functionId).addOffsetSample(codeId, codeOffset);
+        }
+      }
+    }
+  }
+
+  getScript(scriptId) {
+    return this.scripts.get(scriptId);
+  }
+
+  getLineForScriptOffset(script, scriptOffset) {
+    let line = 0;
+    let charsConsumed = 0;
+    for (; line < script.length; ++line) {
+      charsConsumed += script[line].length + 1; // Add 1 for newline.
+      if (charsConsumed > scriptOffset) break;
+    }
+    return line;
+  }
+
+  hasSource(functionId) {
+    return this.functions.has(functionId);
+  }
+
+  generateSourceView(functionId) {
+    console.assert(this.hasSource(functionId));
+    let data = this.functions.get(functionId);
+    let scriptId = data.scriptId;
+    let script = this.getScript(scriptId);
+    let firstLineNumber =
+        this.getLineForScriptOffset(script, data.startScriptOffset);
+    let lastLineNumber =
+        this.getLineForScriptOffset(script, data.endScriptOffset);
+    let lines = script.slice(firstLineNumber, lastLineNumber + 1);
+    normalizeLeadingWhitespace(lines);
+
+    let samplesTotal = 0;
+    let lineSampleCounts = [];
+    for (let [codeId, block] of data.codes) {
+      block.offsets.forEach((sampleCount, codeOffset) => {
+        let sourceOffset = block.positionTable.getScriptOffset(codeOffset);
+        let lineNumber =
+            this.getLineForScriptOffset(script, sourceOffset) - firstLineNumber;
+        samplesTotal += sampleCount;
+        lineSampleCounts[lineNumber] =
+            (lineSampleCounts[lineNumber] || 0) + sampleCount;
+      });
+    }
+
+    return {
+      source: lines,
+      lineSampleCounts: lineSampleCounts,
+      samplesTotal: samplesTotal,
+      firstLineNumber: firstLineNumber + 1  // Source code is 1-indexed.
+    };
+  }
+}
+
+class FunctionSourceData {
+  constructor(scriptId, startScriptOffset, endScriptOffset) {
+    this.scriptId = scriptId;
+    this.startScriptOffset = startScriptOffset;
+    this.endScriptOffset = endScriptOffset;
+
+    this.codes = new Map();
+  }
+
+  addSourceBlock(codeId, source) {
+    this.codes.set(codeId, {
+      positionTable: new SourcePositionTable(source.positions),
+      offsets: []
+    });
+  }
+
+  addOffsetSample(codeId, codeOffset) {
+    let codeIdOffsets = this.codes.get(codeId).offsets;
+    codeIdOffsets[codeOffset] = (codeIdOffsets[codeOffset] || 0) + 1;
+  }
+}
+
+class SourcePositionTable {
+  constructor(encodedTable) {
+    this.offsetTable = [];
+    let offsetPairRegex = /C([0-9]+)O([0-9]+)/g;
+    while (true) {
+      let regexResult = offsetPairRegex.exec(encodedTable);
+      if (!regexResult) break;
+      let codeOffset = parseInt(regexResult[1]);
+      let scriptOffset = parseInt(regexResult[2]);
+      if (isNaN(codeOffset) || isNaN(scriptOffset)) continue;
+      this.offsetTable.push(codeOffset, scriptOffset);
+    }
+  }
+
+  getScriptOffset(codeOffset) {
+    console.assert(codeOffset >= 0);
+    for (let i = this.offsetTable.length - 2; i >= 0; i -= 2) {
+      if (this.offsetTable[i] <= codeOffset) {
+        return this.offsetTable[i + 1];
+      }
+    }
+    return this.offsetTable[1];
+  }
+}
+
 class HelpView {
   constructor() {
     this.element = $("help");