michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: var EIDETICKER_BASE_URL = "http://eideticker.wrla.ch/"; michael@0: michael@0: var gDebugLog = false; michael@0: var gDebugTrace = false; michael@0: var gLocation = window.location + ""; michael@0: if (gLocation.indexOf("file:") == 0) { michael@0: gDebugLog = true; michael@0: gDebugTrace = true; michael@0: PROFILERLOG("Turning on logging+tracing since cleopatra is served from the file protocol"); michael@0: } michael@0: // Use for verbose tracing, otherwise use log michael@0: function PROFILERTRACE(msg) { michael@0: if (gDebugTrace) michael@0: PROFILERLOG(msg); michael@0: } michael@0: function PROFILERLOG(msg) { michael@0: if (gDebugLog) { michael@0: msg = "Cleo: " + msg; michael@0: console.log(msg); michael@0: if (window.dump) michael@0: window.dump(msg + "\n"); michael@0: } michael@0: } michael@0: function PROFILERERROR(msg) { michael@0: msg = "Cleo: " + msg; michael@0: console.log(msg); michael@0: if (window.dump) michael@0: window.dump(msg + "\n"); michael@0: } michael@0: function enableProfilerTracing() { michael@0: gDebugLog = true; michael@0: gDebugTrace = true; michael@0: Parser.updateLogSetting(); michael@0: } michael@0: function enableProfilerLogging() { michael@0: gDebugLog = true; michael@0: Parser.updateLogSetting(); michael@0: } michael@0: michael@0: function removeAllChildren(element) { michael@0: while (element.firstChild) { michael@0: element.removeChild(element.firstChild); michael@0: } michael@0: } michael@0: michael@0: function FileList() { michael@0: this._container = document.createElement("ul"); michael@0: this._container.id = "fileList"; michael@0: this._selectedFileItem = null; michael@0: this._fileItemList = []; michael@0: } michael@0: michael@0: FileList.prototype = { michael@0: getContainer: function FileList_getContainer() { michael@0: return this._container; michael@0: }, michael@0: michael@0: clearFiles: function FileList_clearFiles() { michael@0: this.fileItemList = []; michael@0: this._selectedFileItem = null; michael@0: this._container.innerHTML = ""; michael@0: }, michael@0: michael@0: loadProfileListFromLocalStorage: function FileList_loadProfileListFromLocalStorage() { michael@0: var self = this; michael@0: gLocalStorage.getProfileList(function(profileList) { michael@0: for (var i = profileList.length - 1; i >= 0; i--) { michael@0: (function closure() { michael@0: // This only carries info about the profile and the access key to retrieve it. michael@0: var profileInfo = profileList[i]; michael@0: //PROFILERTRACE("Profile list from local storage: " + JSON.stringify(profileInfo)); michael@0: var dateObj = new Date(profileInfo.date); michael@0: var fileEntry = self.addFile(profileInfo, dateObj.toLocaleString(), function fileEntryClick() { michael@0: PROFILERLOG("open: " + profileInfo.profileKey + "\n"); michael@0: loadLocalStorageProfile(profileInfo.profileKey); michael@0: }); michael@0: })(); michael@0: } michael@0: }); michael@0: gLocalStorage.onProfileListChange(function(profileList) { michael@0: self.clearFiles(); michael@0: self.loadProfileListFromLocalStorage(); michael@0: }); michael@0: }, michael@0: michael@0: addFile: function FileList_addFile(profileInfo, description, onselect) { michael@0: var li = document.createElement("li"); michael@0: michael@0: var fileName; michael@0: if (profileInfo.profileKey && profileInfo.profileKey.indexOf("http://profile-store.commondatastorage.googleapis.com/") >= 0) { michael@0: fileName = profileInfo.profileKey.substring(54); michael@0: fileName = fileName.substring(0, 8) + "..." + fileName.substring(28); michael@0: } else { michael@0: fileName = profileInfo.name; michael@0: } michael@0: li.fileName = fileName || "(New Profile)"; michael@0: li.description = description || "(empty)"; michael@0: michael@0: li.className = "fileListItem"; michael@0: if (!this._selectedFileItem) { michael@0: li.classList.add("selected"); michael@0: this._selectedFileItem = li; michael@0: } michael@0: michael@0: var self = this; michael@0: li.onclick = function() { michael@0: self.setSelection(li); michael@0: if (onselect) michael@0: onselect(); michael@0: } michael@0: michael@0: var fileListItemTitleSpan = document.createElement("span"); michael@0: fileListItemTitleSpan.className = "fileListItemTitle"; michael@0: fileListItemTitleSpan.textContent = li.fileName; michael@0: li.appendChild(fileListItemTitleSpan); michael@0: michael@0: var fileListItemDescriptionSpan = document.createElement("span"); michael@0: fileListItemDescriptionSpan.className = "fileListItemDescription"; michael@0: fileListItemDescriptionSpan.textContent = li.description; michael@0: li.appendChild(fileListItemDescriptionSpan); michael@0: michael@0: this._container.appendChild(li); michael@0: michael@0: this._fileItemList.push(li); michael@0: michael@0: return li; michael@0: }, michael@0: michael@0: setSelection: function FileList_setSelection(fileEntry) { michael@0: if (this._selectedFileItem) { michael@0: this._selectedFileItem.classList.remove("selected"); michael@0: } michael@0: this._selectedFileItem = fileEntry; michael@0: fileEntry.classList.add("selected"); michael@0: if (this._selectedFileItem.onselect) michael@0: this._selectedFileItem.onselect(); michael@0: }, michael@0: michael@0: profileParsingFinished: function FileList_profileParsingFinished() { michael@0: //this._container.querySelector(".fileListItemTitle").textContent = "Current Profile"; michael@0: //this._container.querySelector(".fileListItemDescription").textContent = gNumSamples + " Samples"; michael@0: } michael@0: } michael@0: michael@0: function treeObjSort(a, b) { michael@0: return b.counter - a.counter; michael@0: } michael@0: michael@0: function ProfileTreeManager() { michael@0: this.treeView = new TreeView(); michael@0: this.treeView.setColumns([ michael@0: { name: "sampleCount", title: gStrings["Running Time"] }, michael@0: { name: "selfSampleCount", title: gStrings["Self"] }, michael@0: { name: "resource", title: "" }, michael@0: { name: "symbolName", title: gStrings["Symbol Name"] } michael@0: ]); michael@0: var self = this; michael@0: this.treeView.addEventListener("select", function (frameData) { michael@0: self.highlightFrame(frameData); michael@0: if (window.comparator_setSelection) { michael@0: window.comparator_setSelection(gTreeManager.serializeCurrentSelectionSnapshot(), frameData); michael@0: } michael@0: }); michael@0: this.treeView.addEventListener("contextMenuClick", function (e) { michael@0: self._onContextMenuClick(e); michael@0: }); michael@0: this.treeView.addEventListener("focusCallstackButtonClicked", function (frameData) { michael@0: // NOTE: Not in the original Cleopatra source code. michael@0: notifyParent("displaysource", { michael@0: line: frameData.scriptLocation.lineInformation, michael@0: uri: frameData.scriptLocation.scriptURI, michael@0: isChrome: /^otherhost_*/.test(frameData.library) michael@0: }); michael@0: }); michael@0: this._container = document.createElement("div"); michael@0: this._container.className = "tree"; michael@0: this._container.appendChild(this.treeView.getContainer()); michael@0: michael@0: // If this is set when the tree changes the snapshot is immediately restored. michael@0: this._savedSnapshot = null; michael@0: this._allowNonContiguous = false; michael@0: } michael@0: ProfileTreeManager.prototype = { michael@0: getContainer: function ProfileTreeManager_getContainer() { michael@0: return this._container; michael@0: }, michael@0: highlightFrame: function Treedisplay_highlightFrame(frameData) { michael@0: setHighlightedCallstack(this._getCallstackUpTo(frameData), this._getHeaviestCallstack(frameData)); michael@0: }, michael@0: dataIsOutdated: function ProfileTreeManager_dataIsOutdated() { michael@0: this.treeView.dataIsOutdated(); michael@0: }, michael@0: saveSelectionSnapshot: function ProfileTreeManager_saveSelectionSnapshot(isJavascriptOnly) { michael@0: this._savedSnapshot = this.treeView.getSelectionSnapshot(isJavascriptOnly); michael@0: }, michael@0: saveReverseSelectionSnapshot: function ProfileTreeManager_saveReverseSelectionSnapshot(isJavascriptOnly) { michael@0: this._savedSnapshot = this.treeView.getReverseSelectionSnapshot(isJavascriptOnly); michael@0: }, michael@0: hasNonTrivialSelection: function ProfileTreeManager_hasNonTrivialSelection() { michael@0: return this.treeView.getSelectionSnapshot().length > 1; michael@0: }, michael@0: serializeCurrentSelectionSnapshot: function ProfileTreeManager_serializeCurrentSelectionSnapshot() { michael@0: return JSON.stringify(this.treeView.getSelectionSnapshot()); michael@0: }, michael@0: restoreSerializedSelectionSnapshot: function ProfileTreeManager_restoreSerializedSelectionSnapshot(selection) { michael@0: this._savedSnapshot = JSON.parse(selection); michael@0: }, michael@0: _restoreSelectionSnapshot: function ProfileTreeManager__restoreSelectionSnapshot(snapshot, allowNonContiguous) { michael@0: return this.treeView.restoreSelectionSnapshot(snapshot, allowNonContiguous); michael@0: }, michael@0: setSelection: function ProfileTreeManager_setSelection(frames) { michael@0: return this.treeView.setSelection(frames); michael@0: }, michael@0: _getCallstackUpTo: function ProfileTreeManager__getCallstackUpTo(frame) { michael@0: var callstack = []; michael@0: var curr = frame; michael@0: while (curr != null) { michael@0: if (curr.name != null) { michael@0: var subCallstack = curr.fullFrameNamesAsInSample.clone(); michael@0: subCallstack.reverse(); michael@0: callstack = callstack.concat(subCallstack); michael@0: } michael@0: curr = curr.parent; michael@0: } michael@0: callstack.reverse(); michael@0: if (gInvertCallstack) michael@0: callstack.shift(); // remove (total) michael@0: return callstack; michael@0: }, michael@0: _getHeaviestCallstack: function ProfileTreeManager__getHeaviestCallstack(frame) { michael@0: // FIXME: This gets the first leaf which is not the heaviest leaf. michael@0: while(frame.children && frame.children.length > 0) { michael@0: var nextFrame = frame.children[0].getData(); michael@0: if (!nextFrame) michael@0: break; michael@0: frame = nextFrame; michael@0: } michael@0: return this._getCallstackUpTo(frame); michael@0: }, michael@0: _onContextMenuClick: function ProfileTreeManager__onContextMenuClick(e) { michael@0: var node = e.node; michael@0: var menuItem = e.menuItem; michael@0: michael@0: if (menuItem == "View Source") { michael@0: // Remove anything after ( since MXR doesn't handle search with the arguments. michael@0: var symbol = node.name.split("(")[0]; michael@0: window.open("http://mxr.mozilla.org/mozilla-central/search?string=" + symbol, "View Source"); michael@0: } else if (menuItem == "View JS Source") { michael@0: viewJSSource(node); michael@0: } else if (menuItem == "Plugin View: Pie") { michael@0: focusOnPluginView("protovis", {type:"pie"}); michael@0: } else if (menuItem == "Plugin View: Tree") { michael@0: focusOnPluginView("protovis", {type:"tree"}); michael@0: } else if (menuItem == "Google Search") { michael@0: var symbol = node.name; michael@0: window.open("https://www.google.ca/search?q=" + symbol, "View Source"); michael@0: } else if (menuItem == "Focus Frame") { michael@0: var symbol = node.fullFrameNamesAsInSample[0]; // TODO: we only function one symbol when callpath merging is on, fix that michael@0: focusOnSymbol(symbol, node.name); michael@0: } else if (menuItem == "Focus Callstack") { michael@0: var focusedCallstack = this._getCallstackUpTo(node); michael@0: focusOnCallstack(focusedCallstack, node.name); michael@0: } michael@0: }, michael@0: setAllowNonContiguous: function ProfileTreeManager_setAllowNonContiguous() { michael@0: this._allowNonContiguous = true; michael@0: }, michael@0: display: function ProfileTreeManager_display(tree, symbols, functions, resources, useFunctions, filterByName) { michael@0: this.treeView.display(this.convertToJSTreeData(tree, symbols, functions, useFunctions), resources, filterByName); michael@0: if (this._savedSnapshot) { michael@0: var old = this._savedSnapshot.clone(); michael@0: this._restoreSelectionSnapshot(this._savedSnapshot, this._allowNonContiguous); michael@0: this._savedSnapshot = old; michael@0: this._allowNonContiguous = false; michael@0: } michael@0: }, michael@0: convertToJSTreeData: function ProfileTreeManager__convertToJSTreeData(rootNode, symbols, functions, useFunctions) { michael@0: var totalSamples = rootNode.counter; michael@0: function createTreeViewNode(node, parent) { michael@0: var curObj = {}; michael@0: curObj.parent = parent; michael@0: curObj.counter = node.counter; michael@0: var selfCounter = node.counter; michael@0: for (var i = 0; i < node.children.length; ++i) { michael@0: selfCounter -= node.children[i].counter; michael@0: } michael@0: curObj.selfCounter = selfCounter; michael@0: curObj.ratio = node.counter / totalSamples; michael@0: curObj.fullFrameNamesAsInSample = node.mergedNames ? node.mergedNames : [node.name]; michael@0: if (!(node.name in (useFunctions ? functions : symbols))) { michael@0: curObj.name = node.name; michael@0: curObj.library = ""; michael@0: } else { michael@0: var functionObj = useFunctions ? functions[node.name] : functions[symbols[node.name].functionIndex]; michael@0: var info = { michael@0: functionName: functionObj.functionName, michael@0: libraryName: functionObj.libraryName, michael@0: lineInformation: useFunctions ? "" : symbols[node.name].lineInformation michael@0: }; michael@0: curObj.name = (info.functionName + " " + info.lineInformation).trim(); michael@0: curObj.library = info.libraryName; michael@0: curObj.isJSFrame = functionObj.isJSFrame; michael@0: if (functionObj.scriptLocation) { michael@0: curObj.scriptLocation = functionObj.scriptLocation; michael@0: } michael@0: } michael@0: if (node.children.length) { michael@0: curObj.children = getChildrenObjects(node.children, curObj); michael@0: } michael@0: return curObj; michael@0: } michael@0: function getChildrenObjects(children, parent) { michael@0: var sortedChildren = children.slice(0).sort(treeObjSort); michael@0: return sortedChildren.map(function (child) { michael@0: var createdNode = null; michael@0: return { michael@0: getData: function () { michael@0: if (!createdNode) { michael@0: createdNode = createTreeViewNode(child, parent); michael@0: } michael@0: return createdNode; michael@0: } michael@0: }; michael@0: }); michael@0: } michael@0: return getChildrenObjects([rootNode], null); michael@0: }, michael@0: }; michael@0: michael@0: function SampleBar() { michael@0: this._container = document.createElement("div"); michael@0: this._container.id = "sampleBar"; michael@0: this._container.className = "sideBar"; michael@0: michael@0: this._header = document.createElement("h2"); michael@0: this._header.innerHTML = "Selection - Most time spent in:"; michael@0: this._header.alt = "This shows the heaviest leaf of the selected sample. Use this to get a quick glimpse of where the selection is spending most of its time."; michael@0: this._container.appendChild(this._header); michael@0: michael@0: this._text = document.createElement("ul"); michael@0: this._text.style.whiteSpace = "pre"; michael@0: this._text.innerHTML = "Sample text"; michael@0: this._container.appendChild(this._text); michael@0: } michael@0: michael@0: SampleBar.prototype = { michael@0: getContainer: function SampleBar_getContainer() { michael@0: return this._container; michael@0: }, michael@0: setSample: function SampleBar_setSample(sample) { michael@0: var str = ""; michael@0: var list = []; michael@0: michael@0: this._text.innerHTML = ""; michael@0: michael@0: for (var i = 0; i < sample.length; i++) { michael@0: var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex]; michael@0: if (!functionObj) michael@0: continue; michael@0: var functionItem = document.createElement("li"); michael@0: var functionLink = document.createElement("a"); michael@0: functionLink.textContent = functionLink.title = functionObj.functionName; michael@0: functionLink.href = "#"; michael@0: functionItem.appendChild(functionLink); michael@0: this._text.appendChild(functionItem); michael@0: list.push(functionObj.functionName); michael@0: functionLink.selectIndex = i; michael@0: functionLink.onclick = function() { michael@0: var selectedFrames = []; michael@0: if (gInvertCallstack) { michael@0: for (var i = 0; i <= this.selectIndex; i++) { michael@0: var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex]; michael@0: selectedFrames.push(functionObj.functionName); michael@0: } michael@0: } else { michael@0: for (var i = sample.length - 1; i >= this.selectIndex; i--) { michael@0: var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex]; michael@0: selectedFrames.push(functionObj.functionName); michael@0: } michael@0: } michael@0: gTreeManager.setSelection(selectedFrames); michael@0: return false; michael@0: } michael@0: } michael@0: return list; michael@0: }, michael@0: } michael@0: michael@0: michael@0: function PluginView() { michael@0: this._container = document.createElement("div"); michael@0: this._container.className = "pluginview"; michael@0: this._container.style.visibility = 'hidden'; michael@0: this._iframe = document.createElement("iframe"); michael@0: this._iframe.className = "pluginviewIFrame"; michael@0: this._container.appendChild(this._iframe); michael@0: this._container.style.top = ""; michael@0: } michael@0: PluginView.prototype = { michael@0: getContainer: function PluginView_getContainer() { michael@0: return this._container; michael@0: }, michael@0: hide: function() { michael@0: // get rid of the scrollbars michael@0: this._container.style.top = ""; michael@0: this._container.style.visibility = 'hidden'; michael@0: }, michael@0: show: function() { michael@0: // This creates extra scrollbar so only do it when needed michael@0: this._container.style.top = "0px"; michael@0: this._container.style.visibility = ''; michael@0: }, michael@0: display: function(pluginName, param, data) { michael@0: this._iframe.src = "js/plugins/" + pluginName + "/index.html"; michael@0: var self = this; michael@0: this._iframe.onload = function() { michael@0: self._iframe.contentWindow.initCleopatraPlugin(data, param, gSymbols); michael@0: } michael@0: this.show(); michael@0: }, michael@0: } michael@0: michael@0: function HistogramView() { michael@0: this._container = document.createElement("div"); michael@0: this._container.className = "histogram"; michael@0: michael@0: this._canvas = this._createCanvas(); michael@0: this._container.appendChild(this._canvas); michael@0: michael@0: this._rangeSelector = new RangeSelector(this._canvas, this); michael@0: this._rangeSelector.enableRangeSelectionOnHistogram(); michael@0: this._container.appendChild(this._rangeSelector.getContainer()); michael@0: michael@0: this._busyCover = document.createElement("div"); michael@0: this._busyCover.className = "busyCover"; michael@0: this._container.appendChild(this._busyCover); michael@0: michael@0: this._histogramData = []; michael@0: } michael@0: HistogramView.prototype = { michael@0: dataIsOutdated: function HistogramView_dataIsOutdated() { michael@0: this._busyCover.classList.add("busy"); michael@0: }, michael@0: _createCanvas: function HistogramView__createCanvas() { michael@0: var canvas = document.createElement("canvas"); michael@0: canvas.height = 60; michael@0: canvas.style.width = "100%"; michael@0: canvas.style.height = "100%"; michael@0: return canvas; michael@0: }, michael@0: getContainer: function HistogramView_getContainer() { michael@0: return this._container; michael@0: }, michael@0: selectRange: function HistogramView_selectRange(start, end) { michael@0: this._rangeSelector._finishSelection(start, end); michael@0: }, michael@0: showVideoFramePosition: function HistogramView_showVideoFramePosition(frame) { michael@0: if (!this._frameStart || !this._frameStart[frame]) michael@0: return; michael@0: var frameStart = this._frameStart[frame]; michael@0: // Now we look for the frame end. Because we can swap frame we don't present we have to look ahead michael@0: // in the stream if frame+1 doesn't exist. michael@0: var frameEnd = this._frameStart[frame+1]; michael@0: for (var i = 0; i < 10 && !frameEnd; i++) { michael@0: frameEnd = this._frameStart[frame+1+i]; michael@0: } michael@0: this._rangeSelector.showVideoRange(frameStart, frameEnd); michael@0: }, michael@0: showVideoPosition: function HistogramView_showVideoPosition(position) { michael@0: // position in 0..1 michael@0: this._rangeSelector.showVideoPosition(position); michael@0: }, michael@0: _gatherMarkersList: function HistogramView__gatherMarkersList(histogramData) { michael@0: var markers = []; michael@0: for (var i = 0; i < histogramData.length; ++i) { michael@0: var step = histogramData[i]; michael@0: if ("marker" in step) { michael@0: markers.push({ michael@0: index: i, michael@0: name: step.marker michael@0: }); michael@0: } michael@0: } michael@0: return markers; michael@0: }, michael@0: _calculateWidthMultiplier: function () { michael@0: var minWidth = 2000; michael@0: return Math.ceil(minWidth / this._widthSum); michael@0: }, michael@0: histogramClick: function HistogramView_histogramClick(index) { michael@0: var sample = this._histogramData[index]; michael@0: var frames = sample.frames; michael@0: var list = gSampleBar.setSample(frames[0]); michael@0: gTreeManager.setSelection(list); michael@0: setHighlightedCallstack(frames[0], frames[0]); michael@0: }, michael@0: display: function HistogramView_display(histogramData, frameStart, widthSum, highlightedCallstack) { michael@0: this._histogramData = histogramData; michael@0: this._frameStart = frameStart; michael@0: this._widthSum = widthSum; michael@0: this._widthMultiplier = this._calculateWidthMultiplier(); michael@0: this._canvas.width = this._widthMultiplier * this._widthSum; michael@0: this._render(highlightedCallstack); michael@0: this._busyCover.classList.remove("busy"); michael@0: }, michael@0: _scheduleRender: function HistogramView__scheduleRender(highlightedCallstack) { michael@0: var self = this; michael@0: if (self._pendingAnimationFrame != null) { michael@0: return; michael@0: } michael@0: self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() { michael@0: cancelAnimationFrame(self._pendingAnimationFrame); michael@0: self._pendingAnimationFrame = null; michael@0: self._render(highlightedCallstack); michael@0: self._busyCover.classList.remove("busy"); michael@0: }); michael@0: }, michael@0: _render: function HistogramView__render(highlightedCallstack) { michael@0: var ctx = this._canvas.getContext("2d"); michael@0: var height = this._canvas.height; michael@0: ctx.setTransform(this._widthMultiplier, 0, 0, 1, 0, 0); michael@0: ctx.font = "20px Georgia"; michael@0: ctx.clearRect(0, 0, this._widthSum, height); michael@0: michael@0: var self = this; michael@0: var markerCount = 0; michael@0: for (var i = 0; i < this._histogramData.length; i++) { michael@0: var step = this._histogramData[i]; michael@0: var isSelected = self._isStepSelected(step, highlightedCallstack); michael@0: var isInRangeSelector = self._isInRangeSelector(i); michael@0: if (isSelected) { michael@0: ctx.fillStyle = "green"; michael@0: } else if (isInRangeSelector) { michael@0: ctx.fillStyle = "blue"; michael@0: } else { michael@0: ctx.fillStyle = step.color; michael@0: } michael@0: var roundedHeight = Math.round(step.value * height); michael@0: ctx.fillRect(step.x, height - roundedHeight, step.width, roundedHeight); michael@0: } michael@0: michael@0: this._finishedRendering = true; michael@0: }, michael@0: highlightedCallstackChanged: function HistogramView_highlightedCallstackChanged(highlightedCallstack) { michael@0: this._scheduleRender(highlightedCallstack); michael@0: }, michael@0: _isInRangeSelector: function HistogramView_isInRangeSelector(index) { michael@0: return false; michael@0: }, michael@0: _isStepSelected: function HistogramView__isStepSelected(step, highlightedCallstack) { michael@0: if ("marker" in step) michael@0: return false; michael@0: michael@0: search_frames: for (var i = 0; i < step.frames.length; i++) { michael@0: var frames = step.frames[i]; michael@0: michael@0: if (frames.length < highlightedCallstack.length || michael@0: highlightedCallstack.length <= (gInvertCallstack ? 0 : 1)) michael@0: continue; michael@0: michael@0: var compareFrames = frames; michael@0: if (gInvertCallstack) { michael@0: for (var j = 0; j < highlightedCallstack.length; j++) { michael@0: var compareFrameIndex = compareFrames.length - 1 - j; michael@0: if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) { michael@0: continue search_frames; michael@0: } michael@0: } michael@0: } else { michael@0: for (var j = 0; j < highlightedCallstack.length; j++) { michael@0: var compareFrameIndex = j; michael@0: if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) { michael@0: continue search_frames; michael@0: } michael@0: } michael@0: } michael@0: return true; michael@0: }; michael@0: return false; michael@0: }, michael@0: getHistogramData: function HistogramView__getHistogramData() { michael@0: return this._histogramData; michael@0: }, michael@0: _getStepColor: function HistogramView__getStepColor(step) { michael@0: if ("responsiveness" in step.extraInfo) { michael@0: var res = step.extraInfo.responsiveness; michael@0: var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness)); michael@0: return "rgb(" + redComponent + ",0,0)"; michael@0: } michael@0: michael@0: return "rgb(0,0,0)"; michael@0: }, michael@0: }; michael@0: michael@0: function RangeSelector(graph, histogram) { michael@0: this._histogram = histogram; michael@0: this.container = document.createElement("div"); michael@0: this.container.className = "rangeSelectorContainer"; michael@0: this._graph = graph; michael@0: this._selectedRange = { startX: 0, endX: 0 }; michael@0: this._selectedSampleRange = { start: 0, end: 0 }; michael@0: michael@0: this._highlighter = document.createElement("div"); michael@0: this._highlighter.className = "histogramHilite collapsed"; michael@0: this.container.appendChild(this._highlighter); michael@0: michael@0: this._mouseMarker = document.createElement("div"); michael@0: this._mouseMarker.className = "histogramMouseMarker"; michael@0: this.container.appendChild(this._mouseMarker); michael@0: } michael@0: RangeSelector.prototype = { michael@0: getContainer: function RangeSelector_getContainer() { michael@0: return this.container; michael@0: }, michael@0: // echo the location off the mouse on the histogram michael@0: drawMouseMarker: function RangeSelector_drawMouseMarker(x) { michael@0: var mouseMarker = this._mouseMarker; michael@0: mouseMarker.style.left = x + "px"; michael@0: }, michael@0: showVideoPosition: function RangeSelector_showVideoPosition(position) { michael@0: this.drawMouseMarker(position * (this._graph.parentNode.clientWidth-1)); michael@0: PROFILERLOG("Show video position: " + position); michael@0: }, michael@0: drawHiliteRectangle: function RangeSelector_drawHiliteRectangle(x, y, width, height) { michael@0: var hilite = this._highlighter; michael@0: hilite.style.left = x + "px"; michael@0: hilite.style.top = "0"; michael@0: hilite.style.width = width + "px"; michael@0: hilite.style.height = height + "px"; michael@0: }, michael@0: clearCurrentRangeSelection: function RangeSelector_clearCurrentRangeSelection() { michael@0: try { michael@0: this.changeEventSuppressed = true; michael@0: var children = this.selector.childNodes; michael@0: for (var i = 0; i < children.length; ++i) { michael@0: children[i].selected = false; michael@0: } michael@0: } finally { michael@0: this.changeEventSuppressed = false; michael@0: } michael@0: }, michael@0: showVideoRange: function RangeSelector_showVideoRange(startIndex, endIndex) { michael@0: if (!endIndex || endIndex < 0) michael@0: endIndex = gCurrentlyShownSampleData.length; michael@0: michael@0: var len = this._graph.parentNode.getBoundingClientRect().right - this._graph.parentNode.getBoundingClientRect().left; michael@0: this._selectedRange.startX = startIndex * len / this._histogram._histogramData.length; michael@0: this._selectedRange.endX = endIndex * len / this._histogram._histogramData.length; michael@0: var width = this._selectedRange.endX - this._selectedRange.startX; michael@0: var height = this._graph.parentNode.clientHeight; michael@0: this._highlighter.classList.remove("collapsed"); michael@0: this.drawHiliteRectangle(this._selectedRange.startX, 0, width, height); michael@0: //this._finishSelection(startIndex, endIndex); michael@0: }, michael@0: enableRangeSelectionOnHistogram: function RangeSelector_enableRangeSelectionOnHistogram() { michael@0: var graph = this._graph; michael@0: var isDrawingRectangle = false; michael@0: var origX, origY; michael@0: var self = this; michael@0: // Compute this on the mouse down rather then forcing a sync reflow michael@0: // every frame. michael@0: var boundingRect = null; michael@0: function histogramClick(clickX, clickY) { michael@0: clickX = Math.min(clickX, graph.parentNode.getBoundingClientRect().right); michael@0: clickX = clickX - graph.parentNode.getBoundingClientRect().left; michael@0: var index = self._histogramIndexFromPoint(clickX); michael@0: self._histogram.histogramClick(index); michael@0: } michael@0: function updateHiliteRectangle(newX, newY) { michael@0: newX = Math.min(newX, boundingRect.right); michael@0: var startX = Math.min(newX, origX) - boundingRect.left; michael@0: var startY = 0; michael@0: var width = Math.abs(newX - origX); michael@0: var height = graph.parentNode.clientHeight; michael@0: if (startX < 0) { michael@0: width += startX; michael@0: startX = 0; michael@0: } michael@0: self._selectedRange.startX = startX; michael@0: self._selectedRange.endX = startX + width; michael@0: self.drawHiliteRectangle(startX, startY, width, height); michael@0: } michael@0: function updateMouseMarker(newX) { michael@0: self.drawMouseMarker(newX - graph.parentNode.getBoundingClientRect().left); michael@0: } michael@0: graph.addEventListener("mousedown", function(e) { michael@0: if (e.button != 0) michael@0: return; michael@0: graph.style.cursor = "col-resize"; michael@0: isDrawingRectangle = true; michael@0: self.beginHistogramSelection(); michael@0: origX = e.pageX; michael@0: origY = e.pageY; michael@0: boundingRect = graph.parentNode.getBoundingClientRect(); michael@0: if (this.setCapture) michael@0: this.setCapture(); michael@0: // Reset the highlight rectangle michael@0: updateHiliteRectangle(e.pageX, e.pageY); michael@0: e.preventDefault(); michael@0: this._movedDuringClick = false; michael@0: }, false); michael@0: graph.addEventListener("mouseup", function(e) { michael@0: graph.style.cursor = "default"; michael@0: if (!this._movedDuringClick) { michael@0: isDrawingRectangle = false; michael@0: // Handle as a click on the histogram. Select the sample: michael@0: histogramClick(e.pageX, e.pageY); michael@0: } else if (isDrawingRectangle) { michael@0: isDrawingRectangle = false; michael@0: updateHiliteRectangle(e.pageX, e.pageY); michael@0: self.finishHistogramSelection(e.pageX != origX); michael@0: if (e.pageX == origX) { michael@0: // Simple click in the histogram michael@0: var index = self._sampleIndexFromPoint(e.pageX - graph.parentNode.getBoundingClientRect().left); michael@0: // TODO Select this sample in the tree view michael@0: var sample = gCurrentlyShownSampleData[index]; michael@0: } michael@0: } michael@0: }, false); michael@0: graph.addEventListener("mousemove", function(e) { michael@0: this._movedDuringClick = true; michael@0: if (isDrawingRectangle) { michael@0: updateMouseMarker(-1); // Clear michael@0: updateHiliteRectangle(e.pageX, e.pageY); michael@0: } else { michael@0: updateMouseMarker(e.pageX); michael@0: } michael@0: }, false); michael@0: graph.addEventListener("mouseout", function(e) { michael@0: updateMouseMarker(-1); // Clear michael@0: }, false); michael@0: }, michael@0: beginHistogramSelection: function RangeSelector_beginHistgramSelection() { michael@0: var hilite = this._highlighter; michael@0: hilite.classList.remove("finished"); michael@0: hilite.classList.add("selecting"); michael@0: hilite.classList.remove("collapsed"); michael@0: if (this._transientRestrictionEnteringAffordance) { michael@0: this._transientRestrictionEnteringAffordance.discard(); michael@0: } michael@0: }, michael@0: _finishSelection: function RangeSelector__finishSelection(start, end) { michael@0: var newFilterChain = gSampleFilters.concat({ type: "RangeSampleFilter", start: start, end: end }); michael@0: var self = this; michael@0: self._transientRestrictionEnteringAffordance = gBreadcrumbTrail.add({ michael@0: title: gStrings["Sample Range"] + " [" + start + ", " + (end + 1) + "]", michael@0: enterCallback: function () { michael@0: gSampleFilters = newFilterChain; michael@0: self.collapseHistogramSelection(); michael@0: filtersChanged(); michael@0: } michael@0: }); michael@0: }, michael@0: finishHistogramSelection: function RangeSelector_finishHistgramSelection(isSomethingSelected) { michael@0: var self = this; michael@0: var hilite = this._highlighter; michael@0: hilite.classList.remove("selecting"); michael@0: if (isSomethingSelected) { michael@0: hilite.classList.add("finished"); michael@0: var start = this._sampleIndexFromPoint(this._selectedRange.startX); michael@0: var end = this._sampleIndexFromPoint(this._selectedRange.endX); michael@0: self._finishSelection(start, end); michael@0: } else { michael@0: hilite.classList.add("collapsed"); michael@0: } michael@0: }, michael@0: collapseHistogramSelection: function RangeSelector_collapseHistogramSelection() { michael@0: var hilite = this._highlighter; michael@0: hilite.classList.add("collapsed"); michael@0: }, michael@0: _sampleIndexFromPoint: function RangeSelector__sampleIndexFromPoint(x) { michael@0: // XXX this is completely wrong, fix please michael@0: var totalSamples = parseFloat(gCurrentlyShownSampleData.length); michael@0: var width = parseFloat(this._graph.parentNode.clientWidth); michael@0: var factor = totalSamples / width; michael@0: return parseInt(parseFloat(x) * factor); michael@0: }, michael@0: _histogramIndexFromPoint: function RangeSelector__histogramIndexFromPoint(x) { michael@0: // XXX this is completely wrong, fix please michael@0: var totalSamples = parseFloat(this._histogram._histogramData.length); michael@0: var width = parseFloat(this._graph.parentNode.clientWidth); michael@0: var factor = totalSamples / width; michael@0: return parseInt(parseFloat(x) * factor); michael@0: }, michael@0: }; michael@0: michael@0: function videoPaneTimeChange(video) { michael@0: if (!gMeta || !gMeta.frameStart) michael@0: return; michael@0: michael@0: var frame = gVideoPane.getCurrentFrameNumber(); michael@0: //var frameStart = gMeta.frameStart[frame]; michael@0: //var frameEnd = gMeta.frameStart[frame+1]; // If we don't have a frameEnd assume the end of the profile michael@0: michael@0: gHistogramView.showVideoFramePosition(frame); michael@0: } michael@0: michael@0: michael@0: window.onpopstate = function(ev) { michael@0: return; // Conflicts with document url michael@0: if (!gBreadcrumbTrail) michael@0: return; michael@0: michael@0: gBreadcrumbTrail.pop(); michael@0: if (ev.state) { michael@0: if (ev.state.action === "popbreadcrumb") { michael@0: //gBreadcrumbTrail.pop(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function BreadcrumbTrail() { michael@0: this._breadcrumbs = []; michael@0: this._selectedBreadcrumbIndex = -1; michael@0: michael@0: this._containerElement = document.createElement("div"); michael@0: this._containerElement.className = "breadcrumbTrail"; michael@0: var self = this; michael@0: this._containerElement.addEventListener("click", function (e) { michael@0: if (!e.target.classList.contains("breadcrumbTrailItem")) michael@0: return; michael@0: self._enter(e.target.breadcrumbIndex); michael@0: }); michael@0: } michael@0: BreadcrumbTrail.prototype = { michael@0: getContainer: function BreadcrumbTrail_getContainer() { michael@0: return this._containerElement; michael@0: }, michael@0: /** michael@0: * Add a breadcrumb. The breadcrumb parameter is an object with the following michael@0: * properties: michael@0: * - title: The text that will be shown in the breadcrumb's button. michael@0: * - enterCallback: A function that will be called when entering this michael@0: * breadcrumb. michael@0: */ michael@0: add: function BreadcrumbTrail_add(breadcrumb) { michael@0: for (var i = this._breadcrumbs.length - 1; i > this._selectedBreadcrumbIndex; i--) { michael@0: var rearLi = this._breadcrumbs[i]; michael@0: if (!rearLi.breadcrumbIsTransient) michael@0: throw "Can only add new breadcrumbs if after the current one there are only transient ones."; michael@0: rearLi.breadcrumbDiscarder.discard(); michael@0: } michael@0: var div = document.createElement("div"); michael@0: div.className = "breadcrumbTrailItem"; michael@0: div.textContent = breadcrumb.title; michael@0: var index = this._breadcrumbs.length; michael@0: div.breadcrumbIndex = index; michael@0: div.breadcrumbEnterCallback = breadcrumb.enterCallback; michael@0: div.breadcrumbIsTransient = true; michael@0: div.style.zIndex = 1000 - index; michael@0: this._containerElement.appendChild(div); michael@0: this._breadcrumbs.push(div); michael@0: if (index == 0) michael@0: this._enter(index); michael@0: var self = this; michael@0: div.breadcrumbDiscarder = { michael@0: discard: function () { michael@0: if (div.breadcrumbIsTransient) { michael@0: self._deleteBeyond(index - 1); michael@0: delete div.breadcrumbIsTransient; michael@0: delete div.breadcrumbDiscarder; michael@0: } michael@0: } michael@0: }; michael@0: return div.breadcrumbDiscarder; michael@0: }, michael@0: addAndEnter: function BreadcrumbTrail_addAndEnter(breadcrumb) { michael@0: var removalHandle = this.add(breadcrumb); michael@0: this._enter(this._breadcrumbs.length - 1); michael@0: }, michael@0: pop : function BreadcrumbTrail_pop() { michael@0: if (this._breadcrumbs.length-2 >= 0) michael@0: this._enter(this._breadcrumbs.length-2); michael@0: }, michael@0: enterLastItem: function BreadcrumbTrail_enterLastItem(forceSelection) { michael@0: this._enter(this._breadcrumbs.length-1, forceSelection); michael@0: }, michael@0: _enter: function BreadcrumbTrail__select(index, forceSelection) { michael@0: if (index == this._selectedBreadcrumbIndex) michael@0: return; michael@0: if (forceSelection) { michael@0: gTreeManager.restoreSerializedSelectionSnapshot(forceSelection); michael@0: } else { michael@0: gTreeManager.saveSelectionSnapshot(); michael@0: } michael@0: var prevSelected = this._breadcrumbs[this._selectedBreadcrumbIndex]; michael@0: if (prevSelected) michael@0: prevSelected.classList.remove("selected"); michael@0: var li = this._breadcrumbs[index]; michael@0: if (this === gBreadcrumbTrail && index != 0) { michael@0: // Support for back button, disabled until the forward button is implemented. michael@0: //var state = {action: "popbreadcrumb",}; michael@0: //window.history.pushState(state, "Cleopatra"); michael@0: } michael@0: michael@0: delete li.breadcrumbIsTransient; michael@0: li.classList.add("selected"); michael@0: this._deleteBeyond(index); michael@0: this._selectedBreadcrumbIndex = index; michael@0: li.breadcrumbEnterCallback(); michael@0: // Add history state michael@0: }, michael@0: _deleteBeyond: function BreadcrumbTrail__deleteBeyond(index) { michael@0: while (this._breadcrumbs.length > index + 1) { michael@0: this._hide(this._breadcrumbs[index + 1]); michael@0: this._breadcrumbs.splice(index + 1, 1); michael@0: } michael@0: }, michael@0: _hide: function BreadcrumbTrail__hide(breadcrumb) { michael@0: delete breadcrumb.breadcrumbIsTransient; michael@0: breadcrumb.classList.add("deleted"); michael@0: setTimeout(function () { michael@0: breadcrumb.parentNode.removeChild(breadcrumb); michael@0: }, 1000); michael@0: }, michael@0: }; michael@0: michael@0: function maxResponsiveness() { michael@0: var data = gCurrentlyShownSampleData; michael@0: var maxRes = 0.0; michael@0: for (var i = 0; i < data.length; ++i) { michael@0: if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"]) michael@0: continue; michael@0: if (maxRes < data[i].extraInfo["responsiveness"]) michael@0: maxRes = data[i].extraInfo["responsiveness"]; michael@0: } michael@0: return maxRes; michael@0: } michael@0: michael@0: function effectiveInterval() { michael@0: var data = gCurrentlyShownSampleData; michael@0: var interval = 0.0; michael@0: var sampleCount = 0; michael@0: var timeCount = 0; michael@0: var lastTime = null; michael@0: for (var i = 0; i < data.length; ++i) { michael@0: if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) { michael@0: lastTime = null; michael@0: continue; michael@0: } michael@0: if (lastTime) { michael@0: sampleCount++; michael@0: timeCount += data[i].extraInfo["time"] - lastTime; michael@0: } michael@0: lastTime = data[i].extraInfo["time"]; michael@0: } michael@0: var effectiveInterval = timeCount/sampleCount; michael@0: // Biggest diff michael@0: var biggestDiff = 0; michael@0: lastTime = null; michael@0: for (var i = 0; i < data.length; ++i) { michael@0: if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) { michael@0: lastTime = null; michael@0: continue; michael@0: } michael@0: if (lastTime) { michael@0: if (biggestDiff < Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime))) michael@0: biggestDiff = Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime)); michael@0: } michael@0: lastTime = data[i].extraInfo["time"]; michael@0: } michael@0: michael@0: if (effectiveInterval != effectiveInterval) michael@0: return "Time info not collected"; michael@0: michael@0: return (effectiveInterval).toFixed(2) + " ms ±" + biggestDiff.toFixed(2); michael@0: } michael@0: michael@0: function numberOfCurrentlyShownSamples() { michael@0: var data = gCurrentlyShownSampleData; michael@0: var num = 0; michael@0: for (var i = 0; i < data.length; ++i) { michael@0: if (data[i]) michael@0: num++; michael@0: } michael@0: return num; michael@0: } michael@0: michael@0: function avgResponsiveness() { michael@0: var data = gCurrentlyShownSampleData; michael@0: var totalRes = 0.0; michael@0: for (var i = 0; i < data.length; ++i) { michael@0: if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"]) michael@0: continue; michael@0: totalRes += data[i].extraInfo["responsiveness"]; michael@0: } michael@0: return totalRes / numberOfCurrentlyShownSamples(); michael@0: } michael@0: michael@0: function copyProfile() { michael@0: window.prompt ("Copy to clipboard: Ctrl+C, Enter", document.getElementById("data").value); michael@0: } michael@0: michael@0: function saveProfileToLocalStorage() { michael@0: Parser.getSerializedProfile(true, function (serializedProfile) { michael@0: gLocalStorage.storeLocalProfile(serializedProfile, gMeta.profileId, function profileSaved() { michael@0: michael@0: }); michael@0: }); michael@0: } michael@0: function downloadProfile() { michael@0: Parser.getSerializedProfile(true, function (serializedProfile) { michael@0: var blob = new Blob([serializedProfile], { "type": "application/octet-stream" }); michael@0: location.href = window.URL.createObjectURL(blob); michael@0: }); michael@0: } michael@0: michael@0: function promptUploadProfile(selected) { michael@0: var overlay = document.createElement("div"); michael@0: overlay.style.position = "absolute"; michael@0: overlay.style.top = 0; michael@0: overlay.style.left = 0; michael@0: overlay.style.width = "100%"; michael@0: overlay.style.height = "100%"; michael@0: overlay.style.backgroundColor = "transparent"; michael@0: michael@0: var bg = document.createElement("div"); michael@0: bg.style.position = "absolute"; michael@0: bg.style.top = 0; michael@0: bg.style.left = 0; michael@0: bg.style.width = "100%"; michael@0: bg.style.height = "100%"; michael@0: bg.style.opacity = "0.6"; michael@0: bg.style.backgroundColor = "#aaaaaa"; michael@0: overlay.appendChild(bg); michael@0: michael@0: var contentDiv = document.createElement("div"); michael@0: contentDiv.className = "sideBar"; michael@0: contentDiv.style.position = "absolute"; michael@0: contentDiv.style.top = "50%"; michael@0: contentDiv.style.left = "50%"; michael@0: contentDiv.style.width = "40em"; michael@0: contentDiv.style.height = "20em"; michael@0: contentDiv.style.marginLeft = "-20em"; michael@0: contentDiv.style.marginTop = "-10em"; michael@0: contentDiv.style.padding = "10px"; michael@0: contentDiv.style.border = "2px solid black"; michael@0: contentDiv.style.backgroundColor = "rgb(219, 223, 231)"; michael@0: overlay.appendChild(contentDiv); michael@0: michael@0: var noticeHTML = ""; michael@0: noticeHTML += "