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");