| // Copyright 2017 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| "use strict" |
| |
| function $(id) { |
| return document.getElementById(id); |
| } |
| |
| function removeAllChildren(element) { |
| while (element.firstChild) { |
| element.removeChild(element.firstChild); |
| } |
| } |
| |
| let components; |
| function createViews() { |
| components = [ |
| new CallTreeView(), |
| new TimelineView(), |
| new HelpView(), |
| new SummaryView(), |
| new ModeBarView(), |
| new ScriptSourceView(), |
| ]; |
| } |
| |
| function emptyState() { |
| return { |
| file : null, |
| mode : null, |
| currentCodeId : null, |
| viewingSource: false, |
| start : 0, |
| end : Infinity, |
| timelineSize : { |
| width : 0, |
| height : 0 |
| }, |
| callTree : { |
| attribution : "js-exclude-bc", |
| categories : "code-type", |
| sort : "time" |
| }, |
| sourceData: null |
| }; |
| } |
| |
| function setCallTreeState(state, callTreeState) { |
| state = Object.assign({}, state); |
| state.callTree = callTreeState; |
| return state; |
| } |
| |
| let main = { |
| currentState : emptyState(), |
| renderPending : false, |
| |
| setMode(mode) { |
| if (mode !== main.currentState.mode) { |
| |
| function setCallTreeModifiers(attribution, categories, sort) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.attribution = attribution; |
| callTreeState.categories = categories; |
| callTreeState.sort = sort; |
| return callTreeState; |
| } |
| |
| let state = Object.assign({}, main.currentState); |
| |
| switch (mode) { |
| case "bottom-up": |
| state.callTree = |
| setCallTreeModifiers("js-exclude-bc", "code-type", "time"); |
| break; |
| case "top-down": |
| state.callTree = |
| setCallTreeModifiers("js-exclude-bc", "none", "time"); |
| break; |
| case "function-list": |
| state.callTree = |
| setCallTreeModifiers("js-exclude-bc", "code-type", "own-time"); |
| break; |
| } |
| |
| state.mode = mode; |
| |
| main.currentState = state; |
| main.delayRender(); |
| } |
| }, |
| |
| setCallTreeAttribution(attribution) { |
| if (attribution !== main.currentState.attribution) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.attribution = attribution; |
| main.currentState = setCallTreeState(main.currentState, callTreeState); |
| main.delayRender(); |
| } |
| }, |
| |
| setCallTreeSort(sort) { |
| if (sort !== main.currentState.sort) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.sort = sort; |
| main.currentState = setCallTreeState(main.currentState, callTreeState); |
| main.delayRender(); |
| } |
| }, |
| |
| setCallTreeCategories(categories) { |
| if (categories !== main.currentState.categories) { |
| let callTreeState = Object.assign({}, main.currentState.callTree); |
| callTreeState.categories = categories; |
| main.currentState = setCallTreeState(main.currentState, callTreeState); |
| main.delayRender(); |
| } |
| }, |
| |
| setViewInterval(start, end) { |
| if (start !== main.currentState.start || |
| end !== main.currentState.end) { |
| main.currentState = Object.assign({}, main.currentState); |
| main.currentState.start = start; |
| main.currentState.end = end; |
| 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) { |
| let lastMode = main.currentState.mode || "summary"; |
| main.currentState = emptyState(); |
| main.currentState.file = file; |
| main.updateSources(file); |
| main.setMode(lastMode); |
| main.delayRender(); |
| } |
| }, |
| |
| setCurrentCode(codeId) { |
| if (codeId !== main.currentState.currentCodeId) { |
| main.currentState = Object.assign({}, main.currentState); |
| main.currentState.currentCodeId = codeId; |
| main.delayRender(); |
| } |
| }, |
| |
| setViewingSource(value) { |
| if (main.currentState.viewingSource !== value) { |
| main.currentState = Object.assign({}, main.currentState); |
| main.currentState.viewingSource = value; |
| main.delayRender(); |
| } |
| }, |
| |
| onResize() { |
| main.delayRender(); |
| }, |
| |
| onLoad() { |
| function loadHandler(evt) { |
| let f = evt.target.files[0]; |
| if (f) { |
| let reader = new FileReader(); |
| reader.onload = function(event) { |
| main.setFile(JSON.parse(event.target.result)); |
| }; |
| reader.onerror = function(event) { |
| console.error( |
| "File could not be read! Code " + event.target.error.code); |
| }; |
| reader.readAsText(f); |
| } else { |
| main.setFile(null); |
| } |
| } |
| $("fileinput").addEventListener( |
| "change", loadHandler, false); |
| createViews(); |
| }, |
| |
| delayRender() { |
| if (main.renderPending) return; |
| main.renderPending = true; |
| |
| window.requestAnimationFrame(() => { |
| main.renderPending = false; |
| for (let c of components) { |
| c.render(main.currentState); |
| } |
| }); |
| } |
| }; |
| |
| const CATEGORY_COLOR = "#f5f5f5"; |
| const bucketDescriptors = |
| [ { kinds : [ "JSOPT" ], |
| color : "#64dd17", |
| backgroundColor : "#80e27e", |
| text : "JS Optimized" }, |
| { kinds : [ "JSUNOPT", "BC" ], |
| color : "#dd2c00", |
| backgroundColor : "#ff9e80", |
| text : "JS Unoptimized" }, |
| { kinds : [ "IC" ], |
| color : "#ff6d00", |
| backgroundColor : "#ffab40", |
| text : "IC" }, |
| { kinds : [ "STUB", "BUILTIN", "REGEXP" ], |
| color : "#ffd600", |
| backgroundColor : "#ffea00", |
| text : "Other generated" }, |
| { kinds : [ "CPP", "LIB" ], |
| color : "#304ffe", |
| backgroundColor : "#6ab7ff", |
| text : "C++" }, |
| { kinds : [ "CPPEXT" ], |
| color : "#003c8f", |
| backgroundColor : "#c0cfff", |
| text : "C++/external" }, |
| { kinds : [ "CPPPARSE" ], |
| color : "#aa00ff", |
| backgroundColor : "#ffb2ff", |
| text : "C++/Parser" }, |
| { kinds : [ "CPPCOMPBC" ], |
| color : "#43a047", |
| backgroundColor : "#88c399", |
| text : "C++/Bytecode compiler" }, |
| { kinds : [ "CPPCOMP" ], |
| color : "#00e5ff", |
| backgroundColor : "#6effff", |
| text : "C++/Compiler" }, |
| { kinds : [ "CPPGC" ], |
| color : "#6200ea", |
| backgroundColor : "#e1bee7", |
| text : "C++/GC" }, |
| { kinds : [ "UNKNOWN" ], |
| color : "#bdbdbd", |
| backgroundColor : "#efefef", |
| text : "Unknown" } |
| ]; |
| |
| let kindToBucketDescriptor = {}; |
| for (let i = 0; i < bucketDescriptors.length; i++) { |
| let bucket = bucketDescriptors[i]; |
| for (let j = 0; j < bucket.kinds.length; j++) { |
| kindToBucketDescriptor[bucket.kinds[j]] = bucket; |
| } |
| } |
| |
| function bucketFromKind(kind) { |
| for (let i = 0; i < bucketDescriptors.length; i++) { |
| let bucket = bucketDescriptors[i]; |
| for (let j = 0; j < bucket.kinds.length; j++) { |
| if (bucket.kinds[j] === kind) { |
| return bucket; |
| } |
| } |
| } |
| return null; |
| } |
| |
| function codeTypeToText(type) { |
| switch (type) { |
| case "UNKNOWN": |
| return "Unknown"; |
| case "CPPPARSE": |
| return "C++ Parser"; |
| case "CPPCOMPBC": |
| return "C++ Bytecode Compiler)"; |
| case "CPPCOMP": |
| return "C++ Compiler"; |
| case "CPPGC": |
| return "C++ GC"; |
| case "CPPEXT": |
| return "C++ External"; |
| case "CPP": |
| return "C++"; |
| case "LIB": |
| return "Library"; |
| case "IC": |
| return "IC"; |
| case "BC": |
| return "Bytecode"; |
| case "STUB": |
| return "Stub"; |
| case "BUILTIN": |
| return "Builtin"; |
| case "REGEXP": |
| return "RegExp"; |
| case "JSOPT": |
| return "JS opt"; |
| case "JSUNOPT": |
| return "JS unopt"; |
| } |
| console.error("Unknown type: " + type); |
| } |
| |
| function createTypeNode(type) { |
| if (type === "CAT") { |
| return document.createTextNode(""); |
| } |
| let span = document.createElement("span"); |
| span.classList.add("code-type-chip"); |
| span.textContent = codeTypeToText(type); |
| |
| return span; |
| } |
| |
| function filterFromFilterId(id) { |
| switch (id) { |
| case "full-tree": |
| return (type, kind) => true; |
| case "js-funs": |
| return (type, kind) => type !== 'CODE'; |
| case "js-exclude-bc": |
| return (type, kind) => |
| type !== 'CODE' || kind !== "BytecodeHandler"; |
| } |
| } |
| |
| function createIndentNode(indent) { |
| let div = document.createElement("div"); |
| div.style.display = "inline-block"; |
| 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) { |
| let nameElement = document.createElement("span"); |
| 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"); |
| this.treeElement = $("calltree-table"); |
| this.selectAttribution = $("calltree-attribution"); |
| this.selectCategories = $("calltree-categories"); |
| this.selectSort = $("calltree-sort"); |
| |
| this.selectAttribution.onchange = () => { |
| main.setCallTreeAttribution(this.selectAttribution.value); |
| }; |
| |
| this.selectCategories.onchange = () => { |
| main.setCallTreeCategories(this.selectCategories.value); |
| }; |
| |
| this.selectSort.onchange = () => { |
| main.setCallTreeSort(this.selectSort.value); |
| }; |
| |
| this.currentState = null; |
| } |
| |
| sortFromId(id) { |
| switch (id) { |
| case "time": |
| return (c1, c2) => { |
| if (c1.ticks < c2.ticks) return 1; |
| else if (c1.ticks > c2.ticks) return -1; |
| return c2.ownTicks - c1.ownTicks; |
| }; |
| case "own-time": |
| return (c1, c2) => { |
| if (c1.ownTicks < c2.ownTicks) return 1; |
| else if (c1.ownTicks > c2.ownTicks) return -1; |
| return c2.ticks - c1.ticks; |
| }; |
| case "category-time": |
| return (c1, c2) => { |
| if (c1.type === c2.type) return c2.ticks - c1.ticks; |
| if (c1.type < c2.type) return 1; |
| return -1; |
| }; |
| case "category-own-time": |
| return (c1, c2) => { |
| if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks; |
| if (c1.type < c2.type) return 1; |
| return -1; |
| }; |
| } |
| } |
| |
| expandTree(tree, indent) { |
| let index = 0; |
| let id = "R/"; |
| let row = tree.row; |
| |
| if (row) { |
| index = row.rowIndex; |
| id = row.id; |
| |
| tree.arrow.textContent = EXPANDED_ARROW; |
| // Collapse the children when the row is clicked again. |
| let expandHandler = row.onclick; |
| row.onclick = () => { |
| this.collapseRow(tree, expandHandler); |
| } |
| } |
| |
| // Collect the children, and sort them by ticks. |
| let children = []; |
| let filter = |
| filterFromFilterId(this.currentState.callTree.attribution); |
| for (let childId in tree.children) { |
| let child = tree.children[childId]; |
| if (child.ticks > 0) { |
| children.push(child); |
| if (child.delayedExpansion) { |
| expandTreeNode(this.currentState.file, child, filter); |
| } |
| } |
| } |
| children.sort(this.sortFromId(this.currentState.callTree.sort)); |
| |
| for (let i = 0; i < children.length; i++) { |
| let node = children[i]; |
| let row = this.rows.insertRow(index); |
| row.id = id + i + "/"; |
| |
| if (node.type === "CAT") { |
| row.style.backgroundColor = CATEGORY_COLOR; |
| } else { |
| row.style.backgroundColor = bucketFromKind(node.type).backgroundColor; |
| } |
| |
| // Inclusive time % cell. |
| let c = row.insertCell(); |
| c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| // Percent-of-parent cell. |
| c = row.insertCell(); |
| c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| // Exclusive time % cell. |
| if (this.currentState.mode !== "bottom-up") { |
| c = row.insertCell(-1); |
| c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%"; |
| c.style.textAlign = "right"; |
| } |
| |
| // Create the name cell. |
| let nameCell = row.insertCell(); |
| 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(); |
| c.textContent = node.ticks; |
| c.style.textAlign = "right"; |
| if (this.currentState.mode !== "bottom-up") { |
| // Exclusive ticks cell. |
| c = row.insertCell(-1); |
| c.textContent = node.ownTicks; |
| c.style.textAlign = "right"; |
| } |
| if (node.children.length > 0) { |
| arrow.textContent = COLLAPSED_ARROW; |
| row.onclick = () => { this.expandTree(node, indent + 1); }; |
| } |
| |
| node.row = row; |
| node.arrow = arrow; |
| |
| index++; |
| } |
| } |
| |
| collapseRow(tree, expandHandler) { |
| let row = tree.row; |
| let id = row.id; |
| let index = row.rowIndex; |
| while (row.rowIndex < this.rows.rows.length && |
| this.rows.rows[index].id.startsWith(id)) { |
| this.rows.deleteRow(index); |
| } |
| |
| tree.arrow.textContent = COLLAPSED_ARROW; |
| row.onclick = expandHandler; |
| } |
| |
| fillSelects(mode, calltree) { |
| function addOptions(e, values, current) { |
| while (e.options.length > 0) { |
| e.remove(0); |
| } |
| for (let i = 0; i < values.length; i++) { |
| let option = document.createElement("option"); |
| option.value = values[i].value; |
| option.textContent = values[i].text; |
| e.appendChild(option); |
| } |
| e.value = current; |
| } |
| |
| let attributions = [ |
| { value : "js-exclude-bc", |
| text : "Attribute bytecode handlers to caller" }, |
| { value : "full-tree", |
| text : "Count each code object separately" }, |
| { value : "js-funs", |
| text : "Attribute non-functions to JS functions" } |
| ]; |
| |
| switch (mode) { |
| case "bottom-up": |
| addOptions(this.selectAttribution, attributions, calltree.attribution); |
| addOptions(this.selectCategories, [ |
| { value : "code-type", text : "Code type" }, |
| { value : "none", text : "None" } |
| ], calltree.categories); |
| addOptions(this.selectSort, [ |
| { value : "time", text : "Time (including children)" }, |
| { value : "category-time", text : "Code category, time" }, |
| ], calltree.sort); |
| return; |
| case "top-down": |
| addOptions(this.selectAttribution, attributions, calltree.attribution); |
| addOptions(this.selectCategories, [ |
| { value : "none", text : "None" }, |
| { value : "rt-entry", text : "Runtime entries" } |
| ], calltree.categories); |
| addOptions(this.selectSort, [ |
| { value : "time", text : "Time (including children)" }, |
| { value : "own-time", text : "Own time" }, |
| { value : "category-time", text : "Code category, time" }, |
| { value : "category-own-time", text : "Code category, own time"} |
| ], calltree.sort); |
| return; |
| case "function-list": |
| addOptions(this.selectAttribution, attributions, calltree.attribution); |
| addOptions(this.selectCategories, [ |
| { value : "code-type", text : "Code type" }, |
| { value : "none", text : "None" } |
| ], calltree.categories); |
| addOptions(this.selectSort, [ |
| { value : "own-time", text : "Own time" }, |
| { value : "time", text : "Time (including children)" }, |
| { value : "category-own-time", text : "Code category, own time"}, |
| { value : "category-time", text : "Code category, time" }, |
| ], calltree.sort); |
| return; |
| } |
| console.error("Unexpected mode"); |
| } |
| |
| static isCallTreeMode(mode) { |
| switch (mode) { |
| case "bottom-up": |
| case "top-down": |
| case "function-list": |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| render(newState) { |
| let oldState = this.currentState; |
| if (!newState.file || !CallTreeView.isCallTreeMode(newState.mode)) { |
| this.element.style.display = "none"; |
| this.currentState = null; |
| return; |
| } |
| |
| this.currentState = newState; |
| if (oldState) { |
| if (newState.file === oldState.file && |
| newState.start === oldState.start && |
| newState.end === oldState.end && |
| newState.mode === oldState.mode && |
| newState.callTree.attribution === oldState.callTree.attribution && |
| newState.callTree.categories === oldState.callTree.categories && |
| newState.callTree.sort === oldState.callTree.sort) { |
| // No change => just return. |
| return; |
| } |
| } |
| |
| this.element.style.display = "inherit"; |
| |
| let mode = this.currentState.mode; |
| if (!oldState || mode !== oldState.mode) { |
| // Technically, we should also call this if attribution, categories or |
| // sort change, but the selection is already highlighted by the combobox |
| // itself, so we do need to do anything here. |
| this.fillSelects(newState.mode, newState.callTree); |
| } |
| |
| let ownTimeClass = (mode === "bottom-up") ? "numeric-hidden" : "numeric"; |
| let ownTimeTh = $(this.treeElement.id + "-own-time-header"); |
| ownTimeTh.classList = ownTimeClass; |
| let ownTicksTh = $(this.treeElement.id + "-own-ticks-header"); |
| ownTicksTh.classList = ownTimeClass; |
| |
| // Build the tree. |
| let stackProcessor; |
| let filter = filterFromFilterId(this.currentState.callTree.attribution); |
| if (mode === "top-down") { |
| if (this.currentState.callTree.categories === "rt-entry") { |
| stackProcessor = |
| new RuntimeCallTreeProcessor(); |
| } else { |
| stackProcessor = |
| new PlainCallTreeProcessor(filter, false); |
| } |
| } else if (mode === "function-list") { |
| stackProcessor = new FunctionListTree( |
| filter, this.currentState.callTree.categories === "code-type"); |
| |
| } else { |
| console.assert(mode === "bottom-up"); |
| if (this.currentState.callTree.categories === "none") { |
| stackProcessor = |
| new PlainCallTreeProcessor(filter, true); |
| } else { |
| console.assert(this.currentState.callTree.categories === "code-type"); |
| stackProcessor = |
| new CategorizedCallTreeProcessor(filter, true); |
| } |
| } |
| this.tickCount = |
| generateTree(this.currentState.file, |
| this.currentState.start, |
| this.currentState.end, |
| stackProcessor); |
| // TODO(jarin) Handle the case when tick count is negative. |
| |
| this.tree = stackProcessor.tree; |
| |
| // Remove old content of the table, replace with new one. |
| let oldRows = this.treeElement.getElementsByTagName("tbody"); |
| let newRows = document.createElement("tbody"); |
| this.rows = newRows; |
| |
| // Populate the table. |
| this.expandTree(this.tree, 0); |
| |
| // Swap in the new rows. |
| this.treeElement.replaceChild(newRows, oldRows[0]); |
| } |
| } |
| |
| class TimelineView { |
| constructor() { |
| this.element = $("timeline"); |
| this.canvas = $("timeline-canvas"); |
| this.legend = $("timeline-legend"); |
| this.currentCode = $("timeline-currentCode"); |
| |
| this.canvas.onmousedown = this.onMouseDown.bind(this); |
| this.canvas.onmouseup = this.onMouseUp.bind(this); |
| this.canvas.onmousemove = this.onMouseMove.bind(this); |
| |
| this.selectionStart = null; |
| this.selectionEnd = null; |
| this.selecting = false; |
| |
| this.fontSize = 12; |
| this.imageOffset = Math.round(this.fontSize * 1.2); |
| this.functionTimelineHeight = 24; |
| this.functionTimelineTickHeight = 16; |
| |
| this.currentState = null; |
| } |
| |
| onMouseDown(e) { |
| this.selectionStart = |
| e.clientX - this.canvas.getBoundingClientRect().left; |
| this.selectionEnd = this.selectionStart + 1; |
| this.selecting = true; |
| } |
| |
| onMouseMove(e) { |
| if (this.selecting) { |
| this.selectionEnd = |
| e.clientX - this.canvas.getBoundingClientRect().left; |
| this.drawSelection(); |
| } |
| } |
| |
| onMouseUp(e) { |
| if (this.selectionStart !== null) { |
| let x = e.clientX - this.canvas.getBoundingClientRect().left; |
| if (Math.abs(x - this.selectionStart) < 10) { |
| this.selectionStart = null; |
| this.selectionEnd = null; |
| let ctx = this.canvas.getContext("2d"); |
| ctx.drawImage(this.buffer, 0, this.imageOffset); |
| } else { |
| this.selectionEnd = x; |
| this.drawSelection(); |
| } |
| let file = this.currentState.file; |
| if (file) { |
| let start = this.selectionStart === null ? 0 : this.selectionStart; |
| let end = this.selectionEnd === null ? Infinity : this.selectionEnd; |
| let firstTime = file.ticks[0].tm; |
| let lastTime = file.ticks[file.ticks.length - 1].tm; |
| |
| let width = this.buffer.width; |
| |
| start = (start / width) * (lastTime - firstTime) + firstTime; |
| end = (end / width) * (lastTime - firstTime) + firstTime; |
| |
| if (end < start) { |
| let temp = start; |
| start = end; |
| end = temp; |
| } |
| |
| main.setViewInterval(start, end); |
| } |
| } |
| this.selecting = false; |
| } |
| |
| drawSelection() { |
| let ctx = this.canvas.getContext("2d"); |
| |
| // Draw the timeline image. |
| ctx.drawImage(this.buffer, 0, this.imageOffset); |
| |
| // Draw the current interval highlight. |
| let left; |
| let right; |
| if (this.selectionStart !== null && this.selectionEnd !== null) { |
| ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; |
| left = Math.min(this.selectionStart, this.selectionEnd); |
| right = Math.max(this.selectionStart, this.selectionEnd); |
| let height = this.buffer.height - this.functionTimelineHeight; |
| ctx.fillRect(0, this.imageOffset, left, height); |
| ctx.fillRect(right, this.imageOffset, this.buffer.width - right, height); |
| } else { |
| left = 0; |
| right = this.buffer.width; |
| } |
| |
| // Draw the scale text. |
| let file = this.currentState.file; |
| ctx.fillStyle = "white"; |
| ctx.fillRect(0, 0, this.canvas.width, this.imageOffset); |
| if (file && file.ticks.length > 0) { |
| let firstTime = file.ticks[0].tm; |
| let lastTime = file.ticks[file.ticks.length - 1].tm; |
| |
| let leftTime = |
| firstTime + left / this.canvas.width * (lastTime - firstTime); |
| let rightTime = |
| firstTime + right / this.canvas.width * (lastTime - firstTime); |
| |
| let leftText = (leftTime / 1000000).toFixed(3) + "s"; |
| let rightText = (rightTime / 1000000).toFixed(3) + "s"; |
| |
| ctx.textBaseline = 'top'; |
| ctx.font = this.fontSize + "px Arial"; |
| ctx.fillStyle = "black"; |
| |
| let leftWidth = ctx.measureText(leftText).width; |
| let rightWidth = ctx.measureText(rightText).width; |
| |
| let leftStart = left - leftWidth / 2; |
| let rightStart = right - rightWidth / 2; |
| |
| if (leftStart < 0) leftStart = 0; |
| if (rightStart + rightWidth > this.canvas.width) { |
| rightStart = this.canvas.width - rightWidth; |
| } |
| if (leftStart + leftWidth > rightStart) { |
| if (leftStart > this.canvas.width - (rightStart - rightWidth)) { |
| rightStart = leftStart + leftWidth; |
| |
| } else { |
| leftStart = rightStart - leftWidth; |
| } |
| } |
| |
| ctx.fillText(leftText, leftStart, 0); |
| ctx.fillText(rightText, rightStart, 0); |
| } |
| } |
| |
| render(newState) { |
| let oldState = this.currentState; |
| |
| if (!newState.file) { |
| this.element.style.display = "none"; |
| return; |
| } |
| |
| let width = Math.round(document.documentElement.clientWidth - 20); |
| let height = Math.round(document.documentElement.clientHeight / 5); |
| |
| if (oldState) { |
| if (width === oldState.timelineSize.width && |
| height === oldState.timelineSize.height && |
| newState.file === oldState.file && |
| newState.currentCodeId === oldState.currentCodeId && |
| newState.start === oldState.start && |
| newState.end === oldState.end) { |
| // No change, nothing to do. |
| 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. |
| this.canvas.width = width; |
| this.canvas.height = height; |
| |
| // Make space for the selection text. |
| height -= this.imageOffset; |
| |
| let currentCodeId = this.currentState.currentCodeId; |
| |
| let firstTime = file.ticks[0].tm; |
| let lastTime = file.ticks[file.ticks.length - 1].tm; |
| let start = Math.max(this.currentState.start, firstTime); |
| let end = Math.min(this.currentState.end, lastTime); |
| |
| this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width; |
| this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width; |
| |
| let stackProcessor = new CategorySampler(file, bucketCount); |
| generateTree(file, 0, Infinity, stackProcessor); |
| let codeIdProcessor = new FunctionTimelineProcessor( |
| currentCodeId, |
| filterFromFilterId(this.currentState.callTree.attribution)); |
| generateTree(file, 0, Infinity, codeIdProcessor); |
| |
| let buffer = document.createElement("canvas"); |
| |
| buffer.width = width; |
| buffer.height = height; |
| |
| // Calculate the bar heights for each bucket. |
| let graphHeight = height - this.functionTimelineHeight; |
| let buckets = stackProcessor.buckets; |
| let bucketsGraph = []; |
| for (let i = 0; i < buckets.length; i++) { |
| let sum = 0; |
| let bucketData = []; |
| let total = buckets[i].total; |
| 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)); |
| } |
| } 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 - 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++) { |
| ctx.beginPath(); |
| 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(); |
| ctx.fillStyle = bucketDescriptors[j].color; |
| ctx.fill(); |
| } |
| } |
| |
| // Draw the function ticks. |
| let functionTimelineYOffset = graphHeight; |
| let functionTimelineTickHeight = this.functionTimelineTickHeight; |
| let functionTimelineHalfHeight = |
| Math.round(functionTimelineTickHeight / 2); |
| let timestampScaler = width / (lastTime - firstTime); |
| let timestampToX = (t) => Math.round((t - firstTime) * timestampScaler); |
| ctx.fillStyle = "white"; |
| ctx.fillRect( |
| 0, |
| functionTimelineYOffset, |
| buffer.width, |
| this.functionTimelineHeight); |
| for (let i = 0; i < codeIdProcessor.blocks.length; i++) { |
| let block = codeIdProcessor.blocks[i]; |
| let bucket = kindToBucketDescriptor[block.kind]; |
| ctx.fillStyle = bucket.color; |
| ctx.fillRect( |
| timestampToX(block.start), |
| functionTimelineYOffset, |
| Math.max(1, Math.round((block.end - block.start) * timestampScaler)), |
| block.topOfStack ? |
| functionTimelineTickHeight : functionTimelineHalfHeight); |
| } |
| ctx.strokeStyle = "black"; |
| ctx.lineWidth = "1"; |
| ctx.beginPath(); |
| ctx.moveTo(0, functionTimelineYOffset + 0.5); |
| ctx.lineTo(buffer.width, functionTimelineYOffset + 0.5); |
| ctx.stroke(); |
| ctx.strokeStyle = "rgba(0,0,0,0.2)"; |
| ctx.lineWidth = "1"; |
| ctx.beginPath(); |
| ctx.moveTo(0, functionTimelineYOffset + functionTimelineHalfHeight - 0.5); |
| ctx.lineTo(buffer.width, |
| functionTimelineYOffset + functionTimelineHalfHeight - 0.5); |
| ctx.stroke(); |
| |
| // Draw marks for optimizations and deoptimizations in the function |
| // timeline. |
| if (currentCodeId && currentCodeId >= 0 && |
| file.code[currentCodeId].func) { |
| let y = Math.round(functionTimelineYOffset + functionTimelineTickHeight + |
| (this.functionTimelineHeight - functionTimelineTickHeight) / 2); |
| let func = file.functions[file.code[currentCodeId].func]; |
| for (let i = 0; i < func.codes.length; i++) { |
| let code = file.code[func.codes[i]]; |
| if (code.kind === "Opt") { |
| if (code.deopt) { |
| // Draw deoptimization mark. |
| let x = timestampToX(code.deopt.tm); |
| ctx.lineWidth = 0.7; |
| ctx.strokeStyle = "red"; |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y - 3); |
| ctx.lineTo(x + 3, y + 3); |
| ctx.stroke(); |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y + 3); |
| ctx.lineTo(x + 3, y - 3); |
| ctx.stroke(); |
| } |
| // Draw optimization mark. |
| let x = timestampToX(code.tm); |
| ctx.lineWidth = 0.7; |
| ctx.strokeStyle = "blue"; |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y - 3); |
| ctx.lineTo(x, y); |
| ctx.stroke(); |
| ctx.beginPath(); |
| ctx.moveTo(x - 3, y + 3); |
| ctx.lineTo(x, y); |
| ctx.stroke(); |
| } else { |
| // Draw code creation mark. |
| let x = Math.round(timestampToX(code.tm)); |
| ctx.beginPath(); |
| ctx.fillStyle = "black"; |
| ctx.arc(x, y, 3, 0, 2 * Math.PI); |
| ctx.fill(); |
| } |
| } |
| } |
| |
| // Remember stuff for later. |
| this.buffer = buffer; |
| |
| // Draw the buffer. |
| this.drawSelection(); |
| |
| // (Re-)Populate the graph legend. |
| while (this.legend.cells.length > 0) { |
| this.legend.deleteCell(0); |
| } |
| let cell = this.legend.insertCell(-1); |
| cell.textContent = "Legend: "; |
| cell.style.padding = "1ex"; |
| for (let i = 0; i < bucketDescriptors.length; i++) { |
| let cell = this.legend.insertCell(-1); |
| cell.style.padding = "1ex"; |
| let desc = bucketDescriptors[i]; |
| let div = document.createElement("div"); |
| div.style.display = "inline-block"; |
| div.style.width = "0.6em"; |
| div.style.height = "1.2ex"; |
| div.style.backgroundColor = desc.color; |
| div.style.borderStyle = "solid"; |
| div.style.borderWidth = "1px"; |
| div.style.borderColor = "Black"; |
| cell.appendChild(div); |
| cell.appendChild(document.createTextNode(" " + desc.text)); |
| } |
| |
| removeAllChildren(this.currentCode); |
| if (currentCodeId) { |
| let currentCode = file.code[currentCodeId]; |
| this.currentCode.appendChild(document.createTextNode(currentCode.name)); |
| } else { |
| this.currentCode.appendChild(document.createTextNode("<none>")); |
| } |
| } |
| } |
| |
| class ModeBarView { |
| constructor() { |
| let modeBar = this.element = $("mode-bar"); |
| |
| function addMode(id, text, active) { |
| let div = document.createElement("div"); |
| div.classList = "mode-button" + (active ? " active-mode-button" : ""); |
| div.id = "mode-" + id; |
| div.textContent = text; |
| div.onclick = () => { |
| if (main.currentState.mode === id) return; |
| let old = $("mode-" + main.currentState.mode); |
| old.classList = "mode-button"; |
| div.classList = "mode-button active-mode-button"; |
| main.setMode(id); |
| }; |
| modeBar.appendChild(div); |
| } |
| |
| addMode("summary", "Summary", true); |
| addMode("bottom-up", "Bottom up"); |
| addMode("top-down", "Top down"); |
| addMode("function-list", "Functions"); |
| } |
| |
| render(newState) { |
| if (!newState.file) { |
| this.element.style.display = "none"; |
| return; |
| } |
| |
| this.element.style.display = "inherit"; |
| } |
| } |
| |
| class SummaryView { |
| constructor() { |
| this.element = $("summary"); |
| this.currentState = null; |
| } |
| |
| render(newState) { |
| let oldState = this.currentState; |
| |
| if (!newState.file || newState.mode !== "summary") { |
| this.element.style.display = "none"; |
| this.currentState = null; |
| return; |
| } |
| |
| this.currentState = newState; |
| if (oldState) { |
| if (newState.file === oldState.file && |
| newState.start === oldState.start && |
| newState.end === oldState.end) { |
| // No change, nothing to do. |
| return; |
| } |
| } |
| |
| this.element.style.display = "inherit"; |
| removeAllChildren(this.element); |
| |
| let stats = computeOptimizationStats( |
| this.currentState.file, newState.start, newState.end); |
| |
| let table = document.createElement("table"); |
| let rows = document.createElement("tbody"); |
| |
| function addRow(text, number, indent) { |
| let row = rows.insertRow(-1); |
| let textCell = row.insertCell(-1); |
| textCell.textContent = text; |
| let numberCell = row.insertCell(-1); |
| numberCell.textContent = number; |
| if (indent) { |
| textCell.style.textIndent = indent + "em"; |
| numberCell.style.textIndent = indent + "em"; |
| } |
| return row; |
| } |
| |
| 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); |
| } |
| arrow.textContent = COLLAPSED_ARROW; |
| row.onclick = expandHandler; |
| } |
| } |
| |
| 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); |
| childRow.id = row.id + i + "/"; |
| |
| let deopt = instances[i].deopt; |
| |
| let textCell = childRow.insertCell(-1); |
| textCell.appendChild(document.createTextNode(deopt.posText)); |
| textCell.style.textIndent = indent + "em"; |
| let reasonCell = childRow.insertCell(-1); |
| reasonCell.appendChild( |
| document.createTextNode("Reason: " + deopt.reason)); |
| reasonCell.style.textIndent = indent + "em"; |
| } |
| makeCollapsible(row, arrow); |
| } |
| |
| 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); |
| textCell.appendChild(createIndentNode(indent)); |
| let childArrow = createArrowNode(); |
| textCell.appendChild(childArrow); |
| textCell.appendChild( |
| createFunctionNode(list[i].f.name, list[i].f.codes[0])); |
| |
| let numberCell = childRow.insertCell(-1); |
| numberCell.textContent = list[i].instances.length; |
| numberCell.style.textIndent = indent + "em"; |
| |
| childArrow.textContent = COLLAPSED_ARROW; |
| childRow.onclick = () => { |
| expandDeoptInstances( |
| childRow, childArrow, list[i].instances, indent + 1); |
| }; |
| } |
| makeCollapsible(row, arrow); |
| } |
| |
| 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); |
| childRow.id = row.id + i + "/"; |
| |
| let textCell = childRow.insertCell(-1); |
| textCell.appendChild( |
| createFunctionNode(list[i].f.name, list[i].f.codes[0])); |
| textCell.style.textIndent = indent + "em"; |
| |
| let numberCell = childRow.insertCell(-1); |
| numberCell.textContent = list[i].instances.length; |
| numberCell.style.textIndent = indent + "em"; |
| } |
| 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); |
| textCell.appendChild(createIndentNode(indent)); |
| let arrow = createArrowNode(); |
| textCell.appendChild(arrow); |
| textCell.appendChild(document.createTextNode(text)); |
| |
| let numberCell = row.insertCell(-1); |
| numberCell.textContent = list.count; |
| if (indent) { |
| numberCell.style.textIndent = indent + "em"; |
| } |
| |
| if (list.count > 0) { |
| arrow.textContent = COLLAPSED_ARROW; |
| if (kind === "opt") { |
| row.onclick = () => { |
| expandOptimizedFunctionList( |
| row, arrow, list.functions, indent + 1, kind); |
| }; |
| } else { |
| row.onclick = () => { |
| expandDeoptFunctionList( |
| row, arrow, list.functions, indent + 1, kind); |
| }; |
| } |
| } |
| return row; |
| } |
| |
| addRow("Total function count:", stats.functionCount); |
| addRow("Optimized function count:", stats.optimizedFunctionCount, 1); |
| addRow("Deoptimized function count:", stats.deoptimizedFunctionCount, 2); |
| |
| addExpandableRow("Optimization count:", stats.optimizations, 0, "opt"); |
| let deoptCount = stats.eagerDeoptimizations.count + |
| stats.softDeoptimizations.count + stats.lazyDeoptimizations.count; |
| addRow("Deoptimization count:", deoptCount); |
| addExpandableRow("Eager:", stats.eagerDeoptimizations, 1, "eager"); |
| addExpandableRow("Lazy:", stats.lazyDeoptimizations, 1, "lazy"); |
| addExpandableRow("Soft:", stats.softDeoptimizations, 1, "soft"); |
| |
| table.appendChild(rows); |
| this.element.appendChild(table); |
| } |
| } |
| |
| 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"); |
| } |
| |
| render(newState) { |
| this.element.style.display = newState.file ? "none" : "inherit"; |
| } |
| } |