mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
832 lines
28 KiB
JavaScript
832 lines
28 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
/**
|
|
* Functions handling the profile inspection UI, showing the framerate and
|
|
* cateogry graphs, along with a call tree view.
|
|
*
|
|
* A profile view is a tabbed browser, so recording data will be displayed in
|
|
* tabs. Certain messages like 'Loading' or 'Recording...' may also be shown.
|
|
*/
|
|
let ProfileView = {
|
|
/**
|
|
* Initialization function, called when the tool is started.
|
|
*/
|
|
initialize: function() {
|
|
this._tabs = $("#profile-content tabs");
|
|
this._panels = $("#profile-content tabpanels");
|
|
this._tabTemplate = $("#profile-content-tab-template");
|
|
this._panelTemplate = $("#profile-content-tabpanel-template");
|
|
this._newtabButton = $("#profile-newtab-button");
|
|
this._invertTree = $("#invert-tree");
|
|
|
|
this._recordingInfoByPanel = new WeakMap();
|
|
this._framerateGraphByPanel = new Map();
|
|
this._categoriesGraphByPanel = new Map();
|
|
this._callTreeRootByPanel = new Map();
|
|
|
|
this._onTabSelect = this._onTabSelect.bind(this);
|
|
this._onNewTabClick = this._onNewTabClick.bind(this);
|
|
this._onInvertTree = this._onInvertTree.bind(this);
|
|
this._onGraphLegendSelection = this._onGraphLegendSelection.bind(this);
|
|
this._onGraphMouseUp = this._onGraphMouseUp.bind(this);
|
|
this._onGraphScroll = this._onGraphScroll.bind(this);
|
|
this._onCallViewFocus = this._onCallViewFocus.bind(this);
|
|
this._onCallViewLink = this._onCallViewLink.bind(this);
|
|
this._onCallViewZoom = this._onCallViewZoom.bind(this);
|
|
|
|
this._panels.addEventListener("select", this._onTabSelect, false);
|
|
this._newtabButton.addEventListener("click", this._onNewTabClick, false);
|
|
this._invertTree.addEventListener("command", this._onInvertTree, false);
|
|
},
|
|
|
|
/**
|
|
* Destruction function, called when the tool is closed.
|
|
*/
|
|
destroy: function() {
|
|
this.removeAllTabs();
|
|
|
|
this._panels.removeEventListener("select", this._onTabSelect, false);
|
|
this._newtabButton.removeEventListener("click", this._onNewTabClick, false);
|
|
this._invertTree.removeEventListener("command", this._onInvertTree, false);
|
|
},
|
|
|
|
/**
|
|
* Shows a message detailing that there are is no data available.
|
|
* The tabbed browser will also be hidden.
|
|
*/
|
|
showEmptyNotice: function() {
|
|
$("#profile-pane").selectedPanel = $("#empty-notice");
|
|
window.emit(EVENTS.EMPTY_NOTICE_SHOWN);
|
|
},
|
|
|
|
/**
|
|
* Shows a message detailing that a recording is currently in progress.
|
|
* The tabbed browser will also be hidden.
|
|
*/
|
|
showRecordingNotice: function() {
|
|
$("#profile-pane").selectedPanel = $("#recording-notice");
|
|
window.emit(EVENTS.RECORDING_NOTICE_SHOWN);
|
|
},
|
|
|
|
/**
|
|
* Shows a message detailing that a finished recording is being loaded.
|
|
* The tabbed browser will also be hidden.
|
|
*/
|
|
showLoadingNotice: function() {
|
|
$("#profile-pane").selectedPanel = $("#loading-notice");
|
|
window.emit(EVENTS.LOADING_NOTICE_SHOWN);
|
|
},
|
|
|
|
/**
|
|
* Shows the tabbed browser displaying recording data.
|
|
*/
|
|
showTabbedBrowser: function() {
|
|
$("#profile-pane").selectedPanel = $("#profile-content");
|
|
window.emit(EVENTS.TABBED_BROWSER_SHOWN);
|
|
},
|
|
|
|
/**
|
|
* Selects the tab at the specified index in this tabbed browser.
|
|
*
|
|
* @param number tabIndex
|
|
* The index of the tab to select. If no tab is available at the
|
|
* specified index, all tabs will be deselected.
|
|
*/
|
|
selectTab: function(tabIndex) {
|
|
$("#profile-content").selectedIndex = tabIndex;
|
|
},
|
|
|
|
/**
|
|
* Adds an empty tab in this tabbed browser.
|
|
*
|
|
* @return number
|
|
* The newly created tab's index.
|
|
*/
|
|
addTab: function() {
|
|
let tab = this._tabs.appendChild(this._tabTemplate.cloneNode(true));
|
|
let panel = this._panels.appendChild(this._panelTemplate.cloneNode(true));
|
|
|
|
// "Uncover" the tab via a CSS animation.
|
|
tab.removeAttribute("covered");
|
|
|
|
let tabIndex = this._tabs.itemCount - 1;
|
|
return tabIndex;
|
|
},
|
|
|
|
/**
|
|
* Sets the title of a tab in this tabbed browser.
|
|
*
|
|
* @param number tabIndex
|
|
* The index of the tab to name.
|
|
* @param number beginAt, endAt
|
|
* The 'start → stop' components of the tab title.
|
|
*/
|
|
nameTab: function(tabIndex, beginAt, endAt) {
|
|
let tab = this._getTab(tabIndex);
|
|
let a = L10N.numberWithDecimals(beginAt, 2);
|
|
let b = L10N.numberWithDecimals(endAt, 2);
|
|
let labelNode = $(".tab-title-label", tab);
|
|
labelNode.setAttribute("value", L10N.getFormatStr("profile.tab", a, b));
|
|
},
|
|
|
|
/**
|
|
* Populates the panel for a tab in this tabbed browser with the provided
|
|
* recording data.
|
|
*
|
|
* @param number tabIndex
|
|
* The index of the tab to populate.
|
|
* @param object recordingData
|
|
* The profiler and refresh driver ticks data received from the front.
|
|
* @param number beginAt
|
|
* The earliest time in the recording data to start at (in milliseconds).
|
|
* @param number endAt
|
|
* The latest time in the recording data to end at (in milliseconds).
|
|
* @param object options
|
|
* Additional options supported by this operation.
|
|
* @see ProfileView._populatePanelWidgets
|
|
*/
|
|
populateTab: Task.async(function*(tabIndex, recordingData, beginAt, endAt, options) {
|
|
let tab = this._getTab(tabIndex);
|
|
let panel = this._getPanel(tabIndex);
|
|
if (!tab || !panel) {
|
|
return;
|
|
}
|
|
|
|
this._recordingInfoByPanel.set(panel, {
|
|
recordingData: recordingData,
|
|
displayRange: { beginAt: beginAt, endAt: endAt }
|
|
});
|
|
|
|
let { profilerData, ticksData } = recordingData;
|
|
let categoriesData = RecordingUtils.plotCategoriesFor(profilerData, beginAt, endAt);
|
|
let framerateData = RecordingUtils.plotFramerateFor(ticksData, beginAt, endAt);
|
|
RecordingUtils.syncCategoriesWithFramerate(categoriesData, framerateData);
|
|
|
|
yield this._populatePanelWidgets(panel, {
|
|
profilerData: profilerData,
|
|
framerateData: framerateData,
|
|
categoriesData: categoriesData
|
|
}, beginAt, endAt, options);
|
|
}),
|
|
|
|
/**
|
|
* Adds a new tab in this tabbed browser, populates it with the provided
|
|
* recording data and automatically selects it.
|
|
*
|
|
* @param object recordingData
|
|
* The profiler and refresh driver ticks data received from the front.
|
|
* @param number beginAt
|
|
* The earliest time in the recording data to start at (in milliseconds).
|
|
* @param number endAt
|
|
* The latest time in the recording data to end at (in milliseconds).
|
|
* @param object options
|
|
* Additional options supported by this operation.
|
|
* @see ProfileView._populatePanelWidgets
|
|
*/
|
|
addTabAndPopulate: Task.async(function*(recordingData, beginAt, endAt, options) {
|
|
let tabIndex = this.addTab();
|
|
this.nameTab(tabIndex, beginAt, endAt);
|
|
|
|
// Wait for a few milliseconds before presenting the recording data,
|
|
// to allow the 'Loading' panel to finish being drawn (if there is one).
|
|
yield DevToolsUtils.waitForTime(RECORDING_DATA_DISPLAY_DELAY);
|
|
yield this.populateTab(tabIndex, recordingData, beginAt, endAt, options);
|
|
this.selectTab(tabIndex);
|
|
}),
|
|
|
|
/**
|
|
* Removes all tabs and corresponding views from this tabbed browser.
|
|
*/
|
|
removeAllTabs: function() {
|
|
for (let [, graph] of this._framerateGraphByPanel) graph.destroy();
|
|
for (let [, graph] of this._categoriesGraphByPanel) graph.destroy();
|
|
for (let [, root] of this._callTreeRootByPanel) root.remove();
|
|
|
|
this._recordingInfoByPanel = new WeakMap();
|
|
this._framerateGraphByPanel.clear();
|
|
this._categoriesGraphByPanel.clear();
|
|
this._callTreeRootByPanel.clear();
|
|
|
|
while (this._tabs.hasChildNodes()) {
|
|
this._tabs.firstChild.remove();
|
|
}
|
|
while (this._panels.hasChildNodes()) {
|
|
this._panels.firstChild.remove();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes all tabs exclusively after the one at the specified index.
|
|
*
|
|
* @param number tabIndex
|
|
* The "leftmost" tab to still keep. Remaining tabs will be removed.
|
|
*/
|
|
removeTabsAfter: function(tabIndex) {
|
|
tabIndex++;
|
|
|
|
while (tabIndex < this._tabs.itemCount) {
|
|
let tab = this._getTab(tabIndex);
|
|
let panel = this._getPanel(tabIndex);
|
|
|
|
this._framerateGraphByPanel.delete(panel);
|
|
this._categoriesGraphByPanel.delete(panel);
|
|
this._callTreeRootByPanel.delete(panel);
|
|
tab.remove();
|
|
panel.remove();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets the total number of tabs displayed in this tabbed browser.
|
|
* @return number
|
|
*/
|
|
get tabCount() {
|
|
let tabs = this._tabs.childNodes.length;
|
|
let tabpanels = this._panels.childNodes.length;
|
|
if (tabs != tabpanels) {
|
|
throw "The number of tabs isn't equal to the number of tabpanels.";
|
|
}
|
|
return tabs;
|
|
},
|
|
|
|
/**
|
|
* Adds a new tab in this tabbed browser, populates it based on the current
|
|
* selection range in the displayed data and automatically selects it.
|
|
*/
|
|
_spawnTabFromSelection: Task.async(function*() {
|
|
let { recordingData } = this._getRecordingInfo();
|
|
let categoriesGraph = this._getCategoriesGraph();
|
|
|
|
// A selection is assumed to be available in the current tab.
|
|
let { min: beginAt, max: endAt } = categoriesGraph.getMappedSelection();
|
|
|
|
// Hide the "new tab" button since a selection won't implicitly be made
|
|
// in the newly created tab.
|
|
this._newtabButton.hidden = true;
|
|
|
|
yield this.addTabAndPopulate(recordingData, beginAt, endAt);
|
|
|
|
// Signal that a new tab was spawned from a graph's selection.
|
|
window.emit(EVENTS.TAB_SPAWNED_FROM_SELECTION);
|
|
}),
|
|
|
|
/**
|
|
* Adds a new tab in this tabbed browser, populates it based on the provided
|
|
* frame node and automatically selects it.
|
|
*
|
|
* @param FrameNode frameNode
|
|
* Information about the function call node in the tree.
|
|
*/
|
|
_spawnTabFromFrameNode: Task.async(function*(frameNode) {
|
|
let { recordingData } = this._getRecordingInfo();
|
|
let sampleTimes = frameNode.sampleTimes;
|
|
let beginAt = sampleTimes[0].start;
|
|
let endAt = sampleTimes[sampleTimes.length - 1].end;
|
|
|
|
// Hide the "new tab" button since a selection won't implicitly be made
|
|
// in the newly created tab.
|
|
this._newtabButton.hidden = true;
|
|
|
|
yield this.addTabAndPopulate(recordingData, beginAt, endAt, { skipCallTree: true });
|
|
this._populateCallTreeFromFrameNode(this._getPanel(), frameNode);
|
|
|
|
// Signal that a new tab was spawned from a node in the call tree.
|
|
window.emit(EVENTS.TAB_SPAWNED_FROM_FRAME_NODE);
|
|
}),
|
|
|
|
/**
|
|
* Filters the recording data displayed in the call tree view to match
|
|
* the current selection range in the graphs.
|
|
*
|
|
* @param object options
|
|
* Additional options supported by this operation.
|
|
* @see ProfileView._populatePanelWidgets
|
|
*/
|
|
_rebuildTreeFromSelection: function(options) {
|
|
let { recordingData, displayRange } = this._getRecordingInfo();
|
|
let categoriesGraph = this._getCategoriesGraph();
|
|
let selectedPanel = this._getPanel();
|
|
|
|
// If there's no selection, get the original display range and hide the
|
|
// "new tab" button.
|
|
if (!categoriesGraph.hasSelection()) {
|
|
let { beginAt, endAt } = displayRange;
|
|
this._newtabButton.hidden = true;
|
|
this._populateCallTree(selectedPanel, recordingData.profilerData, beginAt, endAt, options);
|
|
}
|
|
// Otherwise, just get the selected display range and only show the
|
|
// "new tab" button if the selection is wide enough.
|
|
else {
|
|
let { min: beginAt, max: endAt } = categoriesGraph.getMappedSelection();
|
|
this._newtabButton.hidden = (endAt - beginAt) < GRAPH_ZOOM_MIN_TIMESPAN;
|
|
this._populateCallTree(selectedPanel, recordingData.profilerData, beginAt, endAt, options);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Highlights certain areas in the categories graph to match the currently
|
|
* selected frame node's sample times in the tree view.
|
|
*
|
|
* @param ThreadNode | FrameNode frameNode
|
|
* The root node data source for this tree.
|
|
*/
|
|
_highlightAreaFromFrameNode: function(frameNode) {
|
|
let categoriesGraph = this._getCategoriesGraph();
|
|
if (categoriesGraph) {
|
|
categoriesGraph.setMask(frameNode.sampleTimes);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Populates all the widgets in the specified tab's panel with the provided
|
|
* data. The already existing widgets will be removed.
|
|
*
|
|
* @param nsIDOMNode panel
|
|
* The <panel> element in this <tabbox>.
|
|
* @param object dataSource
|
|
* The profiler, framerate and categories data source.
|
|
* @param number beginAt
|
|
* The earliest allowed time for tree nodes (in milliseconds).
|
|
* @param number endAt
|
|
* The latest allowed time for tree nodes (in milliseconds).
|
|
* @param object options
|
|
* Additional options supported by this operation:
|
|
* - skipCallTree: true if the call tree should not be populated
|
|
* - skipCallTreeFocus: true if the root node shouldn't be focused
|
|
*/
|
|
_populatePanelWidgets: Task.async(function*(panel, dataSource, beginAt, endAt, options = {}) {
|
|
let { profilerData, framerateData, categoriesData } = dataSource;
|
|
|
|
let framerateGraph = yield this._populateFramerateGraph(panel, framerateData, beginAt);
|
|
let categoriesGraph = yield this._populateCategoriesGraph(panel, categoriesData, beginAt);
|
|
CanvasGraphUtils.linkAnimation(framerateGraph, categoriesGraph);
|
|
CanvasGraphUtils.linkSelection(framerateGraph, categoriesGraph);
|
|
|
|
if (!options.skipCallTree) {
|
|
this._populateCallTree(panel, profilerData, beginAt, endAt, options);
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Populates the framerate graph in the specified tab's panel with the
|
|
* provided data. The already existing graph will be removed.
|
|
*
|
|
* @param nsIDOMNode panel
|
|
* The <panel> element in this <tabbox>.
|
|
* @param object framerateData
|
|
* The data source for this graph.
|
|
* @param number beginAt
|
|
* The earliest time in the recording data to start at (in milliseconds).
|
|
*/
|
|
_populateFramerateGraph: Task.async(function*(panel, framerateData, beginAt) {
|
|
let oldGraph = this._getFramerateGraph(panel);
|
|
if (oldGraph) {
|
|
oldGraph.destroy();
|
|
}
|
|
|
|
// Don't create a graph if there's not enough data to show.
|
|
if (!framerateData || framerateData.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
let graph = new LineGraphWidget($(".framerate", panel), L10N.getStr("graphs.fps"));
|
|
graph.fixedHeight = FRAMERATE_GRAPH_HEIGHT;
|
|
graph.dataOffsetX = beginAt;
|
|
|
|
yield graph.setDataWhenReady(framerateData);
|
|
|
|
graph.on("mouseup", this._onGraphMouseUp);
|
|
graph.on("scroll", this._onGraphScroll);
|
|
|
|
this._framerateGraphByPanel.set(panel, graph);
|
|
return graph;
|
|
}),
|
|
|
|
/**
|
|
* Populates the categories graph in the specified tab's panel with the
|
|
* provided data. The already existing graph will be removed.
|
|
*
|
|
* @param nsIDOMNode panel
|
|
* The <panel> element in this <tabbox>.
|
|
* @param object categoriesData
|
|
* The data source for this graph.
|
|
* @param number beginAt
|
|
* The earliest time in the recording data to start at (in milliseconds).
|
|
*/
|
|
_populateCategoriesGraph: Task.async(function*(panel, categoriesData, beginAt) {
|
|
let oldGraph = this._getCategoriesGraph(panel);
|
|
if (oldGraph) {
|
|
oldGraph.destroy();
|
|
}
|
|
// Don't create a graph if there's not enough data to show.
|
|
if (!categoriesData || categoriesData.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
let graph = new BarGraphWidget($(".categories", panel));
|
|
graph.fixedHeight = CATEGORIES_GRAPH_HEIGHT;
|
|
graph.minBarsWidth = CATEGORIES_GRAPH_MIN_BARS_WIDTH;
|
|
graph.format = CATEGORIES.sort((a, b) => a.ordinal > b.ordinal);
|
|
graph.dataOffsetX = beginAt;
|
|
|
|
yield graph.setDataWhenReady(categoriesData);
|
|
|
|
graph.on("legend-selection", this._onGraphLegendSelection);
|
|
graph.on("mouseup", this._onGraphMouseUp);
|
|
graph.on("scroll", this._onGraphScroll);
|
|
|
|
this._categoriesGraphByPanel.set(panel, graph);
|
|
return graph;
|
|
}),
|
|
|
|
/**
|
|
* Populates the call tree view in the specified tab's panel with the
|
|
* provided data. The already existing tree will be removed.
|
|
*
|
|
* @param nsIDOMNode panel
|
|
* The <panel> element in this <tabbox>.
|
|
* @param object profilerData
|
|
* The data source for this tree.
|
|
* @param number beginAt
|
|
* The earliest time in the data source to start at (in milliseconds).
|
|
* @param number endAt
|
|
* The latest time in the data source to end at (in milliseconds).
|
|
* @param object options
|
|
* Additional options supported by this operation.
|
|
* @see ProfileView._populatePanelWidgets
|
|
*/
|
|
_populateCallTree: function(panel, profilerData, beginAt, endAt, options = {}) {
|
|
let threadSamples = profilerData.profile.threads[0].samples;
|
|
let contentOnly = !Prefs.showPlatformData;
|
|
let invertChecked = this._invertTree.hasAttribute("checked");
|
|
let threadNode = new ThreadNode(threadSamples, contentOnly, beginAt, endAt,
|
|
invertChecked);
|
|
// If we have an empty profile (no samples), then don't invert the tree, as
|
|
// it would hide the root node and a completely blank call tree space can be
|
|
// mis-interpreted as an error.
|
|
options.inverted = invertChecked && threadNode.samples > 0;
|
|
this._populateCallTreeFromFrameNode(panel, threadNode, options);
|
|
},
|
|
|
|
/**
|
|
* Populates the call tree view in the specified tab's panel with the
|
|
* provided frame node. The already existing tree will be removed.
|
|
*
|
|
* @param nsIDOMNode panel
|
|
* The <panel> element in this <tabbox>.
|
|
* @param ThreadNode | FrameNode frameNode
|
|
* The root node data source for this tree.
|
|
* @param object options
|
|
* Additional options supported by this operation.
|
|
* @see ProfileView._populatePanelWidgets
|
|
*/
|
|
_populateCallTreeFromFrameNode: function(panel, frameNode, options = {}) {
|
|
let oldRoot = this._getCallTreeRoot(panel);
|
|
if (oldRoot) {
|
|
oldRoot.remove();
|
|
}
|
|
|
|
let callTreeRoot = new CallView({
|
|
autoExpandDepth: options.inverted ? 0 : undefined,
|
|
frame: frameNode,
|
|
hidden: options.inverted,
|
|
inverted: options.inverted
|
|
});
|
|
callTreeRoot.on("focus", this._onCallViewFocus);
|
|
callTreeRoot.on("link", this._onCallViewLink);
|
|
callTreeRoot.on("zoom", this._onCallViewZoom);
|
|
callTreeRoot.attachTo($(".call-tree-cells-container", panel));
|
|
|
|
if (!options.skipCallTreeFocus) {
|
|
callTreeRoot.focus();
|
|
}
|
|
|
|
let contentOnly = !Prefs.showPlatformData;
|
|
callTreeRoot.toggleCategories(!contentOnly);
|
|
|
|
this._callTreeRootByPanel.set(panel, callTreeRoot);
|
|
},
|
|
|
|
/**
|
|
* Shortcuts for accessing the recording info or widgets for a <panel>.
|
|
* @param nsIDOMNode panel [optional]
|
|
* @return object
|
|
*/
|
|
_getRecordingInfo: function(panel = this._getPanel()) {
|
|
return this._recordingInfoByPanel.get(panel);
|
|
},
|
|
_getFramerateGraph: function(panel = this._getPanel()) {
|
|
return this._framerateGraphByPanel.get(panel);
|
|
},
|
|
_getCategoriesGraph: function(panel = this._getPanel()) {
|
|
return this._categoriesGraphByPanel.get(panel);
|
|
},
|
|
_getCallTreeRoot: function(panel = this._getPanel()) {
|
|
return this._callTreeRootByPanel.get(panel);
|
|
},
|
|
_getTab: function(tabIndex = this._getSelectedIndex()) {
|
|
return this._tabs.childNodes[tabIndex];
|
|
},
|
|
_getPanel: function(tabIndex = this._getSelectedIndex()) {
|
|
return this._panels.childNodes[tabIndex];
|
|
},
|
|
_getSelectedIndex: function() {
|
|
return $("#profile-content").selectedIndex;
|
|
},
|
|
|
|
/**
|
|
* Listener handling the tab "select" event in this container.
|
|
*/
|
|
_onTabSelect: function() {
|
|
let categoriesGraph = this._getCategoriesGraph();
|
|
if (categoriesGraph) {
|
|
this._newtabButton.hidden = !categoriesGraph.hasSelection();
|
|
} else {
|
|
this._newtabButton.hidden = true;
|
|
}
|
|
|
|
this.removeTabsAfter(this._getSelectedIndex());
|
|
},
|
|
|
|
/**
|
|
* Listener handling the new tab "click" event in this container.
|
|
*/
|
|
_onNewTabClick: function() {
|
|
this._spawnTabFromSelection();
|
|
},
|
|
|
|
_onInvertTree: function() {
|
|
this._rebuildTreeFromSelection();
|
|
},
|
|
|
|
/**
|
|
* Listener handling the "legend-selection" event for the graphs in this container.
|
|
*/
|
|
_onGraphLegendSelection: function() {
|
|
this._rebuildTreeFromSelection({ skipCallTreeFocus: true });
|
|
},
|
|
|
|
/**
|
|
* Listener handling the "mouseup" event for the graphs in this container.
|
|
*/
|
|
_onGraphMouseUp: function() {
|
|
this._rebuildTreeFromSelection();
|
|
},
|
|
|
|
/**
|
|
* Listener handling the "scroll" event for the graphs in this container.
|
|
*/
|
|
_onGraphScroll: function() {
|
|
setNamedTimeout("graph-scroll", GRAPH_SCROLL_EVENTS_DRAIN, () => {
|
|
this._rebuildTreeFromSelection();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Listener handling the "focus" event for the call tree in this container.
|
|
*/
|
|
_onCallViewFocus: function(event, treeItem) {
|
|
setNamedTimeout("graph-focus", CALL_VIEW_FOCUS_EVENTS_DRAIN, () => {
|
|
this._highlightAreaFromFrameNode(treeItem.frame);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Listener handling the "link" event for the call tree in this container.
|
|
*/
|
|
_onCallViewLink: function(event, treeItem) {
|
|
let { url, line } = treeItem.frame.getInfo();
|
|
viewSourceInDebugger(url, line);
|
|
},
|
|
|
|
/**
|
|
* Listener handling the "zoom" event for the call tree in this container.
|
|
*/
|
|
_onCallViewZoom: function(event, treeItem) {
|
|
this._spawnTabFromFrameNode(treeItem.frame);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Utility functions handling recording data.
|
|
*/
|
|
let RecordingUtils = {
|
|
_frameratePlotsCache: new WeakMap(),
|
|
|
|
/**
|
|
* Creates an appropriate data source to be displayed in a categories graph
|
|
* from on the provided profiler data.
|
|
*
|
|
* @param object profilerData
|
|
* The profiler data received from the front.
|
|
* @param number beginAt
|
|
* The earliest time in the profiler data to start at (in milliseconds).
|
|
* @param number endAt
|
|
* The latest time in the profiler data to end at (in milliseconds).
|
|
* @return array
|
|
* A data source useful for a BarGraphWidget.
|
|
*/
|
|
plotCategoriesFor: function(profilerData, beginAt, endAt) {
|
|
let categoriesData = [];
|
|
let profile = profilerData.profile;
|
|
let samples = profile.threads[0].samples;
|
|
|
|
// Accumulate the category of each frame for every sample.
|
|
for (let { frames, time } of samples) {
|
|
if (!time || time < beginAt || time > endAt) continue;
|
|
let blocks = [];
|
|
|
|
for (let { category: bitmask } of frames) {
|
|
if (!bitmask) continue;
|
|
let category = CATEGORY_MAPPINGS[bitmask];
|
|
|
|
// Guard against categories that aren't found in the frontend mappings.
|
|
// This usually means that a new category was added in the platform,
|
|
// but browser/devtools/profiler/utils/global.js wasn't updated yet.
|
|
if (!category) {
|
|
category = CATEGORY_MAPPINGS[CATEGORY_OTHER];
|
|
}
|
|
|
|
if (!blocks[category.ordinal]) {
|
|
blocks[category.ordinal] = 1;
|
|
} else {
|
|
blocks[category.ordinal]++;
|
|
}
|
|
}
|
|
|
|
// If no categories were found in the frames, default to using a
|
|
// single block using the stack depth as height.
|
|
if (blocks.length == 0) {
|
|
blocks[CATEGORY_MAPPINGS[CATEGORY_OTHER].ordinal] = frames.length;
|
|
}
|
|
|
|
categoriesData.push({
|
|
delta: time,
|
|
values: blocks
|
|
});
|
|
}
|
|
|
|
return categoriesData;
|
|
},
|
|
|
|
/**
|
|
* Creates an appropriate data source to be displayed in a framerate graph
|
|
* from on the provided refresh driver ticks data.
|
|
*
|
|
* @param object ticksData
|
|
* The refresh driver ticks received from the front.
|
|
* @param number beginAt
|
|
* The earliest time in the ticks data to start at (in milliseconds).
|
|
* @param number endAt
|
|
* The latest time in the ticks data to end at (in milliseconds).
|
|
* @return array
|
|
* A data source useful for a LineGraphWidget.
|
|
*/
|
|
plotFramerateFor: function(ticksData, beginAt, endAt) {
|
|
// Older Gecko versions don't have a framerate actor implementation,
|
|
// in which case the returned ticks data is null.
|
|
if (ticksData == null) {
|
|
return [];
|
|
}
|
|
|
|
let framerateData = this._frameratePlotsCache.get(ticksData);
|
|
if (framerateData == null) {
|
|
framerateData = FramerateFront.plotFPS(ticksData, FRAMERATE_CALC_INTERVAL);
|
|
this._frameratePlotsCache.set(ticksData, framerateData);
|
|
}
|
|
|
|
// Quickly find the earliest and oldest valid index in the plotted
|
|
// framerate data based on the specified beginAt and endAt time. Sure,
|
|
// using [].findIndex would be more elegant, but also slower.
|
|
let earliestValidIndex = findFirstIndex(framerateData, e => e.delta >= beginAt);
|
|
let oldestValidIndex = findLastIndex(framerateData, e => e.delta <= endAt);
|
|
let totalValues = framerateData.length;
|
|
|
|
// If all the plotted framerate data fits inside the specified time range,
|
|
// simply return it.
|
|
if (earliestValidIndex == 0 && oldestValidIndex == totalValues - 1) {
|
|
return framerateData;
|
|
}
|
|
|
|
// Otherwise, a slice will need to be made. Be very careful here, the
|
|
// beginAt and endAt timestamps can refer to a point in *between* two
|
|
// entries in the framerate data, so we'll need to insert new values where
|
|
// the cuts are made.
|
|
let slicedData = framerateData.slice(earliestValidIndex, oldestValidIndex + 1);
|
|
if (earliestValidIndex > 0) {
|
|
slicedData.unshift({
|
|
delta: beginAt,
|
|
value: framerateData[earliestValidIndex - 1].value
|
|
});
|
|
}
|
|
if (oldestValidIndex < totalValues - 1) {
|
|
slicedData.push({
|
|
delta: endAt,
|
|
value: framerateData[oldestValidIndex + 1].value
|
|
});
|
|
}
|
|
|
|
return slicedData;
|
|
},
|
|
|
|
/**
|
|
* Makes sure the data sources for the categories and framerate graphs
|
|
* have the same beginning and ending, time-wise.
|
|
*
|
|
* @param array categoriesData
|
|
* Data source generated by `RecordingUtils.plotCategoriesFor`.
|
|
* @param array framerateData
|
|
* Data source generated by `RecordingUtils.plotFramerateFor`.
|
|
*/
|
|
syncCategoriesWithFramerate: function(categoriesData, framerateData) {
|
|
if (categoriesData.length < 2 || framerateData.length < 2) {
|
|
return;
|
|
}
|
|
let categoryBegin = categoriesData[0];
|
|
let categoryEnd = categoriesData[categoriesData.length - 1];
|
|
let framerateBegin = framerateData[0];
|
|
let framerateEnd = framerateData[framerateData.length - 1];
|
|
|
|
if (categoryBegin.delta > framerateBegin.delta) {
|
|
categoriesData.unshift({
|
|
delta: framerateBegin.delta,
|
|
values: categoryBegin.values
|
|
});
|
|
} else {
|
|
framerateData.unshift({
|
|
delta: categoryBegin.delta,
|
|
value: framerateBegin.value
|
|
});
|
|
}
|
|
if (categoryEnd.delta < framerateEnd.delta) {
|
|
categoriesData.push({
|
|
delta: framerateEnd.delta,
|
|
values: categoryEnd.values
|
|
});
|
|
} else {
|
|
framerateData.push({
|
|
delta: categoryEnd.delta,
|
|
value: framerateEnd.value
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Finds the index of the first element in an array that validates a predicate.
|
|
* @param array
|
|
* @param function predicate
|
|
* @return number
|
|
*/
|
|
function findFirstIndex(array, predicate) {
|
|
for (let i = 0, len = array.length; i < len; i++) {
|
|
if (predicate(array[i])) return i;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the last of the first element in an array that validates a predicate.
|
|
* @param array
|
|
* @param function predicate
|
|
* @return number
|
|
*/
|
|
function findLastIndex(array, predicate) {
|
|
for (let i = array.length - 1; i >= 0; i--) {
|
|
if (predicate(array[i])) return i;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens/selects the debugger in this toolbox and jumps to the specified
|
|
* file name and line number.
|
|
* @param string url
|
|
* @param number line
|
|
*/
|
|
function viewSourceInDebugger(url, line) {
|
|
let showSource = ({ DebuggerView }) => {
|
|
let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === url);
|
|
if (item) {
|
|
DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true }).then(() => {
|
|
window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
|
|
}, () => {
|
|
window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
|
|
});
|
|
}
|
|
};
|
|
|
|
// If the Debugger was already open, switch to it and try to show the
|
|
// source immediately. Otherwise, initialize it and wait for the sources
|
|
// to be added first.
|
|
let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
|
|
gToolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
|
|
if (debuggerAlreadyOpen) {
|
|
showSource(dbg);
|
|
} else {
|
|
dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
|
|
}
|
|
});
|
|
}
|