Bug 1668219 - [devtools] Remove devtools/client/performance. r=julienw.

Differential Revision: https://phabricator.services.mozilla.com/D145461
This commit is contained in:
Nicolas Chevobbe 2022-05-06 17:16:27 +00:00
parent e1f5f32c3a
commit 30db855c84
239 changed files with 5 additions and 28124 deletions

View File

@ -32,15 +32,12 @@ devtools.jar:
skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg)
skin/animation.css (themes/animation.css)
skin/perf.css (themes/perf.css)
skin/performance.css (themes/performance.css)
skin/memory.css (themes/memory.css)
skin/storage.css (themes/storage.css)
skin/splitview.css (themes/splitview.css)
skin/styleeditor.css (themes/styleeditor.css)
skin/components-frame.css (themes/components-frame.css)
skin/components-h-split-box.css (themes/components-h-split-box.css)
skin/jit-optimizations.css (themes/jit-optimizations.css)
skin/images/filter.svg (themes/images/filter.svg)
skin/images/filter-small.svg (themes/images/filter-small.svg)
skin/images/search.svg (themes/images/search.svg)
skin/images/item-toggle.svg (themes/images/item-toggle.svg)
@ -167,9 +164,6 @@ devtools.jar:
skin/images/pseudo-class.svg (themes/images/pseudo-class.svg)
skin/images/copy.svg (themes/images/copy.svg)
skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
skin/images/performance-details-waterfall.svg (themes/images/performance-details-waterfall.svg)
skin/images/performance-details-call-tree.svg (themes/images/performance-details-call-tree.svg)
skin/images/performance-details-flamegraph.svg (themes/images/performance-details-flamegraph.svg)
skin/breadcrumbs.css (themes/breadcrumbs.css)
skin/chart.css (themes/chart.css)
skin/widgets.css (themes/widgets.css)
@ -336,7 +330,6 @@ devtools.jar:
content/debugger/src/utils/editor/source-editor.css (debugger/src/utils/editor/source-editor.css)
# Perfomance
content/performance/index.xhtml (performance/index.xhtml)
content/performance-new/index.xhtml (performance-new/index.xhtml)
# Memory

View File

@ -1,35 +0,0 @@
# 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/.
# LOCALIZATION NOTE These strings are used within the JIT tools
# in the Performance Tools which is available from the Web Developer
# sub-menu -> 'Performance' The correct localization of this file might
# be to keep it in English, or another language commonly spoken among
# web developers. You want to make that choice consistent across the
# developer tools. A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (jit.title):
# This string is displayed in the header of the JIT Optimizations view.
jit.title=JIT Optimizations
# LOCALIZATION NOTE (jit.optimizationFailure):
# This string is displayed in a tooltip when no JIT optimizations were detected.
jit.optimizationFailure=Optimization failed
# LOCALIZATION NOTE (jit.samples):
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
# This string is displayed for the unit representing the number of times a
# frame is sampled.
# "#1" represents the number of samples
# example: 30 samples
jit.samples=#1 sample;#1 samples
# LOCALIZATION NOTE (jit.types):
# This string is displayed for the group of Ion Types in the optimizations view.
jit.types=Types
# LOCALIZATION NOTE (jit.attempts):
# This string is displayed for the group of optimization attempts in the optimizations view.
jit.attempts=Attempts

View File

@ -1,138 +0,0 @@
# 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/.
# LOCALIZATION NOTE These strings are used inside the Performance Tools
# which is available from the Web Developer sub-menu -> 'Performance'.
# The correct localization of this file might be to keep it in
# English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web. These strings
# are specifically for marker names in the performance tool.
# LOCALIZATION NOTE (marker.label.*):
# These strings are displayed in the Performance Tool waterfall, identifying markers.
# We want to use the same wording as Google Chrome when appropriate.
marker.label.styles=Recalculate Style
marker.label.stylesApplyChanges=Apply Style Changes
marker.label.reflow=Layout
marker.label.paint=Paint
marker.label.composite=Composite Layers
marker.label.compositeForwardTransaction=Composite Request Sent
marker.label.javascript=Function Call
marker.label.parseHTML=Parse HTML
marker.label.parseXML=Parse XML
marker.label.domevent=DOM Event
marker.label.consoleTime=Console
marker.label.garbageCollection2=Garbage Collection
marker.label.garbageCollection.incremental=Incremental GC
marker.label.garbageCollection.nonIncremental=Non-incremental GC
marker.label.minorGC=Minor GC
marker.label.cycleCollection=Cycle Collection
marker.label.cycleCollection.forgetSkippable=CC Graph Reduction
marker.label.timestamp=Timestamp
marker.label.worker=Worker
marker.label.messagePort=MessagePort
marker.label.unknown=Unknown
# LOCALIZATION NOTE (marker.label.javascript.*):
# These strings are displayed as JavaScript markers that have special
# reasons that can be translated.
marker.label.javascript.scriptElement=Script Tag
marker.label.javascript.promiseCallback=Promise Callback
marker.label.javascript.promiseInit=Promise Init
marker.label.javascript.workerRunnable=Worker
marker.label.javascript.jsURI=JavaScript URI
marker.label.javascript.eventHandler=Event Handler
# LOCALIZATION NOTE (marker.field.*):
# Strings used in the waterfall sidebar as property names.
# General marker fields
marker.field.start=Start:
marker.field.end=End:
marker.field.duration=Duration:
# General "reason" for a marker (JavaScript, Garbage Collection)
marker.field.causeName=Cause:
# General "type" for a marker (Cycle Collection, Garbage Collection)
marker.field.type=Type:
# General "label" for a marker (user defined)
marker.field.label=Label:
# Field names for stack values
marker.field.stack=Stack:
marker.field.startStack=Stack at start:
marker.field.endStack=Stack at end:
# %S is the "Async Cause" of a marker, and this signifies that the cause
# was an asynchronous one in a displayed stack.
marker.field.asyncStack=(Async: %S)
# For console.time markers
marker.field.consoleTimerName=Timer Name:
# For DOM Event markers
marker.field.DOMEventType=Event Type:
marker.field.DOMEventPhase=Phase:
# Non-incremental cause for a Garbage Collection marker
marker.field.nonIncrementalCause=Non-incremental Cause:
# For "Recalculate Style" markers
marker.field.isAnimationOnly=Animation Only:
# The type of operation performed by a Worker.
marker.worker.serializeDataOffMainThread=Serialize data in Worker
marker.worker.serializeDataOnMainThread=Serialize data on the main thread
marker.worker.deserializeDataOffMainThread=Deserialize data in Worker
marker.worker.deserializeDataOnMainThread=Deserialize data on the main thread
# The type of operation performed by a MessagePort
marker.messagePort.serializeData=Serialize data
marker.messagePort.deserializeData=Deserialize data
# Strings used in the waterfall sidebar as values.
marker.value.unknownFrame=<unknown location>
marker.value.DOMEventTargetPhase=Target
marker.value.DOMEventCapturingPhase=Capture
marker.value.DOMEventBubblingPhase=Bubbling
# LOCALIZATION NOTE (marker.gcreason.label.*):
# These strings are used to give a concise but readable description of a GC reason.
marker.gcreason.label.API=API Call
marker.gcreason.label.EAGER_ALLOC_TRIGGER=Eager Allocation Trigger
marker.gcreason.label.DESTROY_RUNTIME=Shutdown
marker.gcreason.label.LAST_DITCH=Out of Memory
marker.gcreason.label.TOO_MUCH_MALLOC=Too Many Bytes Allocated
marker.gcreason.label.ALLOC_TRIGGER=Too Many Allocations
marker.gcreason.label.DEBUG_GC=Debug GC
marker.gcreason.label.COMPARTMENT_REVIVED=Dead Global Revived
marker.gcreason.label.RESET=Finish Incremental Cycle
marker.gcreason.label.OUT_OF_NURSERY=Nursery is Full
marker.gcreason.label.EVICT_NURSERY=Nursery Eviction
marker.gcreason.label.FULL_STORE_BUFFER=Nursery Objects Too Active
marker.gcreason.label.SHARED_MEMORY_LIMIT=Large Allocation Failed
marker.gcreason.label.PERIODIC_FULL_GC=Periodic Full GC
marker.gcreason.label.INCREMENTAL_TOO_SLOW=Allocations Rate Too Fast
marker.gcreason.label.COMPONENT_UTILS=Cu.forceGC
marker.gcreason.label.MEM_PRESSURE=Low Memory
marker.gcreason.label.CC_FINISHED=Cycle Collection Finished
marker.gcreason.label.CC_FORCED=Forced by Cycle Collection
marker.gcreason.label.LOAD_END=Page Load Finished
marker.gcreason.label.PAGE_HIDE=Moved to Background
marker.gcreason.label.NSJSCONTEXT_DESTROY=Destroy JS Context
marker.gcreason.label.SET_NEW_DOCUMENT=New Document
marker.gcreason.label.SET_DOC_SHELL=New Document
marker.gcreason.label.DOM_UTILS=API Call
marker.gcreason.label.DOM_IPC=IPC
marker.gcreason.label.DOM_WORKER=Periodic Worker GC
marker.gcreason.label.INTER_SLICE_GC=Periodic Incremental GC Slice
marker.gcreason.label.FULL_GC_TIMER=Periodic Full GC
marker.gcreason.label.SHUTDOWN_CC=Shutdown
marker.gcreason.label.DOM_WINDOW_UTILS=User Inactive
marker.gcreason.label.USER_INACTIVE=User Inactive
# The name of a nursery collection.
marker.nurseryCollection=Nursery Collection

View File

@ -1,129 +0,0 @@
<!-- 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/. -->
<!-- LOCALIZATION NOTE : FILE This file contains the Performance strings -->
<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
- keep it in English, or another language commonly spoken among web developers.
- You want to make that choice consistent across the developer tools.
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (performanceUI.bufferStatusTooltip): This string
- is displayed as the tooltip for the buffer capacity during a recording. -->
<!ENTITY performanceUI.bufferStatusTooltip "The profiler stores samples in a circular buffer, and once the buffer reaches the limit for a recording, newer samples begin to overwrite samples at the beginning of the recording.">
<!-- LOCALIZATION NOTE (performanceUI.disabledRealTime.disabledE10S): This string
- is displayed as a message for why the real time overview graph is disabled
- when running on a build that can run multiprocess Firefox, but just is not enabled. -->
<!ENTITY performanceUI.disabledRealTime.disabledE10S "Enable multiprocess Firefox in preferences for rendering recording data in realtime.">
<!-- LOCALIZATION NOTE (performanceUI.bufferStatusFull): This string
- is displayed when the profiler's circular buffer has started to overlap. -->
<!ENTITY performanceUI.bufferStatusFull "The buffer is full. Older samples are now being overwritten.">
<!-- LOCALIZATION NOTE (performanceUI.unavailableNoticePB): This is the label shown
- in the details view while the profiler is unavailable, for example, while
- in Private Browsing mode. -->
<!ENTITY performanceUI.unavailableNoticePB "Recording a profile is currently unavailable. Please close all private browsing windows and try again.">
<!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
- in the details view while loading a profile. -->
<!ENTITY performanceUI.loadingNotice "Loading…">
<!-- LOCALIZATION NOTE (performanceUI.toolbar.*): These strings are displayed
- in the toolbar on buttons that select which view is currently shown. -->
<!ENTITY performanceUI.toolbar.waterfall "Waterfall">
<!ENTITY performanceUI.toolbar.waterfall.tooltiptext "Shows the different operations the browser is performing during the recording, laid out sequentially as a waterfall.">
<!ENTITY performanceUI.toolbar.js-calltree "Call Tree">
<!ENTITY performanceUI.toolbar.js-calltree.tooltiptext "Highlights JavaScript functions where the browser spent most time during the recording.">
<!ENTITY performanceUI.toolbar.memory-calltree "Allocations">
<!ENTITY performanceUI.toolbar.allocations.tooltiptext "Shows where memory was allocated during the recording.">
<!ENTITY performanceUI.toolbar.js-flamegraph "JS Flame Chart">
<!ENTITY performanceUI.toolbar.js-flamegraph.tooltiptext "Shows the JavaScript call stack over the course of the recording.">
<!ENTITY performanceUI.toolbar.memory-flamegraph "Allocations Flame Chart">
<!-- LOCALIZATION NOTE (performanceUI.table.*): These strings are displayed
- in the call tree headers for a recording. -->
<!ENTITY performanceUI.table.totalDuration "Total Time">
<!ENTITY performanceUI.table.totalDuration.tooltip "The amount of time spent in this function and functions it calls.">
<!ENTITY performanceUI.table.selfDuration "Self Time">
<!ENTITY performanceUI.table.selfDuration.tooltip "The amount of time spent only within this function.">
<!ENTITY performanceUI.table.totalPercentage "Total Cost">
<!ENTITY performanceUI.table.totalPercentage.tooltip "The percentage of time spent in this function and functions it calls.">
<!ENTITY performanceUI.table.selfPercentage "Self Cost">
<!ENTITY performanceUI.table.selfPercentage.tooltip "The percentage of time spent only within this function.">
<!ENTITY performanceUI.table.samples "Samples">
<!ENTITY performanceUI.table.samples.tooltip "The number of times this function was on the stack when the profiler took a sample.">
<!ENTITY performanceUI.table.function "Function">
<!ENTITY performanceUI.table.function.tooltip "The name and source location of the sampled function.">
<!ENTITY performanceUI.table.totalAlloc.tooltip "The total number of Object allocations sampled at this location and in callees.">
<!-- LOCALIZATION NOTE (performanceUI.options.filter.tooltiptext): This string
- is displayed next to the filter button-->
<!ENTITY performanceUI.options.filter.tooltiptext "Select what data to display in the timeline">
<!-- LOCALIZATION NOTE (performanceUI.options.gear.tooltiptext): This is the
- tooltip for the options button. -->
<!ENTITY performanceUI.options.gear.tooltiptext "Configure performance preferences.">
<!-- LOCALIZATION NOTE (performanceUI.invertTree): This is the label shown next to
- a checkbox that inverts and un-inverts the profiler's call tree. -->
<!ENTITY performanceUI.invertTree "Invert Call Tree">
<!ENTITY performanceUI.invertTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
<!-- LOCALIZATION NOTE (performanceUI.invertFlameGraph): This is the label shown next to
- a checkbox that inverts and un-inverts the profiler's flame graph. -->
<!ENTITY performanceUI.invertFlameGraph "Invert Flame Chart">
<!ENTITY performanceUI.invertFlameGraph.tooltiptext "Inverting the flame chart displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
<!-- LOCALIZATION NOTE (performanceUI.showPlatformData): This is the
- label for the checkbox that toggles whether or not Gecko platform data
- is displayed in the profiler. -->
<!ENTITY performanceUI.showPlatformData "Show Gecko Platform Data">
<!ENTITY performanceUI.showPlatformData.tooltiptext "Showing platform data enables the JavaScript Profiler reports to include Gecko platform symbols.">
<!-- LOCALIZATION NOTE (performanceUI.showJITOptimizations): This string
- is displayed next to a checkbox determining whether or not JIT optimization data
- should be displayed. -->
<!ENTITY performanceUI.showJITOptimizations "Show JIT Optimizations">
<!ENTITY performanceUI.showJITOptimizations.tooltiptext "Show JIT optimization data sampled in each JavaScript frame.">
<!-- LOCALIZATION NOTE (performanceUI.flattenTreeRecursion): This is the
- label for the checkbox that toggles the flattening of tree recursion in inspected
- functions in the profiler. -->
<!ENTITY performanceUI.flattenTreeRecursion "Flatten Tree Recursion">
<!ENTITY performanceUI.flattenTreeRecursion.tooltiptext "Flatten recursion when inspecting functions.">
<!-- LOCALIZATION NOTE (performanceUI.enableMemory): This string
- is displayed next to a checkbox determining whether or not memory
- measurements are enabled. -->
<!ENTITY performanceUI.enableMemory "Record Memory">
<!ENTITY performanceUI.enableMemory.tooltiptext "Record memory consumption while profiling.">
<!-- LOCALIZATION NOTE (performanceUI.enableAllocations): This string
- is displayed next to a checkbox determining whether or not allocation
- measurements are enabled. -->
<!ENTITY performanceUI.enableAllocations "Record Allocations">
<!ENTITY performanceUI.enableAllocations.tooltiptext "Record Object allocations while profiling.">
<!-- LOCALIZATION NOTE (performanceUI.enableFramerate): This string
- is displayed next to a checkbox determining whether or not framerate
- is recorded. -->
<!ENTITY performanceUI.enableFramerate "Record Framerate">
<!ENTITY performanceUI.enableFramerate.tooltiptext "Record framerate while profiling.">
<!-- LOCALIZATION NOTE (performanceUI.console.recordingNoticeStart/recordingNoticeEnd):
- This string is displayed when a recording is selected that started via console.profile.
- Wraps the command used to start, like "Currently recording via console.profile("label")" -->
<!ENTITY performanceUI.console.recordingNoticeStart "Currently recording via">
<!ENTITY performanceUI.console.recordingNoticeEnd "">
<!-- LOCALIZATION NOTE (performanceUI.console.stopCommandStart/stopCommandEnd):
- This string is displayed when a recording is selected that started via console.profile.
- Indicates how to stop the recording, wrapping the command, like
- "Stop recording by entering console.profileEnd("label") into the console." -->
<!ENTITY performanceUI.console.stopCommandStart "Stop recording by entering">
<!ENTITY performanceUI.console.stopCommandEnd "into the console.">

View File

@ -1,160 +0,0 @@
# 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/.
# LOCALIZATION NOTE These strings are used inside the Performance Tools
# which is available from the Web Developer sub-menu -> 'Performance'.
# The correct localization of this file might be to keep it in
# English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (noRecordingsText): The text to display in the
# recordings menu when there are no recorded profiles yet.
noRecordingsText=There are no profiles yet.
# LOCALIZATION NOTE (recordingsList.itemLabel):
# This string is displayed in the recordings list of the Performance Tools,
# identifying a set of function calls. %S represents the number of recording,
# iterating for every new recording, resulting in "Recording #1", "Recording #2", etc.
recordingsList.itemLabel=Recording #%S
# LOCALIZATION NOTE (recordingsList.recordingLabel):
# This string is displayed in the recordings list of the Performance Tools,
# for an item that has not finished recording.
recordingsList.recordingLabel=In progress…
# LOCALIZATION NOTE (recordingsList.loadingLabel):
# This string is displayed in the recordings list of the Performance Tools,
# for an item that is finished and is loading.
recordingsList.loadingLabel=Loading…
# LOCALIZATION NOTE (recordingsList.durationLabel):
# This string is displayed in the recordings list of the Performance Tools,
# for an item that has finished recording.
recordingsList.durationLabel=%S ms
# LOCALIZATION NOTE (recordingsList.saveLabel):
# This string is displayed in the recordings list of the Performance Tools,
# for saving an item to disk.
recordingsList.saveLabel=Save
# LOCALIZATION NOTE (graphs.fps):
# This string is displayed in the framerate graph of the Performance Tools,
# as the unit used to measure frames per second. This label should be kept
# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
graphs.fps=fps
# LOCALIZATION NOTE (graphs.ms):
# This string is displayed in the flamegraph of the Performance Tools,
# as the unit used to measure time (in milliseconds). This label should be kept
# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
graphs.ms=ms
# LOCALIZATION NOTE (graphs.memory):
# This string is displayed in the memory graph of the Performance tool,
# as the unit used to memory consumption. This label should be kept
# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
graphs.memory=MB
# LOCALIZATION NOTE (category.*):
# These strings are displayed in the categories graph of the Performance Tools,
# as the legend for each block in every bar. These labels should be kept
# AS SHORT AS POSSIBLE so they don't obstruct important parts of the graph.
category.other=Gecko
category.layout=Layout
category.js=JIT
category.gc=GC
category.network=Network
category.graphics=Graphics
category.dom=DOM
category.idle=Idle
category.tools=Tools
# LOCALIZATION NOTE (table.bytes):
# This string is displayed in the call tree after bytesize units.
# %S represents the value in bytes.
table.bytes=%S B
# LOCALIZATION NOTE (table.ms2):
# This string is displayed in the call tree after units of time in milliseconds.
# %S represents the value in milliseconds.
table.ms2=%S ms
# LOCALIZATION NOTE (table.percentage3):
# This string is displayed in the call tree after units representing percentages.
# %S represents the value in percentage with two decimal points, localized.
# there are two "%" after %S to escape and display "%"
table.percentage3=%S%%
# LOCALIZATION NOTE (table.root):
# This string is displayed in the call tree for the root node.
table.root=(root)
# LOCALIZATION NOTE (table.idle):
# This string is displayed in the call tree for the idle blocks.
table.idle=(idle)
# LOCALIZATION NOTE (table.url.tooltiptext):
# This string is displayed in the call tree as the tooltip text for the url
# labels which, when clicked, jump to the debugger.
table.url.tooltiptext=View source in Debugger
# LOCALIZATION NOTE (table.view-optimizations.tooltiptext2):
# This string is displayed in the icon displayed next to frames that
# have optimization data
table.view-optimizations.tooltiptext2=Frame contains JIT optimization data
# LOCALIZATION NOTE (recordingsList.importDialogTitle):
# This string is displayed as a title for importing a recoring from disk.
recordingsList.importDialogTitle=Import recording…
# LOCALIZATION NOTE (recordingsList.saveDialogTitle):
# This string is displayed as a title for saving a recording to disk.
recordingsList.saveDialogTitle=Save recording…
# LOCALIZATION NOTE (recordingsList.saveDialogJSONFilter):
# This string is displayed as a filter for saving a recording to disk.
recordingsList.saveDialogJSONFilter=JSON Files
# LOCALIZATION NOTE (recordingsList.saveDialogAllFilter):
# This string is displayed as a filter for saving a recording to disk.
recordingsList.saveDialogAllFilter=All Files
# LOCALIZATION NOTE (timeline.tick):
# This string is displayed in the timeline overview, for delimiting ticks
# by time, in milliseconds.
timeline.tick=%S ms
# LOCALIZATION NOTE (timeline.records):
# This string is displayed in the timeline waterfall, as a title for the menu.
timeline.records=RECORDS
# LOCALIZATION NOTE (profiler.bufferFull):
# This string is displayed when recording, indicating how much of the
# buffer is currently be used.
# %S is the percentage of the buffer used -- there are two "%"s after to escape
# the % that is actually displayed.
# Example: "Buffer 54% full"
profiler.bufferFull=Buffer %S%% full
# LOCALIZATION NOTE (recordings.start):
# The label shown on the main recording buttons to start recording.
recordings.start=Start Recording Performance
# LOCALIZATION NOTE (recordings.stop):
# The label shown on the main recording buttons to stop recording.
recordings.stop=Stop Recording Performance
# LOCALIZATION NOTE (recordings.start.tooltip):
# This string is displayed as a tooltip on a button that starts a new profile.
recordings.start.tooltip=Toggle the recording state of a performance recording.
# LOCALIZATION NOTE (recordings.import.tooltip):
# This string is displayed on a button that opens a dialog to import a saved profile data file.
recordings.import.tooltip=Import…
# LOCALIZATION NOTE (recordings.clear.tooltip):
# This string is displayed on a button that removes all the recordings.
recordings.clear.tooltip=Clear

View File

@ -19,7 +19,6 @@ DIRS += [
"locales",
"memory",
"netmonitor",
"performance",
"performance-new",
"preferences",
"responsive",

View File

@ -1,274 +0,0 @@
/* 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";
const { LocalizationHelper } = require("devtools/shared/l10n");
const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties";
const L10N = new LocalizationHelper(STRINGS_URI);
const { assert } = require("devtools/shared/DevToolsUtils");
const {
Component,
createFactory,
} = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const Tree = createFactory(
require("devtools/client/shared/components/VirtualizedTree")
);
const OptimizationsItem = createFactory(
require("devtools/client/performance/components/JITOptimizationsItem")
);
const FrameView = createFactory(
require("devtools/client/shared/components/Frame")
);
const JIT_TITLE = L10N.getStr("jit.title");
// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)`
// in `devtools/client/themes/jit-optimizations.css`
const TREE_ROW_HEIGHT = 14;
/* eslint-disable no-unused-vars */
/**
* TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully
* integrated as of yet, and this may represent intended functionality.
*/
const onClickTooltipString = frame =>
L10N.getFormatStr(
"viewsourceindebugger",
`${frame.source}:${frame.line}:${frame.column}`
);
/* eslint-enable no-unused-vars */
const optimizationAttemptModel = {
id: PropTypes.number.isRequired,
strategy: PropTypes.string.isRequired,
outcome: PropTypes.string.isRequired,
};
const optimizationObservedTypeModel = {
keyedBy: PropTypes.string.isRequired,
name: PropTypes.string,
location: PropTypes.string,
line: PropTypes.string,
};
const optimizationIonTypeModel = {
id: PropTypes.number.isRequired,
typeset: PropTypes.arrayOf(optimizationObservedTypeModel),
site: PropTypes.number.isRequired,
mirType: PropTypes.number.isRequired,
};
const optimizationSiteModel = {
id: PropTypes.number.isRequired,
propertyName: PropTypes.string,
line: PropTypes.number.isRequired,
column: PropTypes.number.isRequired,
data: PropTypes.shape({
attempts: PropTypes.arrayOf(optimizationAttemptModel).isRequired,
types: PropTypes.arrayOf(optimizationIonTypeModel).isRequired,
}).isRequired,
};
class JITOptimizations extends Component {
static get propTypes() {
return {
onViewSourceInDebugger: PropTypes.func.isRequired,
frameData: PropTypes.object.isRequired,
optimizationSites: PropTypes.arrayOf(optimizationSiteModel).isRequired,
autoExpandDepth: PropTypes.number,
};
}
static get defaultProps() {
return {
autoExpandDepth: 0,
};
}
constructor(props) {
super(props);
this.state = {
expanded: new Set(),
};
this._createHeader = this._createHeader.bind(this);
this._createTree = this._createTree.bind(this);
}
/**
* Frame data generated from `frameNode.getInfo()`, or an empty
* object, as well as a handler for clicking on the frame component.
*
* @param {?Object} .frameData
* @param {Function} .onViewSourceInDebugger
* @return {ReactElement}
*/
_createHeader({ frameData, onViewSourceInDebugger }) {
const { isMetaCategory, url, line } = frameData;
const name = isMetaCategory
? frameData.categoryData.label
: frameData.functionName || "";
// Neither Meta Category nodes, or the lack of a selected frame node,
// renders out a frame source, like "file.js:123"; so just use
// an empty span.
let frameComponent;
if (isMetaCategory || !name) {
frameComponent = dom.span();
} else {
frameComponent = FrameView({
frame: {
source: url,
line: +line,
functionDisplayName: name,
},
onClick: onViewSourceInDebugger,
});
}
return dom.div(
{ className: "optimization-header" },
dom.span({ className: "header-title" }, JIT_TITLE),
dom.span({ className: "header-function-name" }, name),
frameComponent
);
}
_createTree(props) {
const {
autoExpandDepth,
frameData,
onViewSourceInDebugger,
optimizationSites: sites,
} = this.props;
const getSite = id => sites.find(site => site.id === id);
const getIonTypeForObserved = type => {
return getSite(type.id).data.types.find(iontype =>
(iontype.typeset || []).includes(type)
);
};
const isSite = site => getSite(site.id) === site;
const isAttempts = attempts =>
getSite(attempts.id).data.attempts === attempts;
const isAttempt = attempt =>
getSite(attempt.id).data.attempts.includes(attempt);
const isTypes = types => getSite(types.id).data.types === types;
const isType = type => getSite(type.id).data.types.includes(type);
const isObservedType = type => getIonTypeForObserved(type);
const getRowType = node => {
if (isSite(node)) {
return "site";
}
if (isAttempts(node)) {
return "attempts";
}
if (isTypes(node)) {
return "types";
}
if (isAttempt(node)) {
return "attempt";
}
if (isType(node)) {
return "type";
}
if (isObservedType(node)) {
return "observedtype";
}
return null;
};
// Creates a unique key for each node in the
// optimizations data
const getKey = node => {
const site = getSite(node.id);
if (isSite(node)) {
return node.id;
} else if (isAttempts(node)) {
return `${node.id}-A`;
} else if (isTypes(node)) {
return `${node.id}-T`;
} else if (isType(node)) {
return `${node.id}-T-${site.data.types.indexOf(node)}`;
} else if (isAttempt(node)) {
return `${node.id}-A-${site.data.attempts.indexOf(node)}`;
} else if (isObservedType(node)) {
const iontype = getIonTypeForObserved(node);
return `${getKey(iontype)}-O-${iontype.typeset.indexOf(node)}`;
}
return "";
};
return Tree({
autoExpandDepth,
preventNavigationOnArrowRight: false,
getParent: node => {
const site = getSite(node.id);
let parent;
if (isAttempts(node) || isTypes(node)) {
parent = site;
} else if (isType(node)) {
parent = site.data.types;
} else if (isAttempt(node)) {
parent = site.data.attempts;
} else if (isObservedType(node)) {
parent = getIonTypeForObserved(node);
}
assert(parent, "Could not find a parent for optimization data node");
return parent;
},
getChildren: node => {
if (isSite(node)) {
return [node.data.types, node.data.attempts];
} else if (isAttempts(node) || isTypes(node)) {
return node;
} else if (isType(node)) {
return node.typeset || [];
}
return [];
},
isExpanded: node => this.state.expanded.has(node),
onExpand: node =>
this.setState(state => {
const expanded = new Set(state.expanded);
expanded.add(node);
return { expanded };
}),
onCollapse: node =>
this.setState(state => {
const expanded = new Set(state.expanded);
expanded.delete(node);
return { expanded };
}),
onFocus: function() {},
getKey,
getRoots: () => sites || [],
itemHeight: TREE_ROW_HEIGHT,
renderItem: (item, depth, focused, arrow, expanded) =>
new OptimizationsItem({
onViewSourceInDebugger,
item,
depth,
focused,
arrow,
expanded,
type: getRowType(item),
frameData,
}),
});
}
render() {
const header = this._createHeader(this.props);
const tree = this._createTree(this.props);
return dom.div({}, header, tree);
}
}
module.exports = JITOptimizations;

View File

@ -1,228 +0,0 @@
/* 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";
const { LocalizationHelper } = require("devtools/shared/l10n");
const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties";
const L10N = new LocalizationHelper(STRINGS_URI);
const { PluralForm } = require("devtools/shared/plural-form");
const {
Component,
createFactory,
} = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const Frame = createFactory(require("devtools/client/shared/components/Frame"));
const PROPNAME_MAX_LENGTH = 4;
// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)`
// in `devtools/client/themes/jit-optimizations.css`
const TREE_ROW_HEIGHT = 14;
const OPTIMIZATION_ITEM_TYPES = [
"site",
"attempts",
"types",
"attempt",
"type",
"observedtype",
];
/* eslint-disable no-unused-vars */
/**
* TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully
* integrated as of yet.
*/
const {
JITOptimizations,
hasSuccessfulOutcome,
isSuccessfulOutcome,
} = require("devtools/client/performance/modules/logic/jit");
const OPTIMIZATION_FAILURE = L10N.getStr("jit.optimizationFailure");
const JIT_SAMPLES = L10N.getStr("jit.samples");
const JIT_TYPES = L10N.getStr("jit.types");
const JIT_ATTEMPTS = L10N.getStr("jit.attempts");
/* eslint-enable no-unused-vars */
class JITOptimizationsItem extends Component {
static get propTypes() {
return {
onViewSourceInDebugger: PropTypes.func.isRequired,
frameData: PropTypes.object.isRequired,
type: PropTypes.oneOf(OPTIMIZATION_ITEM_TYPES).isRequired,
depth: PropTypes.number.isRequired,
arrow: PropTypes.element.isRequired,
item: PropTypes.object,
focused: PropTypes.bool,
};
}
constructor(props) {
super(props);
this._renderSite = this._renderSite.bind(this);
this._renderAttempts = this._renderAttempts.bind(this);
this._renderTypes = this._renderTypes.bind(this);
this._renderAttempt = this._renderAttempt.bind(this);
this._renderType = this._renderType.bind(this);
this._renderObservedType = this._renderObservedType.bind(this);
}
_renderSite({ item: site, onViewSourceInDebugger, frameData }) {
const attempts = site.data.attempts;
const lastStrategy = attempts[attempts.length - 1].strategy;
let propString = "";
const propertyName = site.data.propertyName;
// Display property name if it exists
if (propertyName) {
if (propertyName.length > PROPNAME_MAX_LENGTH) {
propString = ` (.${propertyName.substr(0, PROPNAME_MAX_LENGTH)}…)`;
} else {
propString = ` (.${propertyName})`;
}
}
const sampleString = PluralForm.get(site.samples, JIT_SAMPLES).replace(
"#1",
site.samples
);
const text = dom.span(
{ className: "optimization-site-title" },
`${lastStrategy}${propString} (${sampleString})`
);
const frame = Frame({
onClick: onViewSourceInDebugger,
frame: {
source: frameData.url,
line: +site.data.line,
column: site.data.column,
},
});
const children = [text, frame];
if (!hasSuccessfulOutcome(site)) {
children.unshift(dom.span({ className: "opt-icon warning" }));
}
return dom.span({ className: "optimization-site" }, ...children);
}
_renderAttempts({ item: attempts }) {
return dom.span(
{ className: "optimization-attempts" },
`${JIT_ATTEMPTS} (${attempts.length})`
);
}
_renderTypes({ item: types }) {
return dom.span(
{ className: "optimization-types" },
`${JIT_TYPES} (${types.length})`
);
}
_renderAttempt({ item: attempt }) {
const success = isSuccessfulOutcome(attempt.outcome);
const { strategy, outcome } = attempt;
return dom.span(
{ className: "optimization-attempt" },
dom.span({ className: "optimization-strategy" }, strategy),
" → ",
dom.span(
{
className: `optimization-outcome ${success ? "success" : "failure"}`,
},
outcome
)
);
}
_renderType({ item: type }) {
return dom.span(
{ className: "optimization-ion-type" },
`${type.site}:${type.mirType}`
);
}
_renderObservedType({ onViewSourceInDebugger, item: type }) {
const children = [
dom.span(
{ className: "optimization-observed-type-keyed" },
`${type.keyedBy}${type.name ? `${type.name}` : ""}`
),
];
// If we have a line and location, make a link to the debugger
if (type.location && type.line) {
children.push(
Frame({
onClick: onViewSourceInDebugger,
frame: {
source: type.location,
line: type.line,
column: type.column,
},
})
);
// Otherwise if we just have a location, it's probably just a memory location.
} else if (type.location) {
children.push(`@${type.location}`);
}
return dom.span({ className: "optimization-observed-type" }, ...children);
}
render() {
/* eslint-disable no-unused-vars */
/**
* TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and these
* undefined variables may represent intended functionality.
*/
const {
depth,
arrow,
type,
// TODO - The following are currently unused.
item,
focused,
frameData,
onViewSourceInDebugger,
} = this.props;
/* eslint-enable no-unused-vars */
let content;
switch (type) {
case "site":
content = this._renderSite(this.props);
break;
case "attempts":
content = this._renderAttempts(this.props);
break;
case "types":
content = this._renderTypes(this.props);
break;
case "attempt":
content = this._renderAttempt(this.props);
break;
case "type":
content = this._renderType(this.props);
break;
case "observedtype":
content = this._renderObservedType(this.props);
break;
}
return dom.div(
{
className: `optimization-tree-item optimization-tree-item-${type}`,
style: { marginInlineStart: depth * TREE_ROW_HEIGHT },
},
arrow,
content
);
}
}
module.exports = JITOptimizationsItem;

View File

@ -1,45 +0,0 @@
/* 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";
const { L10N } = require("devtools/client/performance/modules/global");
const { Component } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { button } = dom;
class RecordingButton extends Component {
static get propTypes() {
return {
onRecordButtonClick: PropTypes.func.isRequired,
isRecording: PropTypes.bool,
isLocked: PropTypes.bool,
};
}
render() {
const { onRecordButtonClick, isRecording, isLocked } = this.props;
const classList = ["devtools-button", "record-button"];
if (isRecording) {
classList.push("checked");
}
return button(
{
className: classList.join(" "),
onClick: onRecordButtonClick,
"data-standalone": "true",
"data-text-only": "true",
disabled: isLocked,
},
isRecording
? L10N.getStr("recordings.stop")
: L10N.getStr("recordings.start")
);
}
}
module.exports = RecordingButton;

View File

@ -1,63 +0,0 @@
/* 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";
const { L10N } = require("devtools/client/performance/modules/global");
const { Component } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { div, button } = dom;
class RecordingControls extends Component {
static get propTypes() {
return {
onClearButtonClick: PropTypes.func.isRequired,
onRecordButtonClick: PropTypes.func.isRequired,
onImportButtonClick: PropTypes.func.isRequired,
isRecording: PropTypes.bool,
isLocked: PropTypes.bool,
};
}
render() {
const {
onClearButtonClick,
onRecordButtonClick,
onImportButtonClick,
isRecording,
isLocked,
} = this.props;
const recordButtonClassList = ["devtools-button", "record-button"];
if (isRecording) {
recordButtonClassList.push("checked");
}
return div(
{ className: "devtools-toolbar" },
button({
id: "clear-button",
className: "devtools-button",
title: L10N.getStr("recordings.clear.tooltip"),
onClick: onClearButtonClick,
}),
button({
id: "main-record-button",
className: recordButtonClassList.join(" "),
disabled: isLocked,
title: L10N.getStr("recordings.start.tooltip"),
onClick: onRecordButtonClick,
}),
button({
id: "import-button",
className: "devtools-button",
title: L10N.getStr("recordings.import.tooltip"),
onClick: onImportButtonClick,
})
);
}
}
module.exports = RecordingControls;

View File

@ -1,32 +0,0 @@
/* 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";
const { Component } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { L10N } = require("devtools/client/performance/modules/global");
const { ul, div } = dom;
class RecordingList extends Component {
static get propTypes() {
return {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
itemComponent: PropTypes.func.isRequired,
};
}
render() {
const { items, itemComponent: Item } = this.props;
return items.length > 0
? ul({ className: "recording-list" }, ...items.map(Item))
: div(
{ className: "recording-list-empty" },
L10N.getStr("noRecordingsText")
);
}
}
module.exports = RecordingList;

View File

@ -1,65 +0,0 @@
/* 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";
const { Component } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { div, li, span, button } = dom;
const { L10N } = require("devtools/client/performance/modules/global");
class RecordingListItem extends Component {
static get propTypes() {
return {
label: PropTypes.string.isRequired,
duration: PropTypes.string,
onSelect: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
isLoading: PropTypes.bool,
isSelected: PropTypes.bool,
isRecording: PropTypes.bool,
};
}
render() {
const {
label,
duration,
onSelect,
onSave,
isLoading,
isSelected,
isRecording,
} = this.props;
const className = `recording-list-item ${isSelected ? "selected" : ""}`;
let durationText;
if (isLoading) {
durationText = L10N.getStr("recordingsList.loadingLabel");
} else if (isRecording) {
durationText = L10N.getStr("recordingsList.recordingLabel");
} else {
durationText = L10N.getFormatStr(
"recordingsList.durationLabel",
duration
);
}
return li(
{ className, onClick: onSelect },
div({ className: "recording-list-item-label" }, label),
div(
{ className: "recording-list-item-footer" },
span({ className: "recording-list-item-duration" }, durationText),
button(
{ className: "recording-list-item-save", onClick: onSave },
L10N.getStr("recordingsList.saveLabel")
)
)
);
}
}
module.exports = RecordingListItem;

View File

@ -1,42 +0,0 @@
/* 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";
/**
* This file contains the "waterfall" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const { createFactory } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const WaterfallHeader = createFactory(
require("devtools/client/performance/components/WaterfallHeader")
);
const WaterfallTree = createFactory(
require("devtools/client/performance/components/WaterfallTree")
);
function Waterfall(props) {
return dom.div(
{ className: "waterfall-markers" },
WaterfallHeader(props),
WaterfallTree(props)
);
}
Waterfall.displayName = "Waterfall";
Waterfall.propTypes = {
marker: PropTypes.object.isRequired,
startTime: PropTypes.number.isRequired,
endTime: PropTypes.number.isRequired,
dataScale: PropTypes.number.isRequired,
sidebarWidth: PropTypes.number.isRequired,
waterfallWidth: PropTypes.number.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
};
module.exports = Waterfall;

View File

@ -1,73 +0,0 @@
/* 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";
/**
* The "waterfall ticks" view, a header for the markers displayed in the waterfall.
*/
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { L10N } = require("devtools/client/performance/modules/global");
const {
TickUtils,
} = require("devtools/client/performance/modules/waterfall-ticks");
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
function WaterfallHeader(props) {
const { startTime, dataScale, sidebarWidth, waterfallWidth } = props;
const tickInterval = TickUtils.findOptimalTickInterval({
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale,
});
const ticks = [];
for (let x = 0; x < waterfallWidth; x += tickInterval) {
const left = x + WATERFALL_HEADER_TEXT_PADDING;
const time = Math.round(x / dataScale + startTime);
const label = L10N.getFormatStr("timeline.tick", time);
const node = dom.div(
{
key: x,
className: "plain waterfall-header-tick",
style: { transform: `translateX(${left}px)` },
},
label
);
ticks.push(node);
}
return dom.div(
{ className: "waterfall-header" },
dom.div(
{
className: "waterfall-sidebar theme-sidebar waterfall-header-name",
style: { width: sidebarWidth + "px" },
},
L10N.getStr("timeline.records")
),
dom.div(
{ className: "waterfall-header-ticks waterfall-background-ticks" },
ticks
)
);
}
WaterfallHeader.displayName = "WaterfallHeader";
WaterfallHeader.propTypes = {
startTime: PropTypes.number.isRequired,
dataScale: PropTypes.number.isRequired,
sidebarWidth: PropTypes.number.isRequired,
waterfallWidth: PropTypes.number.isRequired,
};
module.exports = WaterfallHeader;

View File

@ -1,189 +0,0 @@
/* 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";
const {
Component,
createFactory,
} = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const Tree = createFactory(
require("devtools/client/shared/components/VirtualizedTree")
);
const WaterfallTreeRow = createFactory(
require("devtools/client/performance/components/WaterfallTreeRow")
);
// Keep in sync with var(--waterfall-tree-row-height) in performance.css
const WATERFALL_TREE_ROW_HEIGHT = 15; // px
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
const mStart = e.start | 0;
const mEnd = e.end | 0;
return (
// bounds inside
(mStart >= start && mEnd <= end) ||
// bounds outside
(mStart < start && mEnd > end) ||
// overlap start
(mStart < start && mEnd >= start && mEnd <= end) ||
// overlap end
(mEnd > end && mStart >= start && mStart <= end)
);
}
class WaterfallTree extends Component {
static get propTypes() {
return {
marker: PropTypes.object.isRequired,
startTime: PropTypes.number.isRequired,
endTime: PropTypes.number.isRequired,
dataScale: PropTypes.number.isRequired,
sidebarWidth: PropTypes.number.isRequired,
waterfallWidth: PropTypes.number.isRequired,
onFocus: PropTypes.func,
};
}
constructor(props) {
super(props);
this.state = {
focused: null,
expanded: new Set(),
};
this._getRoots = this._getRoots.bind(this);
this._getParent = this._getParent.bind(this);
this._getChildren = this._getChildren.bind(this);
this._getKey = this._getKey.bind(this);
this._isExpanded = this._isExpanded.bind(this);
this._onExpand = this._onExpand.bind(this);
this._onCollapse = this._onCollapse.bind(this);
this._onFocus = this._onFocus.bind(this);
this._filter = this._filter.bind(this);
this._renderItem = this._renderItem.bind(this);
}
_getRoots(node) {
const roots = this.props.marker.submarkers || [];
return roots.filter(this._filter);
}
/**
* Find the parent node of 'node' with a depth-first search of the marker tree
*/
_getParent(node) {
function findParent(marker) {
if (marker.submarkers) {
for (const submarker of marker.submarkers) {
if (submarker === node) {
return marker;
}
const parent = findParent(submarker);
if (parent) {
return parent;
}
}
}
return null;
}
const rootMarker = this.props.marker;
const parent = findParent(rootMarker);
// We are interested only in parent markers that are rendered,
// which rootMarker is not. Return null if the parent is rootMarker.
return parent !== rootMarker ? parent : null;
}
_getChildren(node) {
const submarkers = node.submarkers || [];
return submarkers.filter(this._filter);
}
_getKey(node) {
return `marker-${node.index}`;
}
_isExpanded(node) {
return this.state.expanded.has(node);
}
_onExpand(node) {
this.setState(state => {
const expanded = new Set(state.expanded);
expanded.add(node);
return { expanded };
});
}
_onCollapse(node) {
this.setState(state => {
const expanded = new Set(state.expanded);
expanded.delete(node);
return { expanded };
});
}
_onFocus(node) {
this.setState({ focused: node });
if (this.props.onFocus) {
this.props.onFocus(node);
}
}
_filter(node) {
const { startTime, endTime } = this.props;
return isMarkerInRange(node, startTime, endTime);
}
_renderItem(marker, level, focused, arrow, expanded) {
const { startTime, dataScale, sidebarWidth } = this.props;
return WaterfallTreeRow({
marker,
level,
arrow,
expanded,
focused,
startTime,
dataScale,
sidebarWidth,
});
}
render() {
return Tree({
preventNavigationOnArrowRight: false,
getRoots: this._getRoots,
getParent: this._getParent,
getChildren: this._getChildren,
getKey: this._getKey,
isExpanded: this._isExpanded,
onExpand: this._onExpand,
onCollapse: this._onCollapse,
onFocus: this._onFocus,
renderItem: this._renderItem,
focused: this.state.focused,
itemHeight: WATERFALL_TREE_ROW_HEIGHT,
});
}
}
module.exports = WaterfallTree;

View File

@ -1,116 +0,0 @@
/* 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";
/**
* A single row (node) in the waterfall tree
*/
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const {
MarkerBlueprintUtils,
} = require("devtools/client/performance/modules/marker-blueprint-utils");
const LEVEL_INDENT = 10; // px
const ARROW_NODE_OFFSET = -14; // px
const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5; // px
function buildMarkerSidebar(blueprint, props) {
const { marker, level, sidebarWidth } = props;
const bullet = dom.div({
className: `waterfall-marker-bullet marker-color-${blueprint.colorName}`,
style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
"data-type": marker.name,
});
const label = MarkerBlueprintUtils.getMarkerLabel(marker);
const name = dom.div(
{
className: "plain waterfall-marker-name",
style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
title: label,
},
label
);
return dom.div(
{
className: "waterfall-sidebar theme-sidebar",
style: { width: sidebarWidth + "px" },
},
bullet,
name
);
}
function buildMarkerTimebar(blueprint, props) {
const { marker, startTime, dataScale, arrow } = props;
const offset = (marker.start - startTime) * dataScale + ARROW_NODE_OFFSET;
const width = Math.max(
(marker.end - marker.start) * dataScale,
WATERFALL_MARKER_TIMEBAR_WIDTH_MIN
);
const bar = dom.div(
{
className: "waterfall-marker-wrap",
style: { transform: `translateX(${offset}px)` },
},
arrow,
dom.div({
className: `waterfall-marker-bar marker-color-${blueprint.colorName}`,
style: { width: `${width}px` },
"data-type": marker.name,
})
);
return dom.div(
{ className: "waterfall-marker waterfall-background-ticks" },
bar
);
}
function WaterfallTreeRow(props) {
const { marker, focused } = props;
const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
const attrs = {
className: "waterfall-tree-item" + (focused ? " focused" : ""),
"data-otmt": marker.isOffMainThread,
};
// Don't render an expando-arrow for leaf nodes.
const submarkers = marker.submarkers;
const hasDescendants = submarkers && submarkers.length > 0;
if (hasDescendants) {
attrs["data-expandable"] = "";
} else {
attrs["data-invisible"] = "";
}
return dom.div(
attrs,
buildMarkerSidebar(blueprint, props),
buildMarkerTimebar(blueprint, props)
);
}
WaterfallTreeRow.displayName = "WaterfallTreeRow";
WaterfallTreeRow.propTypes = {
marker: PropTypes.object.isRequired,
level: PropTypes.number.isRequired,
arrow: PropTypes.element.isRequired,
expanded: PropTypes.bool.isRequired,
focused: PropTypes.bool.isRequired,
startTime: PropTypes.number.isRequired,
dataScale: PropTypes.number.isRequired,
sidebarWidth: PropTypes.number.isRequired,
};
module.exports = WaterfallTreeRow;

View File

@ -1,5 +0,0 @@
[DEFAULT]
support-files =
head.js
[test_jit_optimizations_01.html]

View File

@ -1,246 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
yield new Promise(function(){});
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global window, document, SimpleTest, requestAnimationFrame, is, ok */
/* exported Cc, Ci, Cu, Cr, Assert, Task, Toolbox, browserRequire,
forceRender, setProps, dumpn, checkOptimizationHeader, checkOptimizationTree */
const { require } = ChromeUtils.import(
"resource://devtools/shared/loader/Loader.jsm"
);
let { Assert } = require("resource://testing-common/Assert.jsm");
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/shared/loader/browser-loader.js"
);
const flags = require("devtools/shared/flags");
let { Toolbox } = require("devtools/client/framework/toolbox");
let { require: browserRequire } = BrowserLoader({
baseURI: "resource://devtools/client/performance/",
window,
});
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const $ = (selector, scope = document) => scope.querySelector(selector);
const $$ = (selector, scope = document) => scope.querySelectorAll(selector);
function forceRender(comp) {
return setState(comp, {}).then(() => setState(comp, {}));
}
// All tests are asynchronous.
SimpleTest.waitForExplicitFinish();
function onNextAnimationFrame(fn) {
return () => requestAnimationFrame(() => requestAnimationFrame(fn));
}
function setState(component, newState) {
return new Promise(resolve => {
component.setState(newState, onNextAnimationFrame(resolve));
});
}
function setProps(component, newState) {
return new Promise(resolve => {
component.setProps(newState, onNextAnimationFrame(resolve));
});
}
function dumpn(msg) {
dump(`PERFORMANCE-COMPONENT-TEST: ${msg}\n`);
}
/**
* Default opts data for testing. First site has a simple IonType,
* and an IonType with an ObservedType, and a successful outcome.
* Second site does not have a successful outcome.
*/
const OPTS_DATA_GENERAL = [
{
id: 1,
propertyName: "my property name",
line: 100,
column: 200,
samples: 90,
data: {
attempts: [
{
id: 1,
strategy: "GetElem_TypedObject",
outcome: "AccessNotTypedObject",
},
{ id: 1, strategy: "GetElem_Dense", outcome: "AccessNotDense" },
{ id: 1, strategy: "GetElem_TypedStatic", outcome: "Disabled" },
{ id: 1, strategy: "GetElem_TypedArray", outcome: "GenericSuccess" },
],
types: [
{
id: 1,
site: "Receiver",
mirType: "Object",
typeset: [
{
id: 1,
keyedBy: "constructor",
name: "MyView",
location: "http://internet.com/file.js",
line: "123",
},
],
},
{
id: 1,
typeset: void 0,
site: "Index",
mirType: "Int32",
},
],
},
},
{
id: 2,
propertyName: void 0,
line: 50,
column: 51,
samples: 100,
data: {
attempts: [
{ id: 2, strategy: "Call_Inline", outcome: "CantInlineBigData" },
],
types: [
{
id: 2,
site: "Call_Target",
mirType: "Object",
typeset: [
{ id: 2, keyedBy: "primitive" },
{
id: 2,
keyedBy: "constructor",
name: "B",
location: "http://mypage.com/file.js",
line: "2",
},
{
id: 2,
keyedBy: "constructor",
name: "C",
location: "http://mypage.com/file.js",
line: "3",
},
{
id: 2,
keyedBy: "constructor",
name: "D",
location: "http://mypage.com/file.js",
line: "4",
},
],
},
],
},
},
];
OPTS_DATA_GENERAL.forEach(site => {
site.data.types.forEach(type => {
if (type.typeset) {
type.typeset.id = site.id;
}
});
site.data.attempts.id = site.id;
site.data.types.id = site.id;
});
function checkOptimizationHeader(name, file, line) {
is(
$(".optimization-header .header-function-name").textContent,
name,
"correct optimization header function name"
);
is(
$(".optimization-header .frame-link-filename").textContent,
file,
"correct optimization header file name"
);
is(
$(".optimization-header .frame-link-line").textContent,
`:${line}`,
"correct optimization header line"
);
}
function checkOptimizationTree(rowData) {
const rows = $$(".tree .tree-node");
for (let i = 0; i < rowData.length; i++) {
const row = rows[i];
const expected = rowData[i];
switch (expected.type) {
case "site":
is(
$(".optimization-site-title", row).textContent,
`${expected.strategy} (${expected.samples} samples)`,
`row ${i}th: correct optimization site row`
);
is(
!!$(".opt-icon.warning", row),
!!expected.failureIcon,
`row ${i}th: expected visibility of failure icon for unsuccessful outcomes`
);
break;
case "types":
is(
$(".optimization-types", row).textContent,
`Types (${expected.count})`,
`row ${i}th: correct types row`
);
break;
case "attempts":
is(
$(".optimization-attempts", row).textContent,
`Attempts (${expected.count})`,
`row ${i}th: correct attempts row`
);
break;
case "type":
is(
$(".optimization-ion-type", row).textContent,
`${expected.site}:${expected.mirType}`,
`row ${i}th: correct ion type row`
);
break;
case "observedtype":
is(
$(".optimization-observed-type-keyed", row).textContent,
expected.name
? `${expected.keyedBy}${expected.name}`
: expected.keyedBy,
`row ${i}th: correct observed type row`
);
break;
case "attempt":
is(
$(".optimization-strategy", row).textContent,
expected.strategy,
`row ${i}th: correct attempt row, attempt item`
);
is(
$(".optimization-outcome", row).textContent,
expected.outcome,
`row ${i}th: correct attempt row, outcome item`
);
ok(
$(".optimization-outcome", row).classList.contains(
expected.success ? "success" : "failure"
),
`row ${i}th: correct attempt row, failure/success status`
);
break;
}
}
}

View File

@ -1,70 +0,0 @@
<!DOCTYPE HTML>
<html>
<!--
Test the rendering of the JIT Optimizations tree. Tests when jit data has observed types, multiple observed types, multiple sites, a site with a successful strategy, site with no successful strategy.
-->
<head>
<meta charset="utf-8">
<title>JITOptimizations component test</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body style="height: 10000px;">
<pre id="test">
<script src="head.js" type="application/javascript"></script>
<script type="application/javascript">
"use strict";
window.onload = async function () {
try {
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
const React = browserRequire("devtools/client/shared/vendor/react");
const JITOptimizations = React.createFactory(browserRequire("devtools/client/performance/components/JITOptimizations"));
ok(JITOptimizations, "Should get JITOptimizations");
const opts = ReactDOM.render(JITOptimizations({
onViewSourceInDebugger: function(){},
frameData: {
isMetaCategory: false,
url: "http://internet.com/file.js",
line: 1,
functionName: "myfunc",
},
optimizationSites: OPTS_DATA_GENERAL,
autoExpandDepth: 1000,
}), window.document.body);
await forceRender(opts);
checkOptimizationHeader("myfunc", "file.js", "1");
checkOptimizationTree([
{ type: "site", strategy: "GetElem_TypedArray", samples: "90" },
{ type: "types", count: "2" },
{ type: "type", site: "Receiver", mirType: "Object" },
{ type: "observedtype", keyedBy: "constructor", name: "MyView" },
{ type: "type", site: "Index", mirType: "Int32" },
{ type: "attempts", count: "4" },
{ type: "attempt", strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" },
{ type: "attempt", strategy: "GetElem_Dense", outcome: "AccessNotDense" },
{ type: "attempt", strategy: "GetElem_TypedStatic", outcome: "Disabled" },
{ type: "attempt", strategy: "GetElem_TypedArray", outcome: "GenericSuccess", success: true },
{ type: "site", strategy: "Call_Inline", samples: "100", failureIcon: true },
{ type: "types", count: "1" },
{ type: "type", site: "Call_Target", mirType: "Object" },
{ type: "observedtype", keyedBy: "primitive" },
{ type: "observedtype", keyedBy: "constructor", name: "B" },
{ type: "observedtype", keyedBy: "constructor", name: "C" },
{ type: "observedtype", keyedBy: "constructor", name: "D" },
{ type: "attempts", count: "1" },
{ type: "attempt", strategy: "Call_Inline", outcome: "CantInlineBigData" },
]);
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
};
</script>
</pre>
</body>
</html>

View File

@ -1,19 +0,0 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
"JITOptimizations.js",
"JITOptimizationsItem.js",
"RecordingButton.js",
"RecordingControls.js",
"RecordingList.js",
"RecordingListItem.js",
"Waterfall.js",
"WaterfallHeader.js",
"WaterfallTree.js",
"WaterfallTreeRow.js",
)
MOCHITEST_CHROME_MANIFESTS += ["chrome/chrome.ini"]

View File

@ -1,171 +0,0 @@
# Timeline Markers
## Common
* DOMHighResTimeStamp start
* DOMHighResTimeStamp end
* DOMString name
* object? stack
* object? endStack
* unsigned short processType;
* boolean isOffMainThread;
The `processType` a GeckoProcessType enum listed in xpcom/build/nsXULAppAPI.h,
specifying if this marker originates in a content, chrome, plugin etc. process.
Additionally, markers may be created from any thread on those processes, and
`isOffMainThread` highights whether or not they're from the main thread. The most
common type of marker is probably going to be from a GeckoProcessType_Content's
main thread when debugging content.
## DOMEvent
Triggered when a DOM event occurs, like a click or a keypress.
* unsigned short eventPhase - a number indicating what phase this event is
in (target, bubbling, capturing, maps to Event constants)
* DOMString type - the type of event, like "keypress" or "click"
## Reflow
Reflow markers (labeled as "Layout") indicate when a change has occurred to
a DOM element's positioning that requires the frame tree (rendering
representation of the DOM) to figure out the new position of a handful of
elements. Fired via `PresShell::DoReflow`
## Paint
* sequence<{ long height, long width, long x, long y }> rectangles - An array
of rectangle objects indicating where painting has occurred.
## Styles
Style markers (labeled as "Recalculating Styles") are triggered when Gecko
needs to figure out the computational style of an element. Fired via
`RestyleTracker::DoProcessRestyles` when there are elements to restyle.
## Javascript
`Javascript` markers are emitted indicating when JS execution begins and ends,
with a reason that triggered it (causeName), like a requestAnimationFrame or
a setTimeout.
* string causeName - The reason that JS was entered. There are many possible
reasons, and the interesting ones to show web developers (triggered by content) are:
* "\<script\> element"
* "EventListener.handleEvent"
* "setInterval handler"
* "setTimeout handler"
* "FrameRequestCallback"
* "EventHandlerNonNull"
* "promise callback"
* "promise initializer"
* "Worker runnable"
There are also many more potential JS causes, some which are just internally
used and won't emit a marker, but the below ones are only of interest to
Gecko hackers, most likely
* "promise thenable"
* "worker runnable"
* "nsHTTPIndex set HTTPIndex property"
* "XPCWrappedJS method call"
* "nsHTTPIndex OnFTPControlLog"
* "XPCWrappedJS QueryInterface"
* "xpcshell argument processing”
* "XPConnect sandbox evaluation "
* "component loader report global"
* "component loader load module"
* "Cross-Process Object Wrapper call/construct"
* "Cross-Process Object Wrapper set'"
* "Cross-Process Object Wrapper 'get'"
* "nsXULTemplateBuilder creation"
* "TestShellCommand"
* "precompiled XUL \<script\> element"
* "XBL \<field\> initialization "
* "NPAPI NPN_evaluate"
* "NPAPI get"
* "NPAPI set"
* "NPAPI doInvoke"
* "javascript: URI"
* "geolocation.always_precise indexing"
* "geolocation.app_settings enumeration"
* "WebIDL dictionary creation"
* "XBL \<constructor\>/\<destructor\> invocation"
* "message manager script load"
* "message handler script load"
* "nsGlobalWindow report new global"
## GarbageCollection
Emitted after a full GC cycle has completed (which is after any number of
incremental slices).
* DOMString causeName - The reason for a GC event to occur. A full list of
GC reasons can be found in [docs](https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.memory/).
* DOMString nonincremenetalReason - If the GC could not do an incremental
GC (smaller, quick GC events), and we have to walk the entire heap and
GC everything marked, then the reason listed here is why.
## nsCycleCollector::Collect
An `nsCycleCollector::Collect` marker is emitted for each incremental cycle
collection slice and each non-incremental cycle collection.
# nsCycleCollector::ForgetSkippable
`nsCycleCollector::ForgetSkippable` is presented as "Cycle Collection", but in
reality it is preparation/pre-optimization for cycle collection, and not the
actual tracing of edges and collecting of cycles.
## ConsoleTime
A marker generated via `console.time()` and `console.timeEnd()`.
* DOMString causeName - the label passed into `console.time(label)` and
`console.timeEnd(label)` if passed in.
## TimeStamp
A marker generated via `console.timeStamp(label)`.
* DOMString causeName - the label passed into `console.timeStamp(label)`
if passed in.
## document::DOMContentLoaded
A marker generated when the DOMContentLoaded event is fired.
## document::Load
A marker generated when the document's "load" event is fired.
## Parse HTML
## Parse XML
## Worker
Emitted whenever there's an operation dealing with Workers (any kind of worker,
Web Workers, Service Workers etc.). Currently there are 4 types of operations
being tracked: serializing/deserializing data on the main thread, and also
serializing/deserializing data off the main thread.
* ProfileTimelineWorkerOperationType operationType - the type of operation
being done by the Worker or the main thread when dealing with workers.
Can be one of the following enums defined in ProfileTimelineMarker.webidl
* "serializeDataOffMainThread"
* "serializeDataOnMainThread"
* "deserializeDataOffMainThread"
* "deserializeDataOnMainThread"
## Composite
Composite markers trace the actual time an inner composite operation
took on the compositor thread. Currently, these markers are only especially
interesting for Gecko platform developers, and thus disabled by default.
## CompositeForwardTransaction
Markers generated when the IPC request was made to the compositor from
the child process's main thread.

View File

@ -1,110 +0,0 @@
/* 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";
const ControllerEvents = {
// Fired when a performance pref changes (either because the user changed it
// via the tool's UI, by changing something about:config or programatically).
PREF_CHANGED: "Performance:PrefChanged",
// Fired when the devtools theme changes.
THEME_CHANGED: "Performance:ThemeChanged",
// When a new recording model is received by the controller.
RECORDING_ADDED: "Performance:RecordingAdded",
// When a recording model gets removed from the controller.
RECORDING_DELETED: "Performance:RecordingDeleted",
// When a recording model becomes "started", "stopping" or "stopped".
RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
// When a recording is offering information on the profiler's circular buffer.
RECORDING_PROFILER_STATUS_UPDATE: "Performance:RecordingProfilerStatusUpdate",
// When a recording model becomes marked as selected.
RECORDING_SELECTED: "Performance:RecordingSelected",
// When starting a recording is attempted and fails because the backend
// does not permit it at this time.
BACKEND_FAILED_AFTER_RECORDING_START:
"Performance:BackendFailedRecordingStart",
// When a recording is started and the backend has started working.
BACKEND_READY_AFTER_RECORDING_START: "Performance:BackendReadyRecordingStart",
// When a recording is stopped and the backend has finished cleaning up.
BACKEND_READY_AFTER_RECORDING_STOP: "Performance:BackendReadyRecordingStop",
// When a recording is exported.
RECORDING_EXPORTED: "Performance:RecordingExported",
// When a recording is imported.
RECORDING_IMPORTED: "Performance:RecordingImported",
// When a source is shown in the JavaScript Debugger at a specific location.
SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
// Fired by the PerformanceController when `populateWithRecordings` is finished.
RECORDINGS_SEEDED: "Performance:RecordingsSeeded",
};
const ViewEvents = {
// Emitted by the `ToolbarView` when a preference changes.
UI_PREF_CHANGED: "Performance:UI:PrefChanged",
// When the state (display mode) changes, for example when switching between
// "empty", "recording" or "recorded". This causes certain parts of the UI
// to be hidden or visible.
UI_STATE_CHANGED: "Performance:UI:StateChanged",
// Emitted by the `PerformanceView` on clear button click.
UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings",
// Emitted by the `PerformanceView` on record button click.
UI_START_RECORDING: "Performance:UI:StartRecording",
UI_STOP_RECORDING: "Performance:UI:StopRecording",
// Emitted by the `PerformanceView` on import/export button click.
UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
// Emitted by the `PerformanceView` when the profiler's circular buffer
// status has been rendered.
UI_RECORDING_PROFILER_STATUS_RENDERED:
"Performance:UI:RecordingProfilerStatusRendered",
// When a recording is selected in the UI.
UI_RECORDING_SELECTED: "Performance:UI:RecordingSelected",
// Emitted by the `DetailsView` when a subview is selected
UI_DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
// Emitted by the `OverviewView` after something has been rendered.
UI_OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
UI_MARKERS_GRAPH_RENDERED: "Performance:UI:OverviewMarkersRendered",
UI_MEMORY_GRAPH_RENDERED: "Performance:UI:OverviewMemoryRendered",
UI_FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered",
// Emitted by the `OverviewView` when a range has been selected in the graphs.
UI_OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected",
// Emitted by the `WaterfallView` when it has been rendered.
UI_WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
// Emitted by the `JsCallTreeView` when it has been rendered.
UI_JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered",
// Emitted by the `JsFlameGraphView` when it has been rendered.
UI_JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered",
// Emitted by the `MemoryCallTreeView` when it has been rendered.
UI_MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered",
// Emitted by the `MemoryFlameGraphView` when it has been rendered.
UI_MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered",
};
module.exports = Object.assign({}, ControllerEvents, ViewEvents);

View File

@ -1,354 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/content/shared/toolbarbutton.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/performance.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/jit-optimizations.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/components-frame.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % performanceDTD SYSTEM "chrome://devtools/locale/performance.dtd">
%performanceDTD;
]>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<script src="chrome://devtools/content/shared/theme-switching.js"/>
<script src="resource://devtools/client/performance/initializer.js"/>
<popupset id="performance-options-popupset">
<menupopup id="performance-filter-menupopup" position="before_start"/>
<menupopup id="performance-options-menupopup" position="before_end">
<menuitem id="option-show-platform-data"
type="checkbox"
data-pref="show-platform-data"
label="&performanceUI.showPlatformData;"
tooltiptext="&performanceUI.showPlatformData.tooltiptext;"/>
<menuitem id="option-show-jit-optimizations"
class="experimental-option"
type="checkbox"
data-pref="show-jit-optimizations"
label="&performanceUI.showJITOptimizations;"
tooltiptext="&performanceUI.showJITOptimizations.tooltiptext;"/>
<menuitem id="option-enable-memory"
class="experimental-option"
type="checkbox"
data-pref="enable-memory"
label="&performanceUI.enableMemory;"
tooltiptext="&performanceUI.enableMemory.tooltiptext;"/>
<menuitem id="option-enable-allocations"
type="checkbox"
data-pref="enable-allocations"
label="&performanceUI.enableAllocations;"
tooltiptext="&performanceUI.enableAllocations.tooltiptext;"/>
<menuitem id="option-enable-framerate"
type="checkbox"
data-pref="enable-framerate"
label="&performanceUI.enableFramerate;"
tooltiptext="&performanceUI.enableFramerate.tooltiptext;"/>
<menuitem id="option-invert-call-tree"
type="checkbox"
data-pref="invert-call-tree"
label="&performanceUI.invertTree;"
tooltiptext="&performanceUI.invertTree.tooltiptext;"/>
<menuitem id="option-invert-flame-graph"
type="checkbox"
data-pref="invert-flame-graph"
label="&performanceUI.invertFlameGraph;"
tooltiptext="&performanceUI.invertFlameGraph.tooltiptext;"/>
<menuitem id="option-flatten-tree-recursion"
type="checkbox"
data-pref="flatten-tree-recursion"
label="&performanceUI.flattenTreeRecursion;"
tooltiptext="&performanceUI.flattenTreeRecursion.tooltiptext;"/>
</menupopup>
</popupset>
<hbox id="body" class="theme-body performance-tool" flex="1">
<!-- Sidebar: controls and recording list -->
<vbox id="recordings-pane">
<hbox id="recordings-controls">
<html:div id='recording-controls-mount'/>
</hbox>
<vbox id="recordings-list" class="theme-sidebar" flex="1">
<html:div id="recording-list-mount"/>
</vbox>
</vbox>
<!-- Main panel content -->
<vbox id="performance-pane" flex="1">
<!-- Top toolbar controls -->
<toolbar id="performance-toolbar"
class="devtools-toolbar">
<hbox id="performance-toolbar-controls-other"
class="devtools-toolbarbutton-group">
<toolbarbutton id="filter-button"
class="devtools-toolbarbutton"
popup="performance-filter-menupopup"
tooltiptext="&performanceUI.options.filter.tooltiptext;"/>
</hbox>
<hbox id="performance-toolbar-controls-detail-views"
class="devtools-toolbarbutton-group">
<toolbarbutton id="select-waterfall-view"
class="devtools-toolbarbutton"
label="&performanceUI.toolbar.waterfall;"
hidden="true"
data-view="waterfall"
tooltiptext="&performanceUI.toolbar.waterfall.tooltiptext;" />
<toolbarbutton id="select-js-calltree-view"
class="devtools-toolbarbutton"
label="&performanceUI.toolbar.js-calltree;"
hidden="true"
data-view="js-calltree"
tooltiptext="&performanceUI.toolbar.js-calltree.tooltiptext;" />
<toolbarbutton id="select-js-flamegraph-view"
class="devtools-toolbarbutton"
label="&performanceUI.toolbar.js-flamegraph;"
hidden="true"
data-view="js-flamegraph"
tooltiptext="&performanceUI.toolbar.js-flamegraph.tooltiptext;" />
<toolbarbutton id="select-memory-calltree-view"
class="devtools-toolbarbutton"
label="&performanceUI.toolbar.memory-calltree;"
hidden="true"
data-view="memory-calltree"
tooltiptext="&performanceUI.toolbar.allocations.tooltiptext;" />
<toolbarbutton id="select-memory-flamegraph-view"
class="devtools-toolbarbutton"
label="&performanceUI.toolbar.memory-flamegraph;"
hidden="true"
data-view="memory-flamegraph" />
</hbox>
<spacer flex="1"></spacer>
<hbox id="performance-toolbar-controls-options"
class="devtools-toolbarbutton-group">
<toolbarbutton id="performance-options-button"
class="devtools-toolbarbutton devtools-option-toolbarbutton"
popup="performance-options-menupopup"
tooltiptext="&performanceUI.options.gear.tooltiptext;"/>
</hbox>
</toolbar>
<!-- Recording contents and general notice messages -->
<deck id="performance-view" flex="1">
<!-- A default notice, shown while initially opening the tool.
Keep this element the first child of #performance-view. -->
<hbox id="tool-loading-notice"
class="notice-container"
flex="1">
</hbox>
<!-- "Unavailable" notice, shown when the entire tool is disabled,
for example, when in private browsing mode. -->
<vbox id="unavailable-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<hbox pack="center">
<html:div class='recording-button-mount'/>
</hbox>
<description class="tool-disabled-message">
&performanceUI.unavailableNoticePB;
</description>
</vbox>
<!-- "Empty" notice, shown when there's no recordings available -->
<hbox id="empty-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<hbox pack="center">
<html:div class='recording-button-mount'/>
</hbox>
</hbox>
<!-- Recording contents -->
<vbox id="performance-view-content" flex="1">
<!-- Overview graphs -->
<vbox id="overview-pane">
<hbox id="markers-overview"/>
<hbox id="memory-overview"/>
<hbox id="time-framerate"/>
</vbox>
<!-- Detail views and specific notice messages -->
<deck id="details-pane-container" flex="1">
<!-- "Loading" notice, shown when a recording is being loaded -->
<hbox id="loading-notice"
class="notice-container devtools-throbber"
align="center"
pack="center"
flex="1">
<label value="&performanceUI.loadingNotice;"/>
</hbox>
<!-- "Recording" notice, shown when a recording is in progress -->
<vbox id="recording-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<hbox pack="center">
<html:div class='recording-button-mount'/>
</hbox>
<label class="realtime-disabled-on-e10s-message"
value="&performanceUI.disabledRealTime.disabledE10S;"/>
<label class="buffer-status-message"
tooltiptext="&performanceUI.bufferStatusTooltip;"/>
<label class="buffer-status-message-full"
value="&performanceUI.bufferStatusFull;"/>
</vbox>
<!-- "Console" notice, shown when a console recording is in progress -->
<vbox id="console-recording-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<hbox class="console-profile-recording-notice">
<label value="&performanceUI.console.recordingNoticeStart;" />
<label class="console-profile-command" />
<label value="&performanceUI.console.recordingNoticeEnd;" />
</hbox>
<hbox class="console-profile-stop-notice">
<label value="&performanceUI.console.stopCommandStart;" />
<label class="console-profile-command" />
<label value="&performanceUI.console.stopCommandEnd;" />
</hbox>
<label class="realtime-disabled-on-e10s-message"
value="&performanceUI.disabledRealTime.disabledE10S;"/>
<label class="buffer-status-message"
tooltiptext="&performanceUI.bufferStatusTooltip;"/>
<label class="buffer-status-message-full"
value="&performanceUI.bufferStatusFull;"/>
</vbox>
<!-- Detail views -->
<deck id="details-pane" flex="1">
<!-- Waterfall -->
<hbox id="waterfall-view" flex="1">
<html:div xmlns="http://www.w3.org/1999/xhtml" id="waterfall-tree" />
<splitter class="devtools-side-splitter"/>
<vbox id="waterfall-details"
class="theme-sidebar"/>
</hbox>
<!-- JS Tree and JIT view -->
<hbox id="js-profile-view" flex="1">
<vbox id="js-calltree-view" flex="1">
<hbox class="call-tree-headers-container">
<label class="plain call-tree-header"
type="duration"
crop="end"
value="&performanceUI.table.totalDuration;"
tooltiptext="&performanceUI.table.totalDuration.tooltip;"/>
<label class="plain call-tree-header"
type="percentage"
crop="end"
value="&performanceUI.table.totalPercentage;"
tooltiptext="&performanceUI.table.totalPercentage.tooltip;"/>
<label class="plain call-tree-header"
type="self-duration"
crop="end"
value="&performanceUI.table.selfDuration;"
tooltiptext="&performanceUI.table.selfDuration.tooltip;"/>
<label class="plain call-tree-header"
type="self-percentage"
crop="end"
value="&performanceUI.table.selfPercentage;"
tooltiptext="&performanceUI.table.selfPercentage.tooltip;"/>
<label class="plain call-tree-header"
type="samples"
crop="end"
value="&performanceUI.table.samples;"
tooltiptext="&performanceUI.table.samples.tooltip;"/>
<label class="plain call-tree-header"
type="function"
crop="end"
value="&performanceUI.table.function;"
tooltiptext="&performanceUI.table.function.tooltip;"/>
</hbox>
<vbox class="call-tree-cells-container" flex="1"/>
</vbox>
<splitter class="devtools-side-splitter"/>
<!-- Optimizations Panel -->
<vbox id="jit-optimizations-view"
class="hidden">
</vbox>
</hbox>
<!-- JS FlameChart -->
<hbox id="js-flamegraph-view" flex="1">
</hbox>
<!-- Memory Tree -->
<vbox id="memory-calltree-view" flex="1">
<hbox class="call-tree-headers-container">
<label class="plain call-tree-header"
type="self-size"
crop="end"
value="Self Bytes"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="self-size-percentage"
crop="end"
value="Self Bytes %"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="self-count"
crop="end"
value="Self Count"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="self-count-percentage"
crop="end"
value="Self Count %"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="size"
crop="end"
value="Total Size"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="size-percentage"
crop="end"
value="Total Size %"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="count"
crop="end"
value="Total Count"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="count-percentage"
crop="end"
value="Total Count %"
tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
<label class="plain call-tree-header"
type="function"
crop="end"
value="&performanceUI.table.function;"/>
</hbox>
<vbox class="call-tree-cells-container" flex="1"/>
</vbox>
<!-- Memory FlameChart -->
<hbox id="memory-flamegraph-view" flex="1"></hbox>
</deck>
</deck>
</vbox>
</deck>
</vbox>
</hbox>
</window>

View File

@ -1,80 +0,0 @@
/* 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";
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/shared/loader/browser-loader.js"
);
const { require } = BrowserLoader({
baseURI: "resource://devtools/client/performance/",
window: window,
});
const {
PerformanceController,
} = require("devtools/client/performance/performance-controller");
const {
PerformanceView,
} = require("devtools/client/performance/performance-view");
const { DetailsView } = require("devtools/client/performance/views/details");
const {
DetailsSubview,
} = require("devtools/client/performance/views/details-abstract-subview");
const {
JsCallTreeView,
} = require("devtools/client/performance/views/details-js-call-tree");
const {
JsFlameGraphView,
} = require("devtools/client/performance/views/details-js-flamegraph");
const {
MemoryCallTreeView,
} = require("devtools/client/performance/views/details-memory-call-tree");
const {
MemoryFlameGraphView,
} = require("devtools/client/performance/views/details-memory-flamegraph");
const { OverviewView } = require("devtools/client/performance/views/overview");
const {
RecordingsView,
} = require("devtools/client/performance/views/recordings");
const { ToolbarView } = require("devtools/client/performance/views/toolbar");
const {
WaterfallView,
} = require("devtools/client/performance/views/details-waterfall");
const EVENTS = require("devtools/client/performance/events");
/**
* The performance panel used to only share modules through references on the window
* object. We started cleaning this up and to require() explicitly in Bug 1524982, but
* some modules and tests are still relying on those references so we keep exposing them
* for the moment. Bug 1528777.
*/
window.PerformanceController = PerformanceController;
window.PerformanceView = PerformanceView;
window.DetailsView = DetailsView;
window.DetailsSubview = DetailsSubview;
window.JsCallTreeView = JsCallTreeView;
window.JsFlameGraphView = JsFlameGraphView;
window.MemoryCallTreeView = MemoryCallTreeView;
window.MemoryFlameGraphView = MemoryFlameGraphView;
window.OverviewView = OverviewView;
window.RecordingsView = RecordingsView;
window.ToolbarView = ToolbarView;
window.WaterfallView = WaterfallView;
window.EVENTS = EVENTS;
/**
* DOM query helpers.
*/
/* exported $, $$ */
function $(selector, target = document) {
return target.querySelector(selector);
}
window.$ = $;
function $$(selector, target = document) {
return target.querySelectorAll(selector);
}
window.$$ = $$;

View File

@ -1,87 +0,0 @@
/* 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";
const { L10N } = require("devtools/client/performance/modules/global");
/**
* Details about each label stack frame category.
* To be kept in sync with the JS::ProfilingCategory enum in ProfilingCategory.h
*/
const CATEGORIES = [
{
color: "#d99b28",
abbrev: "idle",
label: L10N.getStr("category.idle"),
},
{
color: "#5e88b0",
abbrev: "other",
label: L10N.getStr("category.other"),
},
{
color: "#46afe3",
abbrev: "layout",
label: L10N.getStr("category.layout"),
},
{
color: "#d96629",
abbrev: "js",
label: L10N.getStr("category.js"),
},
{
color: "#eb5368",
abbrev: "gc",
label: L10N.getStr("category.gc"),
},
{
color: "#df80ff",
abbrev: "network",
label: L10N.getStr("category.network"),
},
{
color: "#70bf53",
abbrev: "graphics",
label: L10N.getStr("category.graphics"),
},
{
color: "#8fa1b2",
abbrev: "dom",
label: L10N.getStr("category.dom"),
},
{
// The devtools-only "tools" category which gets computed by
// computeIsContentAndCategory and is not part of the category list in
// ProfilingStack.h.
color: "#8fa1b2",
abbrev: "tools",
label: L10N.getStr("category.tools"),
},
];
/**
* Get the numeric index for the given category abbreviation.
* See `CATEGORIES` above.
*/
const CATEGORY_INDEX = (() => {
const indexForCategory = {};
for (
let categoryIndex = 0;
categoryIndex < CATEGORIES.length;
categoryIndex++
) {
const category = CATEGORIES[categoryIndex];
indexForCategory[category.abbrev] = categoryIndex;
}
return function(name) {
if (!(name in indexForCategory)) {
throw new Error(`Category abbreviation "${name}" does not exist.`);
}
return indexForCategory[name];
};
})();
exports.CATEGORIES = CATEGORIES;
exports.CATEGORY_INDEX = CATEGORY_INDEX;

View File

@ -1,11 +0,0 @@
/* 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";
exports.Constants = {
// ms
FRAMERATE_GRAPH_LOW_RES_INTERVAL: 100,
// ms
FRAMERATE_GRAPH_HIGH_RES_INTERVAL: 16,
};

View File

@ -1,36 +0,0 @@
/* 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";
const { MultiLocalizationHelper } = require("devtools/shared/l10n");
const { PrefsHelper } = require("devtools/client/shared/prefs");
/**
* Localization convenience methods.
*/
exports.L10N = new MultiLocalizationHelper(
"devtools/client/locales/markers.properties",
"devtools/client/locales/performance.properties"
);
/**
* A list of preferences for this tool. The values automatically update
* if somebody edits edits about:config or the prefs change somewhere else.
*
* This needs to be registered and unregistered when used for the auto-update
* functionality to work. The PerformanceController handles this, but if you
* just use this module in a test independently, ensure you call
* `registerObserver()` and `unregisterUnobserver()`.
*/
exports.PREFS = new PrefsHelper("devtools.performance", {
"show-triggers-for-gc-types": ["Char", "ui.show-triggers-for-gc-types"],
"show-platform-data": ["Bool", "ui.show-platform-data"],
"hidden-markers": ["Json", "timeline.hidden-markers"],
"memory-sample-probability": ["Float", "memory.sample-probability"],
"memory-max-log-length": ["Int", "memory.max-log-length"],
"profiler-buffer-size": ["Int", "profiler.buffer-size"],
"profiler-sample-frequency": ["Int", "profiler.sample-frequency-hz"],
// TODO: re-enable once we flame charts via bug 1148663.
"enable-memory-flame": ["Bool", "ui.enable-memory-flame"],
});

View File

@ -1,173 +0,0 @@
/* 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";
const { Cc, Ci } = require("chrome");
const RecordingUtils = require("devtools/shared/performance/recording-utils");
const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
// This identifier string is used to tentatively ascertain whether or not
// a JSON loaded from disk is actually something generated by this tool.
// It isn't, of course, a definitive verification, but a Good Enough™
// approximation before continuing the import. Don't localize this.
const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;
/**
* Helpers for importing/exporting JSON.
*/
/**
* Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
* @return object
*/
function getUnicodeConverter() {
const cname = "@mozilla.org/intl/scriptableunicodeconverter";
const converter = Cc[cname].createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
return converter;
}
/**
* Saves a recording as JSON to a file. The provided data is assumed to be
* acyclical, so that it can be properly serialized.
*
* @param object recordingData
* The recording data to stream as JSON.
* @param nsIFile file
* The file to stream the data into.
* @return object
* A promise that is resolved once streaming finishes, or rejected
* if there was an error.
*/
function saveRecordingToFile(recordingData, file) {
recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
const string = JSON.stringify(recordingData);
const inputStream = getUnicodeConverter().convertToInputStream(string);
const outputStream = FileUtils.openSafeFileOutputStream(file);
return new Promise(resolve => {
NetUtil.asyncCopy(inputStream, outputStream, resolve);
});
}
/**
* Loads a recording stored as JSON from a file.
*
* @param nsIFile file
* The file to import the data from.
* @return object
* A promise that is resolved once importing finishes, or rejected
* if there was an error.
*/
function loadRecordingFromFile(file) {
const channel = NetUtil.newChannel({
uri: NetUtil.newURI(file),
loadUsingSystemPrincipal: true,
});
channel.contentType = "text/plain";
return new Promise((resolve, reject) => {
NetUtil.asyncFetch(channel, inputStream => {
let recordingData;
try {
const string = NetUtil.readInputStreamToString(
inputStream,
inputStream.available()
);
recordingData = JSON.parse(string);
} catch (e) {
reject(new Error("Could not read recording data file."));
return;
}
if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
reject(new Error("Unrecognized recording data file."));
return;
}
if (!isValidSerializerVersion(recordingData.version)) {
reject(new Error("Unsupported recording data file version."));
return;
}
if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
recordingData = convertLegacyData(recordingData);
}
if (recordingData.profile.meta.version === 2) {
RecordingUtils.deflateProfile(recordingData.profile);
}
// If the recording has no label, set it to be the
// filename without its extension.
if (!recordingData.label) {
recordingData.label = file.leafName.replace(/\.[^.]+$/, "");
}
resolve(recordingData);
});
});
}
/**
* Returns a boolean indicating whether or not the passed in `version`
* is supported by this serializer.
*
* @param number version
* @return boolean
*/
function isValidSerializerVersion(version) {
return !!~[
PERF_TOOL_SERIALIZER_LEGACY_VERSION,
PERF_TOOL_SERIALIZER_CURRENT_VERSION,
].indexOf(version);
}
/**
* Takes recording data (with version `1`, from the original profiler tool),
* and massages the data to be line with the current performance tool's
* property names and values.
*
* @param object legacyData
* @return object
*/
function convertLegacyData(legacyData) {
const { profilerData, ticksData, recordingDuration } = legacyData;
// The `profilerData` and `ticksData` stay, but the previously unrecorded
// fields just are empty arrays or objects.
const data = {
label: profilerData.profilerLabel,
duration: recordingDuration,
markers: [],
frames: [],
memory: [],
ticks: ticksData,
allocations: { sites: [], timestamps: [], frames: [], sizes: [] },
profile: profilerData.profile,
// Fake a configuration object here if there's tick data,
// so that it can be rendered.
configuration: {
withTicks: !!ticksData.length,
withMarkers: false,
withMemory: false,
withAllocations: false,
},
systemHost: {},
systemClient: {},
};
return data;
}
exports.saveRecordingToFile = saveRecordingToFile;
exports.loadRecordingFromFile = loadRecordingFromFile;

View File

@ -1,510 +0,0 @@
/* 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";
const global = require("devtools/client/performance/modules/global");
const demangle = require("devtools/client/shared/demangle");
const { assert } = require("devtools/shared/DevToolsUtils");
const {
isChromeScheme,
isContentScheme,
isWASM,
parseURL,
} = require("devtools/client/shared/source-utils");
const {
CATEGORY_INDEX,
CATEGORIES,
} = require("devtools/client/performance/modules/categories");
// Character codes used in various parsing helper functions.
const CHAR_CODE_R = "r".charCodeAt(0);
const CHAR_CODE_0 = "0".charCodeAt(0);
const CHAR_CODE_9 = "9".charCodeAt(0);
const CHAR_CODE_CAP_Z = "Z".charCodeAt(0);
const CHAR_CODE_LPAREN = "(".charCodeAt(0);
const CHAR_CODE_RPAREN = ")".charCodeAt(0);
const CHAR_CODE_COLON = ":".charCodeAt(0);
const CHAR_CODE_SPACE = " ".charCodeAt(0);
const CHAR_CODE_UNDERSCORE = "_".charCodeAt(0);
const EVAL_TOKEN = "%20%3E%20eval";
// The cache used to store inflated frames.
const gInflatedFrameStore = new WeakMap();
// The cache used to store frame data from `getInfo`.
const gFrameData = new WeakMap();
/**
* Parses the raw location of this function call to retrieve the actual
* function name, source url, host name, line and column.
*/
// eslint-disable-next-line complexity
function parseLocation(location, fallbackLine, fallbackColumn) {
// Parse the `location` for the function name, source url, line, column etc.
let line, column, url;
// These two indices are used to extract the resource substring, which is
// location[parenIndex + 1 .. lineAndColumnIndex].
//
// There are 3 variants of location strings in the profiler (with optional
// column numbers):
// 1) "name (resource:line)"
// 2) "resource:line"
// 3) "resource"
//
// For example for (1), take "foo (bar.js:1)".
// ^ ^
// | |
// | |
// | |
// parenIndex will point to ------+ |
// |
// lineAndColumnIndex will point to -----+
//
// For an example without parentheses, take "bar.js:2".
// ^ ^
// | |
// parenIndex will point to ----------------+ |
// |
// lineAndColumIndex will point to ----------------+
//
// To parse, we look for the last occurrence of the string ' ('.
//
// For 1), all occurrences of space ' ' characters in the resource string
// are urlencoded, so the last occurrence of ' (' is the separator between
// the function name and the resource.
//
// For 2) and 3), there can be no occurences of ' (' since ' ' characters
// are urlencoded in the resource string.
//
// XXX: Note that 3) is ambiguous with Gecko Profiler marker locations like
// "EnterJIT". We can't distinguish the two, so we treat 3) like a function
// name.
let parenIndex = -1;
let lineAndColumnIndex = -1;
const lastCharCode = location.charCodeAt(location.length - 1);
let i;
if (lastCharCode === CHAR_CODE_RPAREN) {
// Case 1)
i = location.length - 2;
} else if (isNumeric(lastCharCode)) {
// Case 2)
i = location.length - 1;
} else {
// Case 3)
i = 0;
}
if (i !== 0) {
// Look for a :number.
let end = i;
while (isNumeric(location.charCodeAt(i))) {
i--;
}
if (location.charCodeAt(i) === CHAR_CODE_COLON) {
column = location.substr(i + 1, end - i);
i--;
}
// Look for a preceding :number.
end = i;
while (isNumeric(location.charCodeAt(i))) {
i--;
}
// If two were found, the first is the line and the second is the
// column. If only a single :number was found, then it is the line number.
if (location.charCodeAt(i) === CHAR_CODE_COLON) {
line = location.substr(i + 1, end - i);
lineAndColumnIndex = i;
i--;
} else {
lineAndColumnIndex = i + 1;
line = column;
column = undefined;
}
}
// Look for the last occurrence of ' (' in case 1).
if (lastCharCode === CHAR_CODE_RPAREN) {
for (; i >= 0; i--) {
if (
location.charCodeAt(i) === CHAR_CODE_LPAREN &&
i > 0 &&
location.charCodeAt(i - 1) === CHAR_CODE_SPACE
) {
parenIndex = i;
break;
}
}
}
let parsedUrl;
if (lineAndColumnIndex > 0) {
const resource = location.substring(parenIndex + 1, lineAndColumnIndex);
url = resource.split(" -> ").pop();
if (url) {
parsedUrl = parseURL(url);
}
}
let functionName, fileName, port, host;
line = line || fallbackLine;
column = column || fallbackColumn;
// If the URL digged out from the `location` is valid, this is a JS frame.
if (parsedUrl) {
functionName = location.substring(0, parenIndex - 1);
fileName = parsedUrl.fileName;
port = parsedUrl.port;
host = parsedUrl.host;
// Check for the case of the filename containing eval
// e.g. "file.js%20line%2065%20%3E%20eval"
const evalIndex = fileName.indexOf(EVAL_TOKEN);
if (evalIndex !== -1 && evalIndex === fileName.length - EVAL_TOKEN.length) {
// Match the filename
const evalLine = line;
const [, _fileName, , _line] =
fileName.match(/(.+)(%20line%20(\d+)%20%3E%20eval)/) || [];
fileName = `${_fileName} (eval:${evalLine})`;
line = _line;
assert(
_fileName !== undefined,
"Filename could not be found from an eval location site"
);
assert(
_line !== undefined,
"Line could not be found from an eval location site"
);
// Match the url as well
[, url] = url.match(/(.+)( line (\d+) > eval)/) || [];
assert(
url !== undefined,
"The URL could not be parsed correctly from an eval location site"
);
}
} else {
functionName = location;
url = null;
}
return { functionName, fileName, host, port, url, line, column };
}
/**
* Sets the properties of `isContent` and `category` on a frame.
*
* @param {InflatedFrame} frame
*/
function computeIsContentAndCategory(frame) {
const location = frame.location;
// There are 3 variants of location strings in the profiler (with optional
// column numbers):
// 1) "name (resource:line)"
// 2) "resource:line"
// 3) "resource"
const lastCharCode = location.charCodeAt(location.length - 1);
let schemeStartIndex = -1;
if (lastCharCode === CHAR_CODE_RPAREN) {
// Case 1)
//
// Need to search for the last occurrence of ' (' to find the start of the
// resource string.
for (let i = location.length - 2; i >= 0; i--) {
if (
location.charCodeAt(i) === CHAR_CODE_LPAREN &&
i > 0 &&
location.charCodeAt(i - 1) === CHAR_CODE_SPACE
) {
schemeStartIndex = i + 1;
break;
}
}
} else {
// Cases 2) and 3)
schemeStartIndex = 0;
}
// We can't know if WASM frames are content or not at the time of this writing, so label
// them all as content.
if (isContentScheme(location, schemeStartIndex) || isWASM(location)) {
frame.isContent = true;
return;
}
if (frame.category !== null && frame.category !== undefined) {
return;
}
if (schemeStartIndex !== 0) {
for (let j = schemeStartIndex; j < location.length; j++) {
if (
location.charCodeAt(j) === CHAR_CODE_R &&
isChromeScheme(location, j) &&
(location.includes("resource://devtools") ||
location.includes("resource://devtools"))
) {
frame.category = CATEGORY_INDEX("tools");
return;
}
}
}
if (location === "EnterJIT") {
frame.category = CATEGORY_INDEX("js");
return;
}
frame.category = CATEGORY_INDEX("other");
}
/**
* Get caches to cache inflated frames and computed frame keys of a frame
* table.
*
* @param object framesTable
* @return object
*/
function getInflatedFrameCache(frameTable) {
let inflatedCache = gInflatedFrameStore.get(frameTable);
if (inflatedCache !== undefined) {
return inflatedCache;
}
// Fill with nulls to ensure no holes.
inflatedCache = Array.from({ length: frameTable.data.length }, () => null);
gInflatedFrameStore.set(frameTable, inflatedCache);
return inflatedCache;
}
/**
* Get or add an inflated frame to a cache.
*
* @param object cache
* @param number index
* @param object frameTable
* @param object stringTable
*/
function getOrAddInflatedFrame(cache, index, frameTable, stringTable) {
let inflatedFrame = cache[index];
if (inflatedFrame === null) {
inflatedFrame = cache[index] = new InflatedFrame(
index,
frameTable,
stringTable
);
}
return inflatedFrame;
}
/**
* An intermediate data structured used to hold inflated frames.
*
* @param number index
* @param object frameTable
* @param object stringTable
*/
function InflatedFrame(index, frameTable, stringTable) {
const LOCATION_SLOT = frameTable.schema.location;
const IMPLEMENTATION_SLOT = frameTable.schema.implementation;
const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations;
const LINE_SLOT = frameTable.schema.line;
const CATEGORY_SLOT = frameTable.schema.category;
const frame = frameTable.data[index];
const category = frame[CATEGORY_SLOT];
this.location = stringTable[frame[LOCATION_SLOT]];
this.implementation = frame[IMPLEMENTATION_SLOT];
this.optimizations = frame[OPTIMIZATIONS_SLOT];
this.line = frame[LINE_SLOT];
this.column = undefined;
this.category = category;
this.isContent = false;
// Attempt to compute if this frame is a content frame, and if not,
// its category.
//
// Since only C++ stack frames have associated category information,
// attempt to generate a useful category, fallback to the one provided
// by the profiling data, or fallback to an unknown category.
computeIsContentAndCategory(this);
}
/**
* Gets the frame key (i.e., equivalence group) according to options. Content
* frames are always identified by location. Chrome frames are identified by
* location if content-only filtering is off. If content-filtering is on, they
* are identified by their category.
*
* @param object options
* @return string
*/
InflatedFrame.prototype.getFrameKey = function getFrameKey(options) {
if (this.isContent || !options.contentOnly || options.isRoot) {
options.isMetaCategoryOut = false;
return this.location;
}
if (options.isLeaf) {
// We only care about leaf platform frames if we are displaying content
// only. If no category is present, give the default category of "other".
//
// 1. The leaf is where time is _actually_ being spent, so we _need_ to
// show it to developers in some way to give them accurate profiling
// data. We decide to split the platform into various category buckets
// and just show time spent in each bucket.
//
// 2. The calls leading to the leaf _aren't_ where we are spending time,
// but _do_ give the developer context for how they got to the leaf
// where they _are_ spending time. For non-platform hackers, the
// non-leaf platform frames don't give any meaningful context, and so we
// can safely filter them out.
options.isMetaCategoryOut = true;
return this.category;
}
// Return an empty string denoting that this frame should be skipped.
return "";
};
function isNumeric(c) {
return c >= CHAR_CODE_0 && c <= CHAR_CODE_9;
}
function shouldDemangle(name) {
return (
name?.charCodeAt &&
name.charCodeAt(0) === CHAR_CODE_UNDERSCORE &&
name.charCodeAt(1) === CHAR_CODE_UNDERSCORE &&
name.charCodeAt(2) === CHAR_CODE_CAP_Z
);
}
/**
* Calculates the relative costs of this frame compared to a root,
* and generates allocations information if specified. Uses caching
* if possible.
*
* @param {ThreadNode|FrameNode} node
* The node we are calculating.
* @param {ThreadNode} options.root
* The root thread node to calculate relative costs.
* Generates [self|total] [duration|percentage] values.
* @param {boolean} options.allocations
* Generates `totalAllocations` and `selfAllocations`.
*
* @return {object}
*/
function getFrameInfo(node, options) {
let data = gFrameData.get(node);
if (!data) {
if (node.nodeType === "Thread") {
data = Object.create(null);
data.functionName = global.L10N.getStr("table.root");
} else {
data = parseLocation(node.location, node.line, node.column);
data.hasOptimizations = node.hasOptimizations();
data.isContent = node.isContent;
data.isMetaCategory = node.isMetaCategory;
}
data.samples = node.youngestFrameSamples;
const hasCategory = node.category !== null && node.category !== undefined;
data.categoryData = hasCategory
? CATEGORIES[node.category] || CATEGORIES[CATEGORY_INDEX("other")]
: {};
data.nodeType = node.nodeType;
// Frame name (function location or some meta information)
if (data.isMetaCategory) {
data.name = data.categoryData.label;
} else if (shouldDemangle(data.functionName)) {
data.name = demangle(data.functionName);
} else {
data.name = data.functionName;
}
data.tooltiptext = data.isMetaCategory
? data.categoryData.label
: node.location || "";
gFrameData.set(node, data);
}
// If no options specified, we can't calculate relative values, abort here
if (!options) {
return data;
}
// If a root specified, calculate the relative costs in the context of
// this call tree. The cached store may already have this, but generate
// if it does not.
const totalSamples = options.root.samples;
const totalDuration = options.root.duration;
if (options?.root && !data.COSTS_CALCULATED) {
data.selfDuration =
(node.youngestFrameSamples / totalSamples) * totalDuration;
data.selfPercentage = (node.youngestFrameSamples / totalSamples) * 100;
data.totalDuration = (node.samples / totalSamples) * totalDuration;
data.totalPercentage = (node.samples / totalSamples) * 100;
data.COSTS_CALCULATED = true;
}
if (options?.allocations && !data.ALLOCATION_DATA_CALCULATED) {
const totalBytes = options.root.byteSize;
data.selfCount = node.youngestFrameSamples;
data.totalCount = node.samples;
data.selfCountPercentage = (node.youngestFrameSamples / totalSamples) * 100;
data.totalCountPercentage = (node.samples / totalSamples) * 100;
data.selfSize = node.youngestFrameByteSize;
data.totalSize = node.byteSize;
data.selfSizePercentage = (node.youngestFrameByteSize / totalBytes) * 100;
data.totalSizePercentage = (node.byteSize / totalBytes) * 100;
data.ALLOCATION_DATA_CALCULATED = true;
}
return data;
}
exports.getFrameInfo = getFrameInfo;
/**
* Takes an inverted ThreadNode and searches its youngest frames for
* a FrameNode with matching location.
*
* @param {ThreadNode} threadNode
* @param {string} location
* @return {?FrameNode}
*/
function findFrameByLocation(threadNode, location) {
if (!threadNode.inverted) {
throw new Error(
"FrameUtils.findFrameByLocation only supports leaf nodes in an inverted tree."
);
}
const calls = threadNode.calls;
for (let i = 0; i < calls.length; i++) {
if (calls[i].location === location) {
return calls[i];
}
}
return null;
}
exports.findFrameByLocation = findFrameByLocation;
exports.computeIsContentAndCategory = computeIsContentAndCategory;
exports.parseLocation = parseLocation;
exports.getInflatedFrameCache = getInflatedFrameCache;
exports.getOrAddInflatedFrame = getOrAddInflatedFrame;
exports.InflatedFrame = InflatedFrame;
exports.shouldDemangle = shouldDemangle;

View File

@ -1,350 +0,0 @@
/* 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";
// An outcome of an OptimizationAttempt that is considered successful.
const SUCCESSFUL_OUTCOMES = [
"GenericSuccess",
"Inlined",
"DOM",
"Monomorphic",
"Polymorphic",
];
/**
* Model representing JIT optimization sites from the profiler
* for a frame (represented by a FrameNode). Requires optimization data from
* a profile, which is an array of RawOptimizationSites.
*
* When the ThreadNode for the profile iterates over the samples' frames, each
* frame's optimizations are accumulated in their respective FrameNodes. Each
* FrameNode may contain many different optimization sites. One sample may
* pick up optimization X on line Y in the frame, with the next sample
* containing optimization Z on line W in the same frame, as each frame is
* only function.
*
* An OptimizationSite contains a record of how many times the
* RawOptimizationSite was sampled, as well as the unique id based off of the
* original profiler array, and the RawOptimizationSite itself as a reference.
* @see devtools/client/performance/modules/logic/tree-model.js
*
* @struct RawOptimizationSite
* A structure describing a location in a script that was attempted to be optimized.
* Contains all the IonTypes observed, and the sequence of OptimizationAttempts that
* were attempted, and the line and column in the script. This is retrieved from the
* profiler after a recording, and our base data structure. Should always be referenced,
* and unmodified.
*
* Note that propertyName is an index into a string table, which needs to be
* provided in order for the raw optimization site to be inflated.
*
* @type {Array<IonType>} types
* @type {Array<OptimizationAttempt>} attempts
* @type {?number} propertyName
* @type {number} line
* @type {number} column
*
*
* @struct IonType
* IonMonkey attempts to classify each value in an optimization site by some type.
* Based off of the observed types for a value (like a variable that could be a
* string or an instance of an object), it determines what kind of type it should be
* classified as. Each IonType here contains an array of all ObservedTypes under `types`,
* the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as
* `mirType`, and the component of this optimization type that this value refers to --
* like a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b`
* (the "Index").
*
* Generally the more ObservedTypes, the more deoptimized this OptimizationSite is.
* There could be no ObservedTypes, in which case `typeset` is undefined.
*
* @type {?Array<ObservedType>} typeset
* @type {string} site
* @type {string} mirType
*
*
* @struct ObservedType
* When IonMonkey attempts to determine what type a value is, it checks on each sample.
* The ObservedType can be thought of in more of JavaScripty-terms, rather than C++.
* The `keyedBy` property is a high level description of the type, like "primitive",
* "constructor", "function", "singleton", "alloc-site" (that one is a bit more weird).
* If the `keyedBy` type is a function or constructor, the ObservedType should have a
* `name` property, referring to the function or constructor name from the JS source.
* If IonMonkey can determine the origin of this type (like where the constructor is
* defined), the ObservedType will also have `location` and `line` properties, but
* `location` can sometimes be non-URL strings like "self-hosted" or a memory location
* like "102ca7880", or no location at all, and maybe `line` is 0 or undefined.
*
* @type {string} keyedBy
* @type {?string} name
* @type {?string} location
* @type {?string} line
*
*
* @struct OptimizationAttempt
* Each RawOptimizationSite contains an array of OptimizationAttempts. Generally,
* IonMonkey goes through a series of strategies for each kind of optimization, starting
* from most-niche and optimized, to the less-optimized, but more general strategies --
* for example, a getter opt may first try to optimize for the scenario of a getter on an
* `arguments` object -- that will fail most of the time, as most objects are not
* arguments objects, but it will attempt several strategies in order until it finds a
* strategy that works, or fails. Even in the best scenarios, some attempts will fail
* (like the arguments getter example), which is OK, as long as some attempt succeeds
* (with the earlier attempts preferred, as those are more optimized). In an
* OptimizationAttempt structure, we store just the `strategy` name and `outcome` name,
* both from enums in js/public/TrackedOptimizationInfo.h as TRACKED_STRATEGY_LIST and
* TRACKED_OUTCOME_LIST, respectively. An array of successful outcome strings are above
* in SUCCESSFUL_OUTCOMES.
*
* @see js/public/TrackedOptimizationInfo.h
*
* @type {string} strategy
* @type {string} outcome
*/
/*
* A wrapper around RawOptimizationSite to record sample count and ID (referring to the
* index of where this is in the initially seeded optimizations data), so we don't mutate
* the original data from the profiler. Provides methods to access the underlying
* optimization data easily, so understanding the semantics of JIT data isn't necessary.
*
* @constructor
*
* @param {Array<RawOptimizationSite>} optimizations
* @param {number} optsIndex
*
* @type {RawOptimizationSite} data
* @type {number} samples
* @type {number} id
*/
const OptimizationSite = function(id, opts) {
this.id = id;
this.data = opts;
this.samples = 1;
};
/**
* Constructor for JITOptimizations. A collection of OptimizationSites for a frame.
*
* @constructor
* @param {Array<RawOptimizationSite>} rawSites
* Array of raw optimization sites.
* @param {Array<string>} stringTable
* Array of strings from the profiler used to inflate
* JIT optimizations. Do not modify this!
*/
const JITOptimizations = function(rawSites, stringTable) {
// Build a histogram of optimization sites.
const sites = [];
for (const rawSite of rawSites) {
const existingSite = sites.find(site => site.data === rawSite);
if (existingSite) {
existingSite.samples++;
} else {
sites.push(new OptimizationSite(sites.length, rawSite));
}
}
// Inflate the optimization information.
for (const site of sites) {
const data = site.data;
const STRATEGY_SLOT = data.attempts.schema.strategy;
const OUTCOME_SLOT = data.attempts.schema.outcome;
const attempts = data.attempts.data.map(a => {
return {
id: site.id,
strategy: stringTable[a[STRATEGY_SLOT]],
outcome: stringTable[a[OUTCOME_SLOT]],
};
});
const types = data.types.map(t => {
const typeset = maybeTypeset(t.typeset, stringTable);
if (typeset) {
typeset.forEach(ts => {
ts.id = site.id;
});
}
return {
id: site.id,
typeset,
site: stringTable[t.site],
mirType: stringTable[t.mirType],
};
});
// Add IDs to to all children objects, so we can correllate sites when
// just looking at a specific type, attempt, etc..
attempts.id = types.id = site.id;
site.data = {
attempts,
types,
propertyName: maybeString(stringTable, data.propertyName),
line: data.line,
column: data.column,
};
}
this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);
};
/**
* Make JITOptimizations iterable.
*/
JITOptimizations.prototype = {
[Symbol.iterator]: function*() {
yield* this.optimizationSites;
},
get length() {
return this.optimizationSites.length;
},
};
/**
* Takes an "outcome" string from an OptimizationAttempt and returns
* a boolean indicating whether or not its a successful outcome.
*
* @return {boolean}
*/
function isSuccessfulOutcome(outcome) {
return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome);
}
/**
* Takes an OptimizationSite. Returns a boolean indicating if the passed
* in OptimizationSite has a "good" outcome at the end of its attempted strategies.
*
* @param {OptimizationSite} optimizationSite
* @return {boolean}
*/
function hasSuccessfulOutcome(optimizationSite) {
const attempts = optimizationSite.data.attempts;
const lastOutcome = attempts[attempts.length - 1].outcome;
return isSuccessfulOutcome(lastOutcome);
}
function maybeString(stringTable, index) {
return index ? stringTable[index] : undefined;
}
function maybeTypeset(typeset, stringTable) {
if (!typeset) {
return undefined;
}
return typeset.map(ty => {
return {
keyedBy: maybeString(stringTable, ty.keyedBy),
name: maybeString(stringTable, ty.name),
location: maybeString(stringTable, ty.location),
line: ty.line,
};
});
}
// Map of optimization implementation names to an enum.
const IMPLEMENTATION_MAP = {
interpreter: 0,
baseline: 1,
ion: 2,
};
const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP);
/**
* Takes data from a FrameNode and computes rendering positions for
* a stacked mountain graph, to visualize JIT optimization tiers over time.
*
* @param {FrameNode} frameNode
* The FrameNode who's optimizations we're iterating.
* @param {Array<number>} sampleTimes
* An array of every sample time within the range we're counting.
* From a ThreadNode's `sampleTimes` property.
* @param {number} bucketSize
* Size of each bucket in milliseconds.
* `duration / resolution = bucketSize` in OptimizationsGraph.
* @return {?Array<object>}
*/
function createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize) {
const tierData = frameNode.getTierData();
const stringTable = frameNode._stringTable;
const output = [];
let implEnum;
let tierDataIndex = 0;
let nextOptSample = tierData[tierDataIndex];
// Bucket data
let samplesInCurrentBucket = 0;
let currentBucketStartTime = sampleTimes[0];
let bucket = [];
// Store previous data point so we can have straight vertical lines
let previousValues;
// Iterate one after the samples, so we can finalize the last bucket
for (let i = 0; i <= sampleTimes.length; i++) {
const sampleTime = sampleTimes[i];
// If this sample is in the next bucket, or we're done
// checking sampleTimes and on the last iteration, finalize previous bucket
if (
sampleTime >= currentBucketStartTime + bucketSize ||
i >= sampleTimes.length
) {
const dataPoint = {};
dataPoint.values = [];
dataPoint.delta = currentBucketStartTime;
// Map the opt site counts as a normalized percentage (0-1)
// of its count in context of total samples this bucket
for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) {
dataPoint.values[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1);
}
// Push the values from the previous bucket to the same time
// as the current bucket so we get a straight vertical line.
if (previousValues) {
const data = Object.create(null);
data.values = previousValues;
data.delta = currentBucketStartTime;
output.push(data);
}
output.push(dataPoint);
// Set the new start time of this bucket and reset its count
currentBucketStartTime += bucketSize;
samplesInCurrentBucket = 0;
previousValues = dataPoint.values;
bucket = [];
}
// If this sample observed an optimization in this frame, record it
if (nextOptSample && nextOptSample.time === sampleTime) {
// If no implementation defined, it was the "interpreter".
implEnum =
IMPLEMENTATION_MAP[
stringTable[nextOptSample.implementation] || "interpreter"
];
bucket[implEnum] = (bucket[implEnum] || 0) + 1;
nextOptSample = tierData[++tierDataIndex];
}
samplesInCurrentBucket++;
}
return output;
}
exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode;
exports.OptimizationSite = OptimizationSite;
exports.JITOptimizations = JITOptimizations;
exports.hasSuccessfulOutcome = hasSuccessfulOutcome;
exports.isSuccessfulOutcome = isSuccessfulOutcome;
exports.SUCCESSFUL_OUTCOMES = SUCCESSFUL_OUTCOMES;

View File

@ -1,12 +0,0 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
"frame-utils.js",
"jit.js",
"telemetry.js",
"tree-model.js",
"waterfall-utils.js",
)

View File

@ -1,106 +0,0 @@
/* 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";
const Telemetry = require("devtools/client/shared/telemetry");
const EVENTS = require("devtools/client/performance/events");
const EVENT_MAP_FLAGS = new Map([
[EVENTS.RECORDING_IMPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG"],
[EVENTS.RECORDING_EXPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG"],
]);
const RECORDING_FEATURES = [
"withMarkers",
"withTicks",
"withMemory",
"withAllocations",
];
const SELECTED_VIEW_HISTOGRAM_NAME = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
function PerformanceTelemetry(emitter) {
this._emitter = emitter;
this._telemetry = new Telemetry();
this.onFlagEvent = this.onFlagEvent.bind(this);
this.onRecordingStateChange = this.onRecordingStateChange.bind(this);
this.onViewSelected = this.onViewSelected.bind(this);
for (const [event] of EVENT_MAP_FLAGS) {
this._emitter.on(event, this.onFlagEvent.bind(this, event));
}
this._emitter.on(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
this._emitter.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
}
PerformanceTelemetry.prototype.destroy = function() {
if (this._previousView) {
this._telemetry.finishKeyed(
SELECTED_VIEW_HISTOGRAM_NAME,
this._previousView,
this,
false
);
}
for (const [event] of EVENT_MAP_FLAGS) {
this._emitter.off(event, this.onFlagEvent);
}
this._emitter.off(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
this._emitter.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
this._emitter = null;
};
PerformanceTelemetry.prototype.onFlagEvent = function(eventName, ...data) {
this._telemetry.getHistogramById(EVENT_MAP_FLAGS.get(eventName)).add(true);
};
PerformanceTelemetry.prototype.onRecordingStateChange = function(
status,
model
) {
if (status != "recording-stopped") {
return;
}
if (model.isConsole()) {
this._telemetry
.getHistogramById("DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT")
.add(true);
} else {
this._telemetry
.getHistogramById("DEVTOOLS_PERFTOOLS_RECORDING_COUNT")
.add(true);
}
this._telemetry
.getHistogramById("DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS")
.add(model.getDuration());
const config = model.getConfiguration();
for (const k in config) {
if (RECORDING_FEATURES.includes(k)) {
this._telemetry
.getKeyedHistogramById("DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED")
.add(k, config[k]);
}
}
};
PerformanceTelemetry.prototype.onViewSelected = function(viewName) {
if (this._previousView) {
this._telemetry.finishKeyed(
SELECTED_VIEW_HISTOGRAM_NAME,
this._previousView,
this,
false
);
}
this._previousView = viewName;
this._telemetry.startKeyed(SELECTED_VIEW_HISTOGRAM_NAME, viewName, this);
};
exports.PerformanceTelemetry = PerformanceTelemetry;

View File

@ -1,589 +0,0 @@
/* 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";
const {
JITOptimizations,
} = require("devtools/client/performance/modules/logic/jit");
const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
/**
* A call tree for a thread. This is essentially a linkage between all frames
* of all samples into a single tree structure, with additional information
* on each node, like the time spent (in milliseconds) and samples count.
*
* @param object thread
* The raw thread object received from the backend. Contains samples,
* stackTable, frameTable, and stringTable.
* @param object options
* Additional supported options
* - number startTime
* - number endTime
* - boolean contentOnly [optional]
* - boolean invertTree [optional]
* - boolean flattenRecursion [optional]
*/
function ThreadNode(thread, options = {}) {
if (options.endTime == void 0 || options.startTime == void 0) {
throw new Error("ThreadNode requires both `startTime` and `endTime`.");
}
this.samples = 0;
this.sampleTimes = [];
this.youngestFrameSamples = 0;
this.calls = [];
this.duration = options.endTime - options.startTime;
this.nodeType = "Thread";
this.inverted = options.invertTree;
// Total bytesize of all allocations if enabled
this.byteSize = 0;
this.youngestFrameByteSize = 0;
const { samples, stackTable, frameTable, stringTable } = thread;
// Nothing to do if there are no samples.
if (samples.data.length === 0) {
return;
}
this._buildInverted(samples, stackTable, frameTable, stringTable, options);
if (!options.invertTree) {
this._uninvert();
}
}
ThreadNode.prototype = {
/**
* Build an inverted call tree from profile samples. The format of the
* samples is described in tools/profiler/ProfileEntry.h, under the heading
* "Thread profile JSON Format".
*
* The profile data is naturally presented inverted. Inverting the call tree
* is also the default in the Performance tool.
*
* @param object samples
* The raw samples array received from the backend.
* @param object stackTable
* The table of deduplicated stacks from the backend.
* @param object frameTable
* The table of deduplicated frames from the backend.
* @param object stringTable
* The table of deduplicated strings from the backend.
* @param object options
* Additional supported options
* - number startTime
* - number endTime
* - boolean contentOnly [optional]
* - boolean invertTree [optional]
*/
_buildInverted: function buildInverted(
samples,
stackTable,
frameTable,
stringTable,
options
) {
function getOrAddFrameNode(
calls,
isLeaf,
frameKey,
inflatedFrame,
isMetaCategory,
leafTable
) {
// Insert the inflated frame into the call tree at the current level.
let frameNode;
// Leaf nodes have fan out much greater than non-leaf nodes, thus the
// use of a hash table. Otherwise, do linear search.
//
// Note that this method is very hot, thus the manual looping over
// Array.prototype.find.
if (isLeaf) {
frameNode = leafTable[frameKey];
} else {
for (let i = 0; i < calls.length; i++) {
if (calls[i].key === frameKey) {
frameNode = calls[i];
break;
}
}
}
if (!frameNode) {
frameNode = new FrameNode(frameKey, inflatedFrame, isMetaCategory);
if (isLeaf) {
leafTable[frameKey] = frameNode;
}
calls.push(frameNode);
}
return frameNode;
}
const SAMPLE_STACK_SLOT = samples.schema.stack;
const SAMPLE_TIME_SLOT = samples.schema.time;
const SAMPLE_BYTESIZE_SLOT = samples.schema.size;
const STACK_PREFIX_SLOT = stackTable.schema.prefix;
const STACK_FRAME_SLOT = stackTable.schema.frame;
const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
const samplesData = samples.data;
const stacksData = stackTable.data;
// Caches.
const inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
const leafTable = Object.create(null);
const startTime = options.startTime;
const endTime = options.endTime;
const flattenRecursion = options.flattenRecursion;
// Reused options object passed to InflatedFrame.prototype.getFrameKey.
const mutableFrameKeyOptions = {
contentOnly: options.contentOnly,
isRoot: false,
isLeaf: false,
isMetaCategoryOut: false,
};
let byteSize = 0;
for (let i = 0; i < samplesData.length; i++) {
const sample = samplesData[i];
const sampleTime = sample[SAMPLE_TIME_SLOT];
if (SAMPLE_BYTESIZE_SLOT !== void 0) {
byteSize = sample[SAMPLE_BYTESIZE_SLOT];
}
// A sample's end time is considered to be its time of sampling. Its
// start time is the sampling time of the previous sample.
//
// Thus, we compare sampleTime <= start instead of < to filter out
// samples that end exactly at the start time.
if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) {
continue;
}
let stackIndex = sample[SAMPLE_STACK_SLOT];
let calls = this.calls;
let prevCalls = this.calls;
let prevFrameKey;
let isLeaf = (mutableFrameKeyOptions.isLeaf = true);
const skipRoot = options.invertTree;
// Inflate the stack and build the FrameNode call tree directly.
//
// In the profiler data, each frame's stack is referenced by an index
// into stackTable.
//
// Each entry in stackTable is a pair [ prefixIndex, frameIndex ]. The
// prefixIndex is itself an index into stackTable, referencing the
// prefix of the current stack (that is, the younger frames). In other
// words, the stackTable is encoded as a trie of the inverted
// callstack. The frameIndex is an index into frameTable, describing the
// frame at the current depth.
//
// This algorithm inflates each frame in the frame table while walking
// the stack trie as described above.
//
// The frame key is then computed from the inflated frame /and/ the
// current depth in the FrameNode call tree. That is, the frame key is
// not wholly determinable from just the inflated frame.
//
// For content frames, the frame key is just its location. For chrome
// frames, the key may be a metacategory or its location, depending on
// rendering options and its position in the FrameNode call tree.
//
// The frame key is then used to build up the inverted FrameNode call
// tree.
//
// Note that various filtering functions, such as filtering for content
// frames or flattening recursion, are inlined into the stack inflation
// loop. This is important for performance as it avoids intermediate
// structures and multiple passes.
while (stackIndex !== null) {
const stackEntry = stacksData[stackIndex];
const frameIndex = stackEntry[STACK_FRAME_SLOT];
// Fetch the stack prefix (i.e. older frames) index.
stackIndex = stackEntry[STACK_PREFIX_SLOT];
// Do not include the (root) node in this sample, as the costs of each frame
// will make it clear to differentiate (root)->B vs (root)->A->B
// when a tree is inverted, a revert of bug 1147604
if (stackIndex === null && skipRoot) {
break;
}
// Inflate the frame.
const inflatedFrame = getOrAddInflatedFrame(
inflatedFrameCache,
frameIndex,
frameTable,
stringTable
);
// Compute the frame key.
mutableFrameKeyOptions.isRoot = stackIndex === null;
const frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
// An empty frame key means this frame should be skipped.
if (frameKey === "") {
continue;
}
// If we shouldn't flatten the current frame into the previous one, advance a
// level in the call tree.
const shouldFlatten = flattenRecursion && frameKey === prevFrameKey;
if (!shouldFlatten) {
calls = prevCalls;
}
const frameNode = getOrAddFrameNode(
calls,
isLeaf,
frameKey,
inflatedFrame,
mutableFrameKeyOptions.isMetaCategoryOut,
leafTable
);
if (isLeaf) {
frameNode.youngestFrameSamples++;
frameNode._addOptimizations(
inflatedFrame.optimizations,
inflatedFrame.implementation,
sampleTime,
stringTable
);
if (byteSize) {
frameNode.youngestFrameByteSize += byteSize;
}
}
// Don't overcount flattened recursive frames.
if (!shouldFlatten) {
frameNode.samples++;
if (byteSize) {
frameNode.byteSize += byteSize;
}
}
prevFrameKey = frameKey;
prevCalls = frameNode.calls;
isLeaf = mutableFrameKeyOptions.isLeaf = false;
}
this.samples++;
this.sampleTimes.push(sampleTime);
if (byteSize) {
this.byteSize += byteSize;
}
}
},
/**
* Uninverts the call tree after its having been built.
*/
_uninvert: function uninvert() {
function mergeOrAddFrameNode(calls, node, samples, size) {
// Unlike the inverted call tree, we don't use a root table for the top
// level, as in general, there are many fewer entry points than
// leaves. Instead, linear search is used regardless of level.
for (let i = 0; i < calls.length; i++) {
if (calls[i].key === node.key) {
const foundNode = calls[i];
foundNode._merge(node, samples, size);
return foundNode.calls;
}
}
const copy = node._clone(samples, size);
calls.push(copy);
return copy.calls;
}
const workstack = [{ node: this, level: 0 }];
const spine = [];
let entry;
// The new root.
const rootCalls = [];
// Walk depth-first and keep the current spine (e.g., callstack).
do {
entry = workstack.pop();
if (entry) {
spine[entry.level] = entry;
const node = entry.node;
const calls = node.calls;
let callSamples = 0;
let callByteSize = 0;
// Continue the depth-first walk.
for (let i = 0; i < calls.length; i++) {
workstack.push({ node: calls[i], level: entry.level + 1 });
callSamples += calls[i].samples;
callByteSize += calls[i].byteSize;
}
// The sample delta is used to distinguish stacks.
//
// Suppose we have the following stack samples:
//
// A -> B
// A -> C
// A
//
// The inverted tree is:
//
// A
// / \
// B C
//
// with A.samples = 3, B.samples = 1, C.samples = 1.
//
// A is distinguished as being its own stack because
// A.samples - (B.samples + C.samples) > 0.
//
// Note that bottoming out is a degenerate where callSamples = 0.
const samplesDelta = node.samples - callSamples;
const byteSizeDelta = node.byteSize - callByteSize;
if (samplesDelta > 0) {
// Reverse the spine and add them to the uninverted call tree.
let uninvertedCalls = rootCalls;
for (let level = entry.level; level > 0; level--) {
const callee = spine[level];
uninvertedCalls = mergeOrAddFrameNode(
uninvertedCalls,
callee.node,
samplesDelta,
byteSizeDelta
);
}
}
}
} while (entry);
// Replace the toplevel calls with rootCalls, which now contains the
// uninverted roots.
this.calls = rootCalls;
},
/**
* Gets additional details about this node.
* @see FrameNode.prototype.getInfo for more information.
*
* @return object
*/
getInfo: function(options) {
return FrameUtils.getFrameInfo(this, options);
},
/**
* Mimicks the interface of FrameNode, and a ThreadNode can never have
* optimization data (at the moment, anyway), so provide a function
* to return null so we don't need to check if a frame node is a thread
* or not everytime we fetch optimization data.
*
* @return {null}
*/
hasOptimizations: function() {
return null;
},
};
/**
* A function call node in a tree. Represents a function call with a unique context,
* resulting in each FrameNode having its own row in the corresponding tree view.
* Take samples:
* A()->B()->C()
* A()->B()
* Q()->B()
*
* In inverted tree, A()->B()->C() would have one frame node, and A()->B() and
* Q()->B() would share a frame node.
* In an uninverted tree, A()->B()->C() and A()->B() would share a frame node,
* with Q()->B() having its own.
*
* In all cases, all the frame nodes originated from the same InflatedFrame.
*
* @param string frameKey
* The key associated with this frame. The key determines identity of
* the node.
* @param string location
* The location of this function call. Note that this isn't sanitized,
* so it may very well (not?) include the function name, url, etc.
* @param number line
* The line number inside the source containing this function call.
* @param number category
* The category type of this function call ("js", "graphics" etc.).
* @param number allocations
* The number of memory allocations performed in this frame.
* @param number isContent
* Whether this frame is content.
* @param boolean isMetaCategory
* Whether or not this is a platform node that should appear as a
* generalized meta category or not.
*/
function FrameNode(
frameKey,
{ location, line, category, isContent },
isMetaCategory
) {
this.key = frameKey;
this.location = location;
this.line = line;
this.youngestFrameSamples = 0;
this.samples = 0;
this.calls = [];
this.isContent = !!isContent;
this._optimizations = null;
this._tierData = [];
this._stringTable = null;
this.isMetaCategory = !!isMetaCategory;
this.category = category;
this.nodeType = "Frame";
this.byteSize = 0;
this.youngestFrameByteSize = 0;
}
FrameNode.prototype = {
/**
* Take optimization data observed for this frame.
*
* @param object optimizationSite
* Any JIT optimization information attached to the current
* sample. Lazily inflated via stringTable.
* @param number implementation
* JIT implementation used for this observed frame (baseline, ion);
* can be null indicating "interpreter"
* @param number time
* The time this optimization occurred.
* @param object stringTable
* The string table used to inflate the optimizationSite.
*/
_addOptimizations: function(site, implementation, time, stringTable) {
// Simply accumulate optimization sites for now. Processing is done lazily
// by JITOptimizations, if optimization information is actually displayed.
if (site) {
let opts = this._optimizations;
if (opts === null) {
opts = this._optimizations = [];
}
opts.push(site);
}
if (!this._stringTable) {
this._stringTable = stringTable;
}
// Record type of implementation used and the sample time
this._tierData.push({ implementation, time });
},
_clone: function(samples, size) {
const newNode = new FrameNode(this.key, this, this.isMetaCategory);
newNode._merge(this, samples, size);
return newNode;
},
_merge: function(otherNode, samples, size) {
if (this === otherNode) {
return;
}
this.samples += samples;
this.byteSize += size;
if (otherNode.youngestFrameSamples > 0) {
this.youngestFrameSamples += samples;
}
if (otherNode.youngestFrameByteSize > 0) {
this.youngestFrameByteSize += otherNode.youngestFrameByteSize;
}
if (this._stringTable === null) {
this._stringTable = otherNode._stringTable;
}
if (otherNode._optimizations) {
if (!this._optimizations) {
this._optimizations = [];
}
const opts = this._optimizations;
const otherOpts = otherNode._optimizations;
for (let i = 0; i < otherOpts.length; i++) {
opts.push(otherOpts[i]);
}
}
if (otherNode._tierData.length) {
const tierData = this._tierData;
const otherTierData = otherNode._tierData;
for (let i = 0; i < otherTierData.length; i++) {
tierData.push(otherTierData[i]);
}
tierData.sort((a, b) => a.time - b.time);
}
},
/**
* Returns the parsed location and additional data describing
* this frame. Uses cached data if possible. Takes the following
* options:
*
* @param {ThreadNode} options.root
* The root thread node to calculate relative costs.
* Generates [self|total] [duration|percentage] values.
* @param {boolean} options.allocations
* Generates `totalAllocations` and `selfAllocations`.
*
* @return object
* The computed { name, file, url, line } properties for this
* function call, as well as additional params if options specified.
*/
getInfo: function(options) {
return FrameUtils.getFrameInfo(this, options);
},
/**
* Returns whether or not the frame node has an JITOptimizations model.
*
* @return {Boolean}
*/
hasOptimizations: function() {
return !this.isMetaCategory && !!this._optimizations;
},
/**
* Returns the underlying JITOptimizations model representing
* the optimization attempts occuring in this frame.
*
* @return {JITOptimizations|null}
*/
getOptimizations: function() {
if (!this._optimizations) {
return null;
}
return new JITOptimizations(this._optimizations, this._stringTable);
},
/**
* Returns the tiers used overtime.
*
* @return {Array<object>}
*/
getTierData: function() {
return this._tierData;
},
};
exports.ThreadNode = ThreadNode;
exports.FrameNode = FrameNode;

View File

@ -1,171 +0,0 @@
/* 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";
/**
* Utility functions for collapsing markers into a waterfall.
*/
const {
MarkerBlueprintUtils,
} = require("devtools/client/performance/modules/marker-blueprint-utils");
/**
* Creates a parent marker, which functions like a regular marker,
* but is able to hold additional child markers.
*
* The marker is seeded with values from `marker`.
* @param object marker
* @return object
*/
function createParentNode(marker) {
return Object.assign({}, marker, { submarkers: [] });
}
/**
* Collapses markers into a tree-like structure.
* @param object rootNode
* @param array markersList
* @param array filter
*/
function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
const {
getCurrentParentNode,
pushNode,
popParentNode,
} = createParentNodeFactory(rootNode);
for (let i = 0, len = markersList.length; i < len; i++) {
const curr = markersList[i];
// If this marker type should not be displayed, just skip
if (!MarkerBlueprintUtils.shouldDisplayMarker(curr, filter)) {
continue;
}
let parentNode = getCurrentParentNode();
const blueprint = MarkerBlueprintUtils.getBlueprintFor(curr);
const nestable = "nestable" in blueprint ? blueprint.nestable : true;
const collapsible =
"collapsible" in blueprint ? blueprint.collapsible : true;
let finalized = false;
// Extend the marker with extra properties needed in the marker tree
const extendedProps = { index: i };
if (collapsible) {
extendedProps.submarkers = [];
}
Object.assign(curr, extendedProps);
// If not nestible, just push it inside the root node. Additionally,
// markers originating outside the main thread are considered to be
// "never collapsible", to avoid confusion.
// A beter solution would be to collapse every marker with its siblings
// from the same thread, but that would require a thread id attached
// to all markers, which is potentially expensive and rather useless at
// the moment, since we don't really have that many OTMT markers.
if (!nestable || curr.isOffMainThread) {
pushNode(rootNode, curr);
continue;
}
// First off, if any parent nodes exist, finish them off
// recursively upwards if this marker is outside their ranges and nestable.
while (!finalized && parentNode) {
// If this marker is eclipsed by the current parent marker,
// make it a child of the current parent and stop going upwards.
// If the markers aren't from the same process, attach them to the root
// node as well. Every process has its own main thread.
if (
nestable &&
curr.start >= parentNode.start &&
curr.end <= parentNode.end &&
curr.processType == parentNode.processType
) {
pushNode(parentNode, curr);
finalized = true;
break;
}
// If this marker is still nestable, but outside of the range
// of the current parent, iterate upwards on the next parent
// and finalize the current parent.
if (nestable) {
popParentNode();
parentNode = getCurrentParentNode();
continue;
}
}
if (!finalized) {
pushNode(rootNode, curr);
}
}
}
/**
* Takes a root marker node and creates a hash of functions used
* to manage the creation and nesting of additional parent markers.
*
* @param {object} root
* @return {object}
*/
function createParentNodeFactory(root) {
const parentMarkers = [];
const factory = {
/**
* Pops the most recent parent node off the stack, finalizing it.
* Sets the `end` time based on the most recent child if not defined.
*/
popParentNode: () => {
if (parentMarkers.length === 0) {
throw new Error("Cannot pop parent markers when none exist.");
}
const lastParent = parentMarkers.pop();
// If this finished parent marker doesn't have an end time,
// so probably a synthesized marker, use the last marker's end time.
if (lastParent.end == void 0) {
lastParent.end =
lastParent.submarkers[lastParent.submarkers.length - 1].end;
}
// If no children were ever pushed into this parent node,
// remove its submarkers so it behaves like a non collapsible
// node.
if (!lastParent.submarkers.length) {
delete lastParent.submarkers;
}
return lastParent;
},
/**
* Returns the most recent parent node.
*/
getCurrentParentNode: () =>
parentMarkers.length ? parentMarkers[parentMarkers.length - 1] : null,
/**
* Push this marker into the most recent parent node.
*/
pushNode: (parent, marker) => {
parent.submarkers.push(marker);
// If pushing a parent marker, track it as the top of
// the parent stack.
if (marker.submarkers) {
parentMarkers.push(marker);
}
},
};
return factory;
}
exports.createParentNode = createParentNode;
exports.collapseMarkersIntoNode = collapseMarkersIntoNode;

View File

@ -1,110 +0,0 @@
/* 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";
const {
TIMELINE_BLUEPRINT,
} = require("devtools/client/performance/modules/markers");
/**
* This file contains utilities for parsing out the markers blueprint
* to generate strings to be displayed in the UI.
*/
exports.MarkerBlueprintUtils = {
/**
* Takes a marker and a list of marker names that should be hidden, and
* determines if this marker should be filtered or not.
*
* @param object marker
* @return boolean
*/
shouldDisplayMarker: function(marker, hiddenMarkerNames) {
if (!hiddenMarkerNames || hiddenMarkerNames.length == 0) {
return true;
}
// If this marker isn't yet defined in the blueprint, simply check if the
// entire category of "UNKNOWN" markers are supposed to be visible or not.
const isUnknown = !(marker.name in TIMELINE_BLUEPRINT);
if (isUnknown) {
return !hiddenMarkerNames.includes("UNKNOWN");
}
return !hiddenMarkerNames.includes(marker.name);
},
/**
* Takes a marker and returns the blueprint definition for that marker type,
* falling back to the UNKNOWN blueprint definition if undefined.
*
* @param object marker
* @return object
*/
getBlueprintFor: function(marker) {
return TIMELINE_BLUEPRINT[marker.name] || TIMELINE_BLUEPRINT.UNKNOWN;
},
/**
* Returns the label to display for a marker, based off the blueprints.
*
* @param object marker
* @return string
*/
getMarkerLabel: function(marker) {
const blueprint = this.getBlueprintFor(marker);
const dynamic = typeof blueprint.label === "function";
const label = dynamic ? blueprint.label(marker) : blueprint.label;
return label;
},
/**
* Returns the generic label to display for a marker name.
* (e.g. "Function Call" for JS markers, rather than "setTimeout", etc.)
*
* @param string type
* @return string
*/
getMarkerGenericName: function(markerName) {
const blueprint = this.getBlueprintFor({ name: markerName });
const dynamic = typeof blueprint.label === "function";
const generic = dynamic ? blueprint.label() : blueprint.label;
// If no class name found, attempt to throw a descriptive error as to
// how the marker implementor can fix this.
if (!generic) {
let message = `Could not find marker generic name for "${markerName}".`;
if (typeof blueprint.label === "function") {
message +=
` The following function must return a generic name string when no` +
` marker passed: ${blueprint.label}`;
} else {
message += ` ${markerName}.label must be defined in the marker blueprint.`;
}
throw new Error(message);
}
return generic;
},
/**
* Returns an array of objects with key/value pairs of what should be rendered
* in the marker details view.
*
* @param object marker
* @return array<object>
*/
getMarkerFields: function(marker) {
const blueprint = this.getBlueprintFor(marker);
const dynamic = typeof blueprint.fields === "function";
const fields = dynamic ? blueprint.fields(marker) : blueprint.fields;
return Object.entries(fields || {})
.filter(([_, value]) => (dynamic ? true : value in marker))
.map(([label, value]) => ({
label,
value: dynamic ? value : marker[value],
}));
},
};

View File

@ -1,275 +0,0 @@
/* 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";
/**
* This file contains utilities for creating DOM nodes for markers
* to be displayed in the UI.
*/
const { L10N, PREFS } = require("devtools/client/performance/modules/global");
const {
MarkerBlueprintUtils,
} = require("devtools/client/performance/modules/marker-blueprint-utils");
const { getSourceNames } = require("devtools/client/shared/source-utils");
/**
* Utilites for creating elements for markers.
*/
exports.MarkerDOMUtils = {
/**
* Builds all the fields possible for the given marker. Returns an
* array of elements to be appended to a parent element.
*
* @param document doc
* @param object marker
* @return array<Node>
*/
buildFields: function(doc, marker) {
const fields = MarkerBlueprintUtils.getMarkerFields(marker);
return fields.map(({ label, value }) =>
this.buildNameValueLabel(doc, label, value)
);
},
/**
* Builds the label representing the marker's type.
*
* @param document doc
* @param object marker
* @return Node
*/
buildTitle: function(doc, marker) {
const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
const hbox = doc.createXULElement("hbox");
hbox.setAttribute("align", "center");
const bullet = doc.createXULElement("hbox");
bullet.className = `marker-details-bullet marker-color-${blueprint.colorName}`;
const title = MarkerBlueprintUtils.getMarkerLabel(marker);
const label = doc.createXULElement("label");
label.className = "marker-details-type";
label.setAttribute("value", title);
hbox.appendChild(bullet);
hbox.appendChild(label);
return hbox;
},
/**
* Builds the label representing the marker's duration.
*
* @param document doc
* @param object marker
* @return Node
*/
buildDuration: function(doc, marker) {
const label = L10N.getStr("marker.field.duration");
const start = L10N.getFormatStrWithNumbers("timeline.tick", marker.start);
const end = L10N.getFormatStrWithNumbers("timeline.tick", marker.end);
const duration = L10N.getFormatStrWithNumbers(
"timeline.tick",
marker.end - marker.start
);
const el = this.buildNameValueLabel(doc, label, duration);
el.classList.add("marker-details-duration");
el.setAttribute("tooltiptext", `${start}${end}`);
return el;
},
/**
* Builds labels for name:value pairs.
* E.g. "Start: 100ms", "Duration: 200ms", ...
*
* @param document doc
* @param string field
* @param string value
* @return Node
*/
buildNameValueLabel: function(doc, field, value) {
const hbox = doc.createXULElement("hbox");
hbox.className = "marker-details-labelcontainer";
const nameLabel = doc.createXULElement("label");
nameLabel.className = "plain marker-details-name-label";
nameLabel.setAttribute("value", field);
hbox.appendChild(nameLabel);
const valueLabel = doc.createXULElement("label");
valueLabel.className = "plain marker-details-value-label";
valueLabel.setAttribute("value", value);
hbox.appendChild(valueLabel);
return hbox;
},
/**
* Builds a stack trace in an element.
*
* @param document doc
* @param object params
* An options object with the following members:
* - string type: string identifier for type of stack ("stack", "startStack"
or "endStack"
* - number frameIndex: the index of the topmost stack frame
* - array frames: array of stack frames
*/
buildStackTrace: function(doc, { type, frameIndex, frames }) {
const container = doc.createXULElement("vbox");
container.className = "marker-details-stack";
container.setAttribute("type", type);
const nameLabel = doc.createXULElement("label");
nameLabel.className = "plain marker-details-name-label";
nameLabel.setAttribute("value", L10N.getStr(`marker.field.${type}`));
container.appendChild(nameLabel);
// Workaround for profiles that have looping stack traces. See
// bug 1246555.
let wasAsyncParent = false;
const seen = new Set();
while (frameIndex > 0) {
if (seen.has(frameIndex)) {
break;
}
seen.add(frameIndex);
const frame = frames[frameIndex];
const url = frame.source;
const displayName = frame.functionDisplayName;
const line = frame.line;
// If the previous frame had an async parent, then the async
// cause is in this frame and should be displayed.
if (wasAsyncParent) {
const asyncStr = L10N.getFormatStr(
"marker.field.asyncStack",
frame.asyncCause
);
const asyncBox = doc.createXULElement("hbox");
const asyncLabel = doc.createXULElement("label");
asyncLabel.className = "devtools-monospace";
asyncLabel.setAttribute("value", asyncStr);
asyncBox.appendChild(asyncLabel);
container.appendChild(asyncBox);
wasAsyncParent = false;
}
const hbox = doc.createXULElement("hbox");
if (displayName) {
const functionLabel = doc.createXULElement("label");
functionLabel.className = "devtools-monospace";
functionLabel.setAttribute("value", displayName);
hbox.appendChild(functionLabel);
}
if (url) {
const linkNode = doc.createXULElement("a");
linkNode.className = "waterfall-marker-location devtools-source-link";
linkNode.href = url;
linkNode.draggable = false;
linkNode.setAttribute("title", url);
const urlLabel = doc.createXULElement("label");
urlLabel.className = "filename";
urlLabel.setAttribute("value", getSourceNames(url).short);
linkNode.appendChild(urlLabel);
const lineLabel = doc.createXULElement("label");
lineLabel.className = "line-number";
lineLabel.setAttribute("value", `:${line}`);
linkNode.appendChild(lineLabel);
hbox.appendChild(linkNode);
// Clicking here will bubble up to the parent,
// which handles the view source.
linkNode.setAttribute(
"data-action",
JSON.stringify({
url: url,
line: line,
action: "view-source",
})
);
}
if (!displayName && !url) {
const unknownLabel = doc.createXULElement("label");
unknownLabel.setAttribute(
"value",
L10N.getStr("marker.value.unknownFrame")
);
hbox.appendChild(unknownLabel);
}
container.appendChild(hbox);
if (frame.asyncParent) {
frameIndex = frame.asyncParent;
wasAsyncParent = true;
} else {
frameIndex = frame.parent;
}
}
return container;
},
/**
* Builds any custom fields specific to the marker.
*
* @param document doc
* @param object marker
* @param object options
* @return array<Node>
*/
buildCustom: function(doc, marker, options) {
const elements = [];
if (options.allocations && shouldShowAllocationsTrigger(marker)) {
const hbox = doc.createXULElement("hbox");
hbox.className = "marker-details-customcontainer";
const label = doc.createXULElement("label");
label.className = "custom-button";
label.setAttribute("value", "Show allocation triggers");
label.setAttribute("type", "show-allocations");
label.setAttribute(
"data-action",
JSON.stringify({
endTime: marker.start,
action: "show-allocations",
})
);
hbox.appendChild(label);
elements.push(hbox);
}
return elements;
},
};
/**
* Takes a marker and determines if this marker should display
* the allocations trigger button.
*
* @param object marker
* @return boolean
*/
function shouldShowAllocationsTrigger(marker) {
if (marker.name == "GarbageCollection") {
const showTriggers = PREFS["show-triggers-for-gc-types"];
return showTriggers.split(" ").includes(marker.causeName);
}
return false;
}

View File

@ -1,204 +0,0 @@
/* 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";
/**
* This file contains utilities for creating elements for markers to be displayed,
* and parsing out the blueprint to generate correct values for markers.
*/
const { L10N, PREFS } = require("devtools/client/performance/modules/global");
// String used to fill in platform data when it should be hidden.
const GECKO_SYMBOL = "(Gecko)";
/**
* Mapping of JS marker causes to a friendlier form. Only
* markers that are considered "from content" should be labeled here.
*/
const JS_MARKER_MAP = {
"<script> element": L10N.getStr("marker.label.javascript.scriptElement"),
"promise callback": L10N.getStr("marker.label.javascript.promiseCallback"),
"promise initializer": L10N.getStr("marker.label.javascript.promiseInit"),
"Worker runnable": L10N.getStr("marker.label.javascript.workerRunnable"),
"javascript: URI": L10N.getStr("marker.label.javascript.jsURI"),
// The difference between these two event handler markers are differences
// in their WebIDL implementation, so distinguishing them is not necessary.
EventHandlerNonNull: L10N.getStr("marker.label.javascript.eventHandler"),
"EventListener.handleEvent": L10N.getStr(
"marker.label.javascript.eventHandler"
),
// These markers do not get L10N'd because they're JS names.
"setInterval handler": "setInterval",
"setTimeout handler": "setTimeout",
FrameRequestCallback: "requestAnimationFrame",
};
/**
* A series of formatters used by the blueprint.
*/
exports.Formatters = {
/**
* Uses the marker name as the label for markers that do not have
* a blueprint entry. Uses "Other" in the marker filter menu.
*/
UnknownLabel: function(marker = {}) {
return marker.name || L10N.getStr("marker.label.unknown");
},
/* Group 0 - Reflow and Rendering pipeline */
StylesFields: function(marker) {
if ("isAnimationOnly" in marker) {
return {
[L10N.getStr("marker.field.isAnimationOnly")]: marker.isAnimationOnly,
};
}
return null;
},
/* Group 1 - JS */
DOMEventFields: function(marker) {
const fields = Object.create(null);
if ("type" in marker) {
fields[L10N.getStr("marker.field.DOMEventType")] = marker.type;
}
if ("eventPhase" in marker) {
let label;
switch (marker.eventPhase) {
case Event.AT_TARGET:
label = L10N.getStr("marker.value.DOMEventTargetPhase");
break;
case Event.CAPTURING_PHASE:
label = L10N.getStr("marker.value.DOMEventCapturingPhase");
break;
case Event.BUBBLING_PHASE:
label = L10N.getStr("marker.value.DOMEventBubblingPhase");
break;
}
fields[L10N.getStr("marker.field.DOMEventPhase")] = label;
}
return fields;
},
JSLabel: function(marker = {}) {
const generic = L10N.getStr("marker.label.javascript");
if ("causeName" in marker) {
return JS_MARKER_MAP[marker.causeName] || generic;
}
return generic;
},
JSFields: function(marker) {
if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
const label = PREFS["show-platform-data"]
? marker.causeName
: GECKO_SYMBOL;
return {
[L10N.getStr("marker.field.causeName")]: label,
};
}
return null;
},
GCLabel: function(marker) {
if (!marker) {
return L10N.getStr("marker.label.garbageCollection2");
}
// Only if a `nonincrementalReason` exists, do we want to label
// this as a non incremental GC event.
if ("nonincrementalReason" in marker) {
return L10N.getStr("marker.label.garbageCollection.nonIncremental");
}
return L10N.getStr("marker.label.garbageCollection.incremental");
},
GCFields: function(marker) {
const fields = Object.create(null);
if ("causeName" in marker) {
const cause = marker.causeName;
const label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
fields[L10N.getStr("marker.field.causeName")] = label;
}
if ("nonincrementalReason" in marker) {
const label = marker.nonincrementalReason;
fields[L10N.getStr("marker.field.nonIncrementalCause")] = label;
}
return fields;
},
MinorGCFields: function(marker) {
const fields = Object.create(null);
if ("causeName" in marker) {
const cause = marker.causeName;
const label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
fields[L10N.getStr("marker.field.causeName")] = label;
}
fields[L10N.getStr("marker.field.type")] = L10N.getStr(
"marker.nurseryCollection"
);
return fields;
},
CycleCollectionFields: function(marker) {
const label = marker.name.replace(/nsCycleCollector::/g, "");
return {
[L10N.getStr("marker.field.type")]: label,
};
},
WorkerFields: function(marker) {
if ("workerOperation" in marker) {
const label = L10N.getStr(`marker.worker.${marker.workerOperation}`);
return {
[L10N.getStr("marker.field.type")]: label,
};
}
return null;
},
MessagePortFields: function(marker) {
if ("messagePortOperation" in marker) {
const label = L10N.getStr(
`marker.messagePort.${marker.messagePortOperation}`
);
return {
[L10N.getStr("marker.field.type")]: label,
};
}
return null;
},
/* Group 2 - User Controlled */
ConsoleTimeFields: {
[L10N.getStr("marker.field.consoleTimerName")]: "causeName",
},
TimeStampFields: {
[L10N.getStr("marker.field.label")]: "causeName",
},
};
/**
* Takes a main label (e.g. "Timestamp") and a property name (e.g. "causeName"),
* and returns a string that represents that property value for a marker if it
* exists (e.g. "Timestamp (rendering)"), or just the main label if it does not.
*
* @param string mainLabel
* @param string propName
*/
exports.Formatters.labelForProperty = function(mainLabel, propName) {
return (marker = {}) =>
marker[propName] ? `${mainLabel} (${marker[propName]})` : mainLabel;
};

View File

@ -1,180 +0,0 @@
/* 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";
const { L10N } = require("devtools/client/performance/modules/global");
const {
Formatters,
} = require("devtools/client/performance/modules/marker-formatters");
/**
* A simple schema for mapping markers to the timeline UI. The keys correspond
* to marker names, while the values are objects with the following format:
*
* - group: The row index in the overview graph; multiple markers
* can be added on the same row. @see <overview.js/buildGraphImage>
* - label: The label used in the waterfall to identify the marker. Can be a
* string or just a function that accepts the marker and returns a
* string (if you want to use a dynamic property for the main label).
* If you use a function for a label, it *must* handle the case where
* no marker is provided, to get a generic label used to describe
* all markers of this type.
* - fields: The fields used in the marker details view to display more
* information about a currently selected marker. Can either be an
* object of fields, or simply a function that accepts the marker and
* returns such an object (if you want to use properties dynamically).
* For example, a field in the object such as { "Cause": "causeName" }
* would render something like `Cause: ${marker.causeName}` in the UI.
* - colorName: The label of the DevTools color used for this marker. If
* adding a new color, be sure to check that there's an entry
* for `.marker-color-graphs-{COLORNAME}` for the equivilent
* entry in "./devtools/client/themes/performance.css"
* - collapsible: Whether or not this marker can contain other markers it
* eclipses, and becomes collapsible to reveal its nestable
* children. Defaults to true.
* - nestable: Whether or not this marker can be nested inside an eclipsing
* collapsible marker. Defaults to true.
*/
const TIMELINE_BLUEPRINT = {
/* Default definition used for markers that occur but are not defined here.
* Should ultimately be defined, but this gives us room to work on the
* front end separately from the platform. */
UNKNOWN: {
group: 2,
colorName: "graphs-grey",
label: Formatters.UnknownLabel,
},
/* Group 0 - Reflow and Rendering pipeline */
Styles: {
group: 0,
colorName: "graphs-purple",
label: L10N.getStr("marker.label.styles"),
fields: Formatters.StylesFields,
},
StylesApplyChanges: {
group: 0,
colorName: "graphs-purple",
label: L10N.getStr("marker.label.stylesApplyChanges"),
},
Reflow: {
group: 0,
colorName: "graphs-purple",
label: L10N.getStr("marker.label.reflow"),
},
Paint: {
group: 0,
colorName: "graphs-green",
label: L10N.getStr("marker.label.paint"),
},
Composite: {
group: 0,
colorName: "graphs-green",
label: L10N.getStr("marker.label.composite"),
},
CompositeForwardTransaction: {
group: 0,
colorName: "graphs-bluegrey",
label: L10N.getStr("marker.label.compositeForwardTransaction"),
},
/* Group 1 - JS */
DOMEvent: {
group: 1,
colorName: "graphs-yellow",
label: L10N.getStr("marker.label.domevent"),
fields: Formatters.DOMEventFields,
},
"document::DOMContentLoaded": {
group: 1,
colorName: "graphs-full-red",
label: "DOMContentLoaded",
},
"document::Load": {
group: 1,
colorName: "graphs-full-blue",
label: "Load",
},
Javascript: {
group: 1,
colorName: "graphs-yellow",
label: Formatters.JSLabel,
fields: Formatters.JSFields,
},
"Parse HTML": {
group: 1,
colorName: "graphs-yellow",
label: L10N.getStr("marker.label.parseHTML"),
},
"Parse XML": {
group: 1,
colorName: "graphs-yellow",
label: L10N.getStr("marker.label.parseXML"),
},
GarbageCollection: {
group: 1,
colorName: "graphs-red",
label: Formatters.GCLabel,
fields: Formatters.GCFields,
},
MinorGC: {
group: 1,
colorName: "graphs-red",
label: L10N.getStr("marker.label.minorGC"),
fields: Formatters.MinorGCFields,
},
"nsCycleCollector::Collect": {
group: 1,
colorName: "graphs-red",
label: L10N.getStr("marker.label.cycleCollection"),
fields: Formatters.CycleCollectionFields,
},
"nsCycleCollector::ForgetSkippable": {
group: 1,
colorName: "graphs-red",
label: L10N.getStr("marker.label.cycleCollection.forgetSkippable"),
fields: Formatters.CycleCollectionFields,
},
Worker: {
group: 1,
colorName: "graphs-orange",
label: L10N.getStr("marker.label.worker"),
fields: Formatters.WorkerFields,
},
MessagePort: {
group: 1,
colorName: "graphs-orange",
label: L10N.getStr("marker.label.messagePort"),
fields: Formatters.MessagePortFields,
},
/* Group 2 - User Controlled */
ConsoleTime: {
group: 2,
colorName: "graphs-blue",
label: Formatters.labelForProperty(
L10N.getStr("marker.label.consoleTime"),
"causeName"
),
fields: Formatters.ConsoleTimeFields,
nestable: false,
collapsible: false,
},
TimeStamp: {
group: 2,
colorName: "graphs-blue",
label: Formatters.labelForProperty(
L10N.getStr("marker.label.timestamp"),
"causeName"
),
fields: Formatters.TimeStampFields,
collapsible: false,
},
};
// Exported symbols.
exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;

View File

@ -1,22 +0,0 @@
# vim: set filetype=python:
# 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/.
DIRS += [
"logic",
"widgets",
]
DevToolsModules(
"categories.js",
"constants.js",
"global.js",
"io.js",
"marker-blueprint-utils.js",
"marker-dom-utils.js",
"marker-formatters.js",
"markers.js",
"utils.js",
"waterfall-ticks.js",
)

View File

@ -1,24 +0,0 @@
/* 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";
/* globals document */
/**
* React components grab the namespace of the element they are mounting to. This function
* takes a XUL element, and makes sure to create a properly namespaced HTML element to
* avoid React creating XUL elements.
*
* {XULElement} xulElement
* return {HTMLElement} div
*/
exports.createHtmlMount = function(xulElement) {
const htmlElement = document.createElementNS(
"http://www.w3.org/1999/xhtml",
"div"
);
xulElement.appendChild(htmlElement);
return htmlElement;
};

View File

@ -1,98 +0,0 @@
/* 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";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
/**
* Creates the background displayed on the marker's waterfall.
*/
function drawWaterfallBackground(doc, dataScale, waterfallWidth) {
const canvas = doc.createElementNS(HTML_NS, "canvas");
const ctx = canvas.getContext("2d");
// Nuke the context.
const canvasWidth = (canvas.width = Math.max(waterfallWidth, 1));
// Awww yeah, 1px, repeats on Y axis.
const canvasHeight = (canvas.height = 1);
// Start over.
const imageData = ctx.createImageData(canvasWidth, canvasHeight);
const pixelArray = imageData.data;
const buf = new ArrayBuffer(pixelArray.length);
const view8bit = new Uint8ClampedArray(buf);
const view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
const [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
const tickInterval = findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale,
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
const increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
const position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
doc.mozSetImageElement("waterfall-background", canvas);
return canvas;
}
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
function findOptimalTickInterval({
ticksMultiple,
ticksSpacingMin,
dataScale,
}) {
let timingStep = ticksMultiple;
const maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) {
const scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
}
exports.TickUtils = { findOptimalTickInterval, drawWaterfallBackground };

View File

@ -1,527 +0,0 @@
/* 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";
/**
* This file contains the base line graph that all Performance line graphs use.
*/
const { extend } = require("devtools/shared/extend");
const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
const EventEmitter = require("devtools/shared/event-emitter");
const { colorUtils } = require("devtools/shared/css/color");
const { getColor } = require("devtools/client/shared/theme");
const ProfilerGlobal = require("devtools/client/performance/modules/global");
const {
MarkersOverview,
} = require("devtools/client/performance/modules/widgets/markers-overview");
const {
createTierGraphDataFromFrameNode,
} = require("devtools/client/performance/modules/logic/jit");
/**
* For line graphs
*/
const HEIGHT = 35; // px
const STROKE_WIDTH = 1; // px
const DAMPEN_VALUES = 0.95;
const CLIPHEAD_LINE_COLOR = "#666";
const SELECTION_LINE_COLOR = "#555";
const SELECTION_BACKGROUND_COLOR_NAME = "graphs-blue";
const FRAMERATE_GRAPH_COLOR_NAME = "graphs-green";
const MEMORY_GRAPH_COLOR_NAME = "graphs-blue";
/**
* For timeline overview
*/
const MARKERS_GRAPH_HEADER_HEIGHT = 14; // px
const MARKERS_GRAPH_ROW_HEIGHT = 10; // px
const MARKERS_GROUP_VERTICAL_PADDING = 4; // px
/**
* For optimization graph
*/
const OPTIMIZATIONS_GRAPH_RESOLUTION = 100;
/**
* A base class for performance graphs to inherit from.
*
* @param Node parent
* The parent node holding the overview.
* @param string metric
* The unit of measurement for this graph.
*/
function PerformanceGraph(parent, metric) {
LineGraphWidget.call(this, parent, { metric });
this.setTheme();
}
PerformanceGraph.prototype = extend(LineGraphWidget.prototype, {
strokeWidth: STROKE_WIDTH,
dampenValuesFactor: DAMPEN_VALUES,
fixedHeight: HEIGHT,
clipheadLineColor: CLIPHEAD_LINE_COLOR,
selectionLineColor: SELECTION_LINE_COLOR,
withTooltipArrows: false,
withFixedTooltipPositions: true,
/**
* Disables selection and empties this graph.
*/
clearView: function() {
this.selectionEnabled = false;
this.dropSelection();
this.setData([]);
},
/**
* Sets the theme via `theme` to either "light" or "dark",
* and updates the internal styling to match. Requires a redraw
* to see the effects.
*/
setTheme: function(theme) {
theme = theme || "light";
const mainColor = getColor(this.mainColor || "graphs-blue", theme);
this.backgroundColor = getColor("body-background", theme);
this.strokeColor = mainColor;
this.backgroundGradientStart = colorUtils.setAlpha(mainColor, 0.2);
this.backgroundGradientEnd = colorUtils.setAlpha(mainColor, 0.2);
this.selectionBackgroundColor = colorUtils.setAlpha(
getColor(SELECTION_BACKGROUND_COLOR_NAME, theme),
0.25
);
this.selectionStripesColor = "rgba(255, 255, 255, 0.1)";
this.maximumLineColor = colorUtils.setAlpha(mainColor, 0.4);
this.averageLineColor = colorUtils.setAlpha(mainColor, 0.7);
this.minimumLineColor = colorUtils.setAlpha(mainColor, 0.9);
},
});
/**
* Constructor for the framerate graph. Inherits from PerformanceGraph.
*
* @param Node parent
* The parent node holding the overview.
*/
function FramerateGraph(parent) {
PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.fps"));
}
FramerateGraph.prototype = extend(PerformanceGraph.prototype, {
mainColor: FRAMERATE_GRAPH_COLOR_NAME,
setPerformanceData: function({ duration, ticks }, resolution) {
this.dataDuration = duration;
return this.setDataFromTimestamps(ticks, resolution, duration);
},
});
/**
* Constructor for the memory graph. Inherits from PerformanceGraph.
*
* @param Node parent
* The parent node holding the overview.
*/
function MemoryGraph(parent) {
PerformanceGraph.call(
this,
parent,
ProfilerGlobal.L10N.getStr("graphs.memory")
);
}
MemoryGraph.prototype = extend(PerformanceGraph.prototype, {
mainColor: MEMORY_GRAPH_COLOR_NAME,
setPerformanceData: function({ duration, memory }) {
this.dataDuration = duration;
return this.setData(memory);
},
});
function TimelineGraph(parent, filter) {
MarkersOverview.call(this, parent, filter);
}
TimelineGraph.prototype = extend(MarkersOverview.prototype, {
headerHeight: MARKERS_GRAPH_HEADER_HEIGHT,
rowHeight: MARKERS_GRAPH_ROW_HEIGHT,
groupPadding: MARKERS_GROUP_VERTICAL_PADDING,
setPerformanceData: MarkersOverview.prototype.setData,
});
/**
* Definitions file for GraphsController, indicating the constructor,
* selector and other meta for each of the graphs controller by
* GraphsController.
*/
const GRAPH_DEFINITIONS = {
memory: {
constructor: MemoryGraph,
selector: "#memory-overview",
},
framerate: {
constructor: FramerateGraph,
selector: "#time-framerate",
},
timeline: {
constructor: TimelineGraph,
selector: "#markers-overview",
primaryLink: true,
},
};
/**
* A controller for orchestrating the performance's tool overview graphs. Constructs,
* syncs, toggles displays and defines the memory, framerate and timeline view.
*
* @param {object} definition
* @param {DOMElement} root
* @param {function} getFilter
* @param {function} getTheme
*/
function GraphsController({ definition, root, getFilter, getTheme }) {
this._graphs = {};
this._enabled = new Set();
this._definition = definition || GRAPH_DEFINITIONS;
this._root = root;
this._getFilter = getFilter;
this._getTheme = getTheme;
this._primaryLink = Object.keys(this._definition).filter(
name => this._definition[name].primaryLink
)[0];
this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument);
EventEmitter.decorate(this);
this._onSelecting = this._onSelecting.bind(this);
}
GraphsController.prototype = {
/**
* Returns the corresponding graph by `graphName`.
*/
get: function(graphName) {
return this._graphs[graphName];
},
/**
* Iterates through all graphs and renders the data
* from a RecordingModel. Takes a resolution value used in
* some graphs.
* Saves rendering progress as a promise to be consumed by `destroy`,
* to wait for cleaning up rendering during destruction.
*/
async render(recordingData, resolution) {
// Get the previous render promise so we don't start rendering
// until the previous render cycle completes, which can occur
// especially when a recording is finished, and triggers a
// fresh rendering at a higher rate
await this._rendering;
// Check after yielding to ensure we're not tearing down,
// as this can create a race condition in tests
if (this._destroyed) {
return;
}
this._rendering = (async () => {
for (const graph of await this._getEnabled()) {
await graph.setPerformanceData(recordingData, resolution);
this.emit("rendered", graph.graphName);
}
})();
await this._rendering;
},
/**
* Destroys the underlying graphs.
*/
async destroy() {
const primary = this._getPrimaryLink();
this._destroyed = true;
if (primary) {
primary.off("selecting", this._onSelecting);
}
// If there was rendering, wait until the most recent render cycle
// has finished
if (this._rendering) {
await this._rendering;
}
for (const graph of this.getWidgets()) {
await graph.destroy();
}
},
/**
* Applies the theme to the underlying graphs. Optionally takes
* a `redraw` boolean in the options to force redraw.
*/
setTheme: function(options = {}) {
const theme = options.theme || this._getTheme();
for (const graph of this.getWidgets()) {
graph.setTheme(theme);
graph.refresh({ force: options.redraw });
}
},
/**
* Sets up the graph, if needed. Returns a promise resolving
* to the graph if it is enabled once it's ready, or otherwise returns
* null if disabled.
*/
async isAvailable(graphName) {
if (!this._enabled.has(graphName)) {
return null;
}
let graph = this.get(graphName);
if (!graph) {
graph = await this._construct(graphName);
}
await graph.ready();
return graph;
},
/**
* Enable or disable a subgraph controlled by GraphsController.
* This determines what graphs are visible and get rendered.
*/
enable: function(graphName, isEnabled) {
const el = this.$(this._definition[graphName].selector);
el.classList[isEnabled ? "remove" : "add"]("hidden");
// If no status change, just return
if (this._enabled.has(graphName) === isEnabled) {
return;
}
if (isEnabled) {
this._enabled.add(graphName);
} else {
this._enabled.delete(graphName);
}
// Invalidate our cache of ready-to-go graphs
this._enabledGraphs = null;
},
/**
* Disables all graphs controller by the GraphsController, and
* also hides the root element. This is a one way switch, and used
* when older platforms do not have any timeline data.
*/
disableAll: function() {
this._root.classList.add("hidden");
// Hide all the subelements
Object.keys(this._definition).forEach(graphName =>
this.enable(graphName, false)
);
},
/**
* Sets a mapped selection on the graph that is the main controller
* for keeping the graphs' selections in sync.
*/
setMappedSelection: function(selection, { mapStart, mapEnd }) {
return this._getPrimaryLink().setMappedSelection(selection, {
mapStart,
mapEnd,
});
},
/**
* Fetches the currently mapped selection. If graphs are not yet rendered,
* (which throws in Graphs.js), return null.
*/
getMappedSelection: function({ mapStart, mapEnd }) {
const primary = this._getPrimaryLink();
if (primary && primary.hasData()) {
return primary.getMappedSelection({ mapStart, mapEnd });
}
return null;
},
/**
* Returns an array of graphs that have been created, not necessarily
* enabled currently.
*/
getWidgets: function() {
return Object.keys(this._graphs).map(name => this._graphs[name]);
},
/**
* Drops the selection.
*/
dropSelection: function() {
if (this._getPrimaryLink()) {
return this._getPrimaryLink().dropSelection();
}
return null;
},
/**
* Makes sure the selection is enabled or disabled in all the graphs.
*/
async selectionEnabled(enabled) {
for (const graph of await this._getEnabled()) {
graph.selectionEnabled = enabled;
}
},
/**
* Creates the graph `graphName` and initializes it.
*/
async _construct(graphName) {
const def = this._definition[graphName];
const el = this.$(def.selector);
const filter = this._getFilter();
const graph = (this._graphs[graphName] = new def.constructor(el, filter));
graph.graphName = graphName;
await graph.ready();
// Sync the graphs' animations and selections together
if (def.primaryLink) {
graph.on("selecting", this._onSelecting);
} else {
CanvasGraphUtils.linkAnimation(this._getPrimaryLink(), graph);
CanvasGraphUtils.linkSelection(this._getPrimaryLink(), graph);
}
// Sets the container element's visibility based off of enabled status
el.classList[this._enabled.has(graphName) ? "remove" : "add"]("hidden");
this.setTheme();
return graph;
},
/**
* Returns the main graph for this collection, that all graphs
* are bound to for syncing and selection.
*/
_getPrimaryLink: function() {
return this.get(this._primaryLink);
},
/**
* Emitted when a selection occurs.
*/
_onSelecting: function() {
this.emit("selecting");
},
/**
* Resolves to an array with all graphs that are enabled, and
* creates them if needed. Different than just iterating over `this._graphs`,
* as those could be enabled. Uses caching, as rendering happens many times per second,
* compared to how often which graphs/features are changed (rarely).
*/
async _getEnabled() {
if (this._enabledGraphs) {
return this._enabledGraphs;
}
const enabled = [];
for (const graphName of this._enabled) {
const graph = await this.isAvailable(graphName);
if (graph) {
enabled.push(graph);
}
}
this._enabledGraphs = enabled;
return this._enabledGraphs;
},
};
/**
* A base class for performance graphs to inherit from.
*
* @param Node parent
* The parent node holding the overview.
* @param string metric
* The unit of measurement for this graph.
*/
function OptimizationsGraph(parent) {
MountainGraphWidget.call(this, parent);
this.setTheme();
}
OptimizationsGraph.prototype = extend(MountainGraphWidget.prototype, {
async render(threadNode, frameNode) {
// Regardless if we draw or clear the graph, wait
// until it's ready.
await this.ready();
if (!threadNode || !frameNode) {
this.setData([]);
return;
}
const { sampleTimes } = threadNode;
if (!sampleTimes.length) {
this.setData([]);
return;
}
// Take startTime/endTime from samples recorded, rather than
// using duration directly from threadNode, as the first sample that
// equals the startTime does not get recorded.
const startTime = sampleTimes[0];
const endTime = sampleTimes[sampleTimes.length - 1];
const bucketSize = (endTime - startTime) / OPTIMIZATIONS_GRAPH_RESOLUTION;
const data = createTierGraphDataFromFrameNode(
frameNode,
sampleTimes,
bucketSize
);
// If for some reason we don't have data (like the frameNode doesn't
// have optimizations, but it shouldn't be at this point if it doesn't),
// log an error.
if (!data) {
console.error(
`FrameNode#${frameNode.location} does not have optimizations data to render.`
);
return;
}
this.dataOffsetX = startTime;
await this.setData(data);
},
/**
* Sets the theme via `theme` to either "light" or "dark",
* and updates the internal styling to match. Requires a redraw
* to see the effects.
*/
setTheme: function(theme) {
theme = theme || "light";
const interpreterColor = getColor("graphs-red", theme);
const baselineColor = getColor("graphs-blue", theme);
const ionColor = getColor("graphs-green", theme);
this.format = [
{ color: interpreterColor },
{ color: baselineColor },
{ color: ionColor },
];
this.backgroundColor = getColor("sidebar-background", theme);
},
});
exports.OptimizationsGraph = OptimizationsGraph;
exports.FramerateGraph = FramerateGraph;
exports.MemoryGraph = MemoryGraph;
exports.TimelineGraph = TimelineGraph;
exports.GraphsController = GraphsController;

View File

@ -1,177 +0,0 @@
/* 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";
/**
* This file contains the rendering code for the marker sidebar.
*/
const EventEmitter = require("devtools/shared/event-emitter");
const {
MarkerDOMUtils,
} = require("devtools/client/performance/modules/marker-dom-utils");
/**
* A detailed view for one single marker.
*
* @param Node parent
* The parent node holding the view.
* @param Node splitter
* The splitter node that the resize event is bound to.
*/
function MarkerDetails(parent, splitter) {
EventEmitter.decorate(this);
this._document = parent.ownerDocument;
this._parent = parent;
this._splitter = splitter;
this._onClick = this._onClick.bind(this);
this._onSplitterMouseUp = this._onSplitterMouseUp.bind(this);
this._parent.addEventListener("click", this._onClick);
this._splitter.addEventListener("mouseup", this._onSplitterMouseUp);
this.hidden = true;
}
MarkerDetails.prototype = {
/**
* Sets this view's width.
* @param number
*/
set width(value) {
this._parent.setAttribute("width", value);
},
/**
* Sets this view's width.
* @return number
*/
get width() {
return +this._parent.getAttribute("width");
},
/**
* Sets this view's visibility.
* @param boolean
*/
set hidden(value) {
if (this._parent.hidden != value) {
this._parent.hidden = value;
this.emit("resize");
}
},
/**
* Gets this view's visibility.
* @param boolean
*/
get hidden() {
return this._parent.hidden;
},
/**
* Clears the marker details from this view.
*/
empty: function() {
this._parent.innerHTML = "";
},
/**
* Populates view with marker's details.
*
* @param object params
* An options object holding:
* - marker: The marker to display.
* - frames: Array of stack frame information; see stack.js.
* - allocations: Whether or not allocations were enabled for this
* recording. [optional]
*/
render: function(options) {
const { marker, frames } = options;
this.empty();
const elements = [];
elements.push(MarkerDOMUtils.buildTitle(this._document, marker));
elements.push(MarkerDOMUtils.buildDuration(this._document, marker));
MarkerDOMUtils.buildFields(this._document, marker).forEach(f =>
elements.push(f)
);
MarkerDOMUtils.buildCustom(this._document, marker, options).forEach(f =>
elements.push(f)
);
// Build a stack element -- and use the "startStack" label if
// we have both a startStack and endStack.
if (marker.stack) {
const type = marker.endStack ? "startStack" : "stack";
elements.push(
MarkerDOMUtils.buildStackTrace(this._document, {
frameIndex: marker.stack,
frames,
type,
})
);
}
if (marker.endStack) {
const type = "endStack";
elements.push(
MarkerDOMUtils.buildStackTrace(this._document, {
frameIndex: marker.endStack,
frames,
type,
})
);
}
elements.forEach(el => this._parent.appendChild(el));
},
/**
* Handles click in the marker details view. Based on the target,
* can handle different actions -- only supporting view source links
* for the moment.
*/
_onClick: function(e) {
const data = findActionFromEvent(e.target, this._parent);
if (!data) {
return;
}
this.emit(data.action, data);
},
/**
* Handles the "mouseup" event on the marker details view splitter.
*/
_onSplitterMouseUp: function() {
this.emit("resize");
},
};
/**
* Take an element from an event `target`, and ascend through
* the DOM, looking for an element with a `data-action` attribute. Return
* the parsed `data-action` value found, or null if none found before
* reaching the parent `container`.
*
* @param {Element} target
* @param {Element} container
* @return {?object}
*/
function findActionFromEvent(target, container) {
let el = target;
let action;
while (el !== container) {
action = el.getAttribute("data-action");
if (action) {
return JSON.parse(action);
}
el = el.parentNode;
}
return null;
}
exports.MarkerDetails = MarkerDetails;

View File

@ -1,256 +0,0 @@
/* 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";
/**
* This file contains the "markers overview" graph, which is a minimap of all
* the timeline data. Regions inside it may be selected, determining which
* markers are visible in the "waterfall".
*/
const { extend } = require("devtools/shared/extend");
const {
AbstractCanvasGraph,
} = require("devtools/client/shared/widgets/Graphs");
const { colorUtils } = require("devtools/shared/css/color");
const { getColor } = require("devtools/client/shared/theme");
const ProfilerGlobal = require("devtools/client/performance/modules/global");
const {
MarkerBlueprintUtils,
} = require("devtools/client/performance/modules/marker-blueprint-utils");
const {
TickUtils,
} = require("devtools/client/performance/modules/waterfall-ticks");
const {
TIMELINE_BLUEPRINT,
} = require("devtools/client/performance/modules/markers");
const OVERVIEW_HEADER_HEIGHT = 14; // px
const OVERVIEW_ROW_HEIGHT = 11; // px
const OVERVIEW_SELECTION_LINE_COLOR = "#666";
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
const OVERVIEW_MARKER_WIDTH_MIN = 4; // px
const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
/**
* An overview for the markers data.
*
* @param Node parent
* The parent node holding the overview.
* @param Array<String> filter
* List of names of marker types that should not be shown.
*/
function MarkersOverview(parent, filter = [], ...args) {
AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
this.setTheme();
this.setFilter(filter);
}
MarkersOverview.prototype = extend(AbstractCanvasGraph.prototype, {
clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
headerHeight: OVERVIEW_HEADER_HEIGHT,
rowHeight: OVERVIEW_ROW_HEIGHT,
groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
/**
* Compute the height of the overview.
*/
get fixedHeight() {
return this.headerHeight + this.rowHeight * this._numberOfGroups;
},
/**
* List of marker types that should not be shown in the graph.
*/
setFilter: function(filter) {
this._paintBatches = new Map();
this._filter = filter;
this._groupMap = Object.create(null);
const observedGroups = new Set();
for (const type in TIMELINE_BLUEPRINT) {
if (filter.includes(type)) {
continue;
}
this._paintBatches.set(type, {
definition: TIMELINE_BLUEPRINT[type],
batch: [],
});
observedGroups.add(TIMELINE_BLUEPRINT[type].group);
}
// Take our set of observed groups and order them and map
// the group numbers to fill in the holes via `_groupMap`.
// This normalizes our rows by removing rows that aren't used
// if filters are enabled.
let actualPosition = 0;
for (const groupNumber of Array.from(observedGroups).sort()) {
this._groupMap[groupNumber] = actualPosition++;
}
this._numberOfGroups = Object.keys(this._groupMap).length;
},
/**
* Disables selection and empties this graph.
*/
clearView: function() {
this.selectionEnabled = false;
this.dropSelection();
this.setData({ duration: 0, markers: [] });
},
/**
* Renders the graph's data source.
* @see AbstractCanvasGraph.prototype.buildGraphImage
*/
buildGraphImage: function() {
const { markers, duration } = this._data;
const { canvas, ctx } = this._getNamedCanvas("markers-overview-data");
const canvasWidth = this._width;
const canvasHeight = this._height;
// Group markers into separate paint batches. This is necessary to
// draw all markers sharing the same style at once.
for (const marker of markers) {
// Again skip over markers that we're filtering -- we don't want them
// to be labeled as "Unknown"
if (!MarkerBlueprintUtils.shouldDisplayMarker(marker, this._filter)) {
continue;
}
const markerType =
this._paintBatches.get(marker.name) ||
this._paintBatches.get("UNKNOWN");
markerType.batch.push(marker);
}
// Calculate each row's height, and the time-based scaling.
const groupHeight = this.rowHeight * this._pixelRatio;
const groupPadding = this.groupPadding * this._pixelRatio;
const headerHeight = this.headerHeight * this._pixelRatio;
const dataScale = (this.dataScaleX = canvasWidth / duration);
// Draw the header and overview background.
ctx.fillStyle = this.headerBackgroundColor;
ctx.fillRect(0, 0, canvasWidth, headerHeight);
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
// Draw the alternating odd/even group backgrounds.
ctx.fillStyle = this.alternatingBackgroundColor;
ctx.beginPath();
for (let i = 0; i < this._numberOfGroups; i += 2) {
const top = headerHeight + i * groupHeight;
ctx.rect(0, top, canvasWidth, groupHeight);
}
ctx.fill();
// Draw the timeline header ticks.
const fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
const fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
const textPaddingLeft =
OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
const textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
const tickInterval = TickUtils.findOptimalTickInterval({
ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale,
});
ctx.textBaseline = "middle";
ctx.font = fontSize + "px " + fontFamily;
ctx.fillStyle = this.headerTextColor;
ctx.strokeStyle = this.headerTimelineStrokeColor;
ctx.beginPath();
for (let x = 0; x < canvasWidth; x += tickInterval) {
const lineLeft = x;
const textLeft = lineLeft + textPaddingLeft;
const time = Math.round(x / dataScale);
const label = ProfilerGlobal.L10N.getFormatStr("timeline.tick", time);
ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
ctx.moveTo(lineLeft, 0);
ctx.lineTo(lineLeft, canvasHeight);
}
ctx.stroke();
// Draw the timeline markers.
for (const [, { definition, batch }] of this._paintBatches) {
const group = this._groupMap[definition.group];
const top = headerHeight + group * groupHeight + groupPadding / 2;
const height = groupHeight - groupPadding;
const color = getColor(definition.colorName, this.theme);
ctx.fillStyle = color;
ctx.beginPath();
for (const { start, end } of batch) {
const left = start * dataScale;
const width = Math.max(
(end - start) * dataScale,
OVERVIEW_MARKER_WIDTH_MIN
);
ctx.rect(left, top, width, height);
}
ctx.fill();
// Since all the markers in this batch (thus sharing the same style) have
// been drawn, empty it. The next time new markers will be available,
// they will be sorted and drawn again.
batch.length = 0;
}
return canvas;
},
/**
* Sets the theme via `theme` to either "light" or "dark",
* and updates the internal styling to match. Requires a redraw
* to see the effects.
*/
setTheme: function(theme) {
this.theme = theme = theme || "light";
this.backgroundColor = getColor("body-background", theme);
this.selectionBackgroundColor = colorUtils.setAlpha(
getColor("selection-background", theme),
0.25
);
this.selectionStripesColor = colorUtils.setAlpha("#fff", 0.1);
this.headerBackgroundColor = getColor("body-background", theme);
this.headerTextColor = getColor("body-color", theme);
this.headerTimelineStrokeColor = colorUtils.setAlpha(
getColor("text-color-alt", theme),
0.25
);
this.alternatingBackgroundColor = colorUtils.setAlpha(
getColor("body-color", theme),
0.05
);
},
});
exports.MarkersOverview = MarkersOverview;

View File

@ -1,11 +0,0 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
"graphs.js",
"marker-details.js",
"markers-overview.js",
"tree-view.js",
)

View File

@ -1,461 +0,0 @@
/* 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";
/**
* This file contains the tree view, displaying all the samples and frames
* received from the proviler in a tree-like structure.
*/
const { L10N } = require("devtools/client/performance/modules/global");
const { extend } = require("devtools/shared/extend");
const {
AbstractTreeItem,
} = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
const VIEW_OPTIMIZATIONS_TOOLTIP = L10N.getStr(
"table.view-optimizations.tooltiptext2"
);
const CALL_TREE_INDENTATION = 16; // px
// Used for rendering values in cells
const FORMATTERS = {
TIME: value =>
L10N.getFormatStr("table.ms2", L10N.numberWithDecimals(value, 2)),
PERCENT: value =>
L10N.getFormatStr("table.percentage3", L10N.numberWithDecimals(value, 2)),
NUMBER: value => value || 0,
BYTESIZE: value => L10N.getFormatStr("table.bytes", value || 0),
};
/**
* Definitions for rendering cells. Triads of class name, property name from
* `frame.getInfo()`, and a formatter function.
*/
const CELLS = {
duration: ["duration", "totalDuration", FORMATTERS.TIME],
percentage: ["percentage", "totalPercentage", FORMATTERS.PERCENT],
selfDuration: ["self-duration", "selfDuration", FORMATTERS.TIME],
selfPercentage: ["self-percentage", "selfPercentage", FORMATTERS.PERCENT],
samples: ["samples", "samples", FORMATTERS.NUMBER],
selfSize: ["self-size", "selfSize", FORMATTERS.BYTESIZE],
selfSizePercentage: [
"self-size-percentage",
"selfSizePercentage",
FORMATTERS.PERCENT,
],
selfCount: ["self-count", "selfCount", FORMATTERS.NUMBER],
selfCountPercentage: [
"self-count-percentage",
"selfCountPercentage",
FORMATTERS.PERCENT,
],
size: ["size", "totalSize", FORMATTERS.BYTESIZE],
sizePercentage: [
"size-percentage",
"totalSizePercentage",
FORMATTERS.PERCENT,
],
count: ["count", "totalCount", FORMATTERS.NUMBER],
countPercentage: [
"count-percentage",
"totalCountPercentage",
FORMATTERS.PERCENT,
],
};
const CELL_TYPES = Object.keys(CELLS);
const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
const dataA = frameA.getDisplayedData();
const dataB = frameB.getDisplayedData();
const isAllocations = "totalSize" in dataA;
if (isAllocations) {
if (this.inverted && dataA.selfSize !== dataB.selfSize) {
return dataA.selfSize < dataB.selfSize ? 1 : -1;
}
return dataA.totalSize < dataB.totalSize ? 1 : -1;
}
if (this.inverted && dataA.selfPercentage !== dataB.selfPercentage) {
return dataA.selfPercentage < dataB.selfPercentage ? 1 : -1;
}
return dataA.totalPercentage < dataB.totalPercentage ? 1 : -1;
};
// depth
const DEFAULT_AUTO_EXPAND_DEPTH = 3;
const DEFAULT_VISIBLE_CELLS = {
duration: true,
percentage: true,
selfDuration: true,
selfPercentage: true,
samples: true,
function: true,
// allocation columns
count: false,
selfCount: false,
size: false,
selfSize: false,
countPercentage: false,
selfCountPercentage: false,
sizePercentage: false,
selfSizePercentage: false,
};
/**
* An item in a call tree view, which looks like this:
*
* Time (ms) | Cost | Calls | Function
* ============================================================================
* 1,000.00 | 100.00% | | (root)
* 500.12 | 50.01% | 300 | foo Categ. 1
* 300.34 | 30.03% | 1500 | bar Categ. 2
* 10.56 | 0.01% | 42 | call_with_children Categ. 3
* 90.78 | 0.09% | 25 | call_without_children Categ. 4
*
* Every instance of a `CallView` represents a row in the call tree. The same
* parent node is used for all rows.
*
* @param CallView caller
* The CallView considered the "caller" frame. This newly created
* instance will be represent the "callee". Should be null for root nodes.
* @param ThreadNode | FrameNode frame
* Details about this function, like { samples, duration, calls } etc.
* @param number level [optional]
* The indentation level in the call tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
* calculations. Defaults to false.
* @param boolean inverted [optional]
* Whether the call tree has been inverted (bottom up, rather than
* top-down). Defaults to false.
* @param function sortingPredicate [optional]
* The predicate used to sort the tree items when created. Defaults to
* the caller's `sortingPredicate` if a caller exists, otherwise defaults
* to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
* @param number autoExpandDepth [optional]
* The depth to which the tree should automatically expand. Defualts to
* the caller's `autoExpandDepth` if a caller exists, otherwise defaults
* to DEFAULT_AUTO_EXPAND_DEPTH.
* @param object visibleCells
* An object specifying which cells are visible in the tree. Defaults to
* the caller's `visibleCells` if a caller exists, otherwise defaults
* to DEFAULT_VISIBLE_CELLS.
* @param boolean showOptimizationHint [optional]
* Whether or not to show an icon indicating if the frame has optimization
* data.
*/
function CallView({
caller,
frame,
level,
hidden,
inverted,
sortingPredicate,
autoExpandDepth,
visibleCells,
showOptimizationHint,
}) {
AbstractTreeItem.call(this, {
parent: caller,
level: level | (0 - (hidden ? 1 : 0)),
});
if (sortingPredicate != null) {
this.sortingPredicate = sortingPredicate;
} else if (caller) {
this.sortingPredicate = caller.sortingPredicate;
} else {
this.sortingPredicate = DEFAULT_SORTING_PREDICATE;
}
if (autoExpandDepth != null) {
this.autoExpandDepth = autoExpandDepth;
} else if (caller) {
this.autoExpandDepth = caller.autoExpandDepth;
} else {
this.autoExpandDepth = DEFAULT_AUTO_EXPAND_DEPTH;
}
if (visibleCells != null) {
this.visibleCells = visibleCells;
} else if (caller) {
this.visibleCells = caller.visibleCells;
} else {
this.visibleCells = Object.create(DEFAULT_VISIBLE_CELLS);
}
this.caller = caller;
this.frame = frame;
this.hidden = hidden;
this.inverted = inverted;
this.showOptimizationHint = showOptimizationHint;
this._onUrlClick = this._onUrlClick.bind(this);
}
CallView.prototype = extend(AbstractTreeItem.prototype, {
/**
* Creates the view for this tree node.
* @param Node document
* @param Node arrowNode
* @return Node
*/
_displaySelf: function(document, arrowNode) {
const frameInfo = this.getDisplayedData();
const cells = [];
for (const type of CELL_TYPES) {
if (this.visibleCells[type]) {
// Inline for speed, but pass in the formatted value via
// cell definition, as well as the element type.
cells.push(
this._createCell(
document,
CELLS[type][2](frameInfo[CELLS[type][1]]),
CELLS[type][0]
)
);
}
}
if (this.visibleCells.function) {
cells.push(
this._createFunctionCell(
document,
arrowNode,
frameInfo.name,
frameInfo,
this.level
)
);
}
const targetNode = document.createXULElement("hbox");
targetNode.className = "call-tree-item";
targetNode.setAttribute(
"origin",
frameInfo.isContent ? "content" : "chrome"
);
targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
targetNode.setAttribute("tooltiptext", frameInfo.tooltiptext);
if (this.hidden) {
targetNode.style.display = "none";
}
for (let i = 0; i < cells.length; i++) {
targetNode.appendChild(cells[i]);
}
return targetNode;
},
/**
* Populates this node in the call tree with the corresponding "callees".
* These are defined in the `frame` data source for this call view.
* @param array:AbstractTreeItem children
*/
_populateSelf: function(children) {
const newLevel = this.level + 1;
for (const newFrame of this.frame.calls) {
children.push(
new CallView({
caller: this,
frame: newFrame,
level: newLevel,
inverted: this.inverted,
})
);
}
// Sort the "callees" asc. by samples, before inserting them in the tree,
// if no other sorting predicate was specified on this on the root item.
children.sort(this.sortingPredicate.bind(this));
},
/**
* Functions creating each cell in this call view.
* Invoked by `_displaySelf`.
*/
_createCell: function(doc, value, type) {
const cell = doc.createXULElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", type);
cell.setAttribute("crop", "end");
// Add a tabulation to the cell text in case it's is selected and copied.
cell.textContent = value + "\t";
return cell;
},
_createFunctionCell: function(
doc,
arrowNode,
frameName,
frameInfo,
frameLevel
) {
const cell = doc.createXULElement("hbox");
cell.className = "call-tree-cell";
cell.style.marginInlineStart = frameLevel * CALL_TREE_INDENTATION + "px";
cell.setAttribute("type", "function");
cell.appendChild(arrowNode);
// Render optimization hint if this frame has opt data.
if (
this.root.showOptimizationHint &&
frameInfo.hasOptimizations &&
!frameInfo.isMetaCategory
) {
const icon = doc.createXULElement("description");
icon.setAttribute("tooltiptext", VIEW_OPTIMIZATIONS_TOOLTIP);
icon.className = "opt-icon";
cell.appendChild(icon);
}
// Don't render a name label node if there's no function name. A different
// location label node will be rendered instead.
if (frameName) {
const nameNode = doc.createXULElement("description");
nameNode.className = "plain call-tree-name";
nameNode.textContent = frameName;
cell.appendChild(nameNode);
}
// Don't render detailed labels for meta category frames
if (!frameInfo.isMetaCategory) {
this._appendFunctionDetailsCells(doc, cell, frameInfo);
}
// Don't render an expando-arrow for leaf nodes.
const hasDescendants = Object.keys(this.frame.calls).length > 0;
if (!hasDescendants) {
arrowNode.setAttribute("invisible", "");
}
// Add a line break to the last description of the row in case it's selected
// and copied.
const lastDescription = cell.querySelector("description:last-of-type");
lastDescription.textContent = lastDescription.textContent + "\n";
// Add spaces as frameLevel indicators in case the row is selected and
// copied. These spaces won't be displayed in the cell content.
const firstDescription = cell.querySelector("description:first-of-type");
const levelIndicator = frameLevel > 0 ? " ".repeat(frameLevel) : "";
firstDescription.textContent =
levelIndicator + firstDescription.textContent;
return cell;
},
_appendFunctionDetailsCells: function(doc, cell, frameInfo) {
if (frameInfo.fileName) {
const urlNode = doc.createXULElement("description");
urlNode.className = "plain call-tree-url";
urlNode.textContent = frameInfo.fileName;
urlNode.setAttribute(
"tooltiptext",
URL_LABEL_TOOLTIP + " → " + frameInfo.url
);
urlNode.addEventListener("mousedown", this._onUrlClick);
cell.appendChild(urlNode);
}
if (frameInfo.line) {
const lineNode = doc.createXULElement("description");
lineNode.className = "plain call-tree-line";
lineNode.textContent = ":" + frameInfo.line;
cell.appendChild(lineNode);
}
if (frameInfo.column) {
const columnNode = doc.createXULElement("description");
columnNode.className = "plain call-tree-column";
columnNode.textContent = ":" + frameInfo.column;
cell.appendChild(columnNode);
}
if (frameInfo.host) {
const hostNode = doc.createXULElement("description");
hostNode.className = "plain call-tree-host";
hostNode.textContent = frameInfo.host;
cell.appendChild(hostNode);
}
if (frameInfo.categoryData.label) {
const categoryNode = doc.createXULElement("description");
categoryNode.className = "plain call-tree-category";
categoryNode.style.color = frameInfo.categoryData.color;
categoryNode.textContent = frameInfo.categoryData.label;
cell.appendChild(categoryNode);
}
},
/**
* Gets the data displayed about this tree item, based on the FrameNode
* model associated with this view.
*
* @return object
*/
getDisplayedData: function() {
if (this._cachedDisplayedData) {
return this._cachedDisplayedData;
}
this._cachedDisplayedData = this.frame.getInfo({
root: this.root.frame,
allocations: this.visibleCells.count || this.visibleCells.selfCount,
});
return this._cachedDisplayedData;
/**
* When inverting call tree, the costs and times are dependent on position
* in the tree. We must only count leaf nodes with self cost, and total costs
* dependent on how many times the leaf node was found with a full stack path.
*
* Total | Self | Calls | Function
* ============================================================================
* 100% | 100% | 100 | C
* 50% | 0% | 50 | B
* 50% | 0% | 50 | A
* 50% | 0% | 50 | B
*
* Every instance of a `CallView` represents a row in the call tree. The same
* container node is used for all rows.
*/
},
/**
* Toggles the category information hidden or visible.
* @param boolean visible
*/
toggleCategories: function(visible) {
if (!visible) {
this.container.setAttribute("categories-hidden", "");
} else {
this.container.removeAttribute("categories-hidden");
}
},
/**
* Handler for the "click" event on the url node of this call view.
*/
_onUrlClick: function(e) {
e.preventDefault();
e.stopPropagation();
// Only emit for left click events
if (e.button === 0) {
this.root.emit("link", this);
}
},
});
exports.CallView = CallView;

View File

@ -1,25 +0,0 @@
# vim: set filetype=python:
# 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/.
DIRS += [
"components",
"modules",
"test",
"views",
]
DevToolsModules(
"events.js",
"initializer.js",
"panel.js",
"performance-controller.js",
"performance-view.js",
)
BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
with Files("**"):
BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)")

View File

@ -1,160 +0,0 @@
/* 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";
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
function PerformancePanel(iframeWindow, toolbox, commands) {
this.panelWin = iframeWindow;
this.toolbox = toolbox;
this.commands = commands;
this._targetAvailablePromise = Promise.resolve();
this._onTargetAvailable = this._onTargetAvailable.bind(this);
EventEmitter.decorate(this);
}
exports.PerformancePanel = PerformancePanel;
PerformancePanel.prototype = {
/**
* Open is effectively an asynchronous constructor.
*
* @return object
* A promise that is resolved when the Performance tool
* completes opening.
*/
async open() {
if (this._opening) {
return this._opening;
}
this._checkRecordingStatus = this._checkRecordingStatus.bind(this);
const { PerformanceController, EVENTS } = this.panelWin;
PerformanceController.on(
EVENTS.RECORDING_ADDED,
this._checkRecordingStatus
);
PerformanceController.on(
EVENTS.RECORDING_STATE_CHANGE,
this._checkRecordingStatus
);
// In case that the target is switched across process, the corresponding front also
// will be changed. In order to detect that, watch the change.
// Also, we wait for `watchTargets` to end. Indeed the function `_onTargetAvailable
// will be called synchronously with current target as a parameter by
// the `watchTargets` function.
// So this `await` waits for initialization with current target, happening
// in `_onTargetAvailable`.
await this.commands.targetCommand.watchTargets({
types: [this.commands.targetCommand.TYPES.FRAME],
onAvailable: this._onTargetAvailable,
});
// Fire this once incase we have an in-progress recording (console profile)
// that caused this start up, and no state change yet, so we can highlight the
// tab if we need.
this._checkRecordingStatus();
this._opening = new Promise(resolve => {
resolve(this);
});
return this._opening;
},
// DevToolPanel API
get target() {
return this.toolbox.target;
},
async destroy() {
// Make sure this panel is not already destroyed.
if (this._destroyed) {
return;
}
const { PerformanceController, PerformanceView, EVENTS } = this.panelWin;
PerformanceController.off(
EVENTS.RECORDING_ADDED,
this._checkRecordingStatus
);
PerformanceController.off(
EVENTS.RECORDING_STATE_CHANGE,
this._checkRecordingStatus
);
this.commands.targetCommand.unwatchTargets({
types: [this.commands.targetCommand.TYPES.FRAME],
onAvailable: this._onTargetAvailable,
});
await PerformanceController.destroy();
await PerformanceView.destroy();
PerformanceController.disableFrontEventListeners();
this.emit("destroyed");
this._destroyed = true;
},
_checkRecordingStatus: function() {
if (this.panelWin.PerformanceController.isRecording()) {
this.toolbox.highlightTool("performance");
} else {
this.toolbox.unhighlightTool("performance");
}
},
/**
* This function executes actual logic for the target-switching.
*
* @param {TargetFront} - targetFront
* As we are watching only FRAME type for this panel,
* the target should be a instance of WindowGlobalTarget.
*/
async _handleTargetAvailable({ targetFront }) {
if (targetFront.isTopLevel) {
const { PerformanceController, PerformanceView } = this.panelWin;
const performanceFront = await targetFront.getFront("performance");
if (!this._isPanelInitialized) {
await PerformanceController.initialize(
this.toolbox,
targetFront,
performanceFront
);
await PerformanceView.initialize();
PerformanceController.enableFrontEventListeners();
this._isPanelInitialized = true;
} else {
const isRecording = PerformanceController.isRecording();
if (isRecording) {
await PerformanceController.stopRecording();
}
PerformanceView.resetBufferStatus();
PerformanceController.updateFronts(targetFront, performanceFront);
if (isRecording) {
await PerformanceController.startRecording();
}
}
}
},
/**
* This function is called for every target is available.
*/
_onTargetAvailable(parameters) {
// As this function is called asynchronous, while previous processing, this might be
// called. Thus, we wait until finishing previous one before starting next.
this._targetAvailablePromise = this._targetAvailablePromise.then(() =>
this._handleTargetAvailable(parameters)
);
return this._targetAvailablePromise;
},
};

View File

@ -1,542 +0,0 @@
/* 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/. */
/* globals $ */
"use strict";
// Events emitted by various objects in the panel.
const EVENTS = require("devtools/client/performance/events");
const Services = require("Services");
const EventEmitter = require("devtools/shared/event-emitter");
const flags = require("devtools/shared/flags");
const {
getTheme,
addThemeObserver,
removeThemeObserver,
} = require("devtools/client/shared/theme");
// Logic modules
const {
PerformanceTelemetry,
} = require("devtools/client/performance/modules/logic/telemetry");
const {
PerformanceView,
} = require("devtools/client/performance/performance-view");
const { DetailsView } = require("devtools/client/performance/views/details");
const {
RecordingsView,
} = require("devtools/client/performance/views/recordings");
const { ToolbarView } = require("devtools/client/performance/views/toolbar");
/**
* Functions handling target-related lifetime events and
* UI interaction.
*/
const PerformanceController = {
_recordings: [],
_currentRecording: null,
/**
* Listen for events emitted by the current tab target and
* main UI events.
*/
async initialize(toolbox, targetFront, performanceFront) {
this.toolbox = toolbox;
this.target = targetFront;
this.front = performanceFront;
this._telemetry = new PerformanceTelemetry(this);
this.startRecording = this.startRecording.bind(this);
this.stopRecording = this.stopRecording.bind(this);
this.importRecording = this.importRecording.bind(this);
this.exportRecording = this.exportRecording.bind(this);
this.clearRecordings = this.clearRecordings.bind(this);
this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(
this
);
this._onPrefChanged = this._onPrefChanged.bind(this);
this._onThemeChanged = this._onThemeChanged.bind(this);
this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
this._onProfilerStatus = this._onProfilerStatus.bind(this);
this._onRecordingStarted = this._emitRecordingStateChange.bind(
this,
"recording-started"
);
this._onRecordingStopping = this._emitRecordingStateChange.bind(
this,
"recording-stopping"
);
this._onRecordingStopped = this._emitRecordingStateChange.bind(
this,
"recording-stopped"
);
// Store data regarding if e10s is enabled.
this._e10s = Services.appinfo.browserTabsRemoteAutostart;
this._setMultiprocessAttributes();
this._prefs = require("devtools/client/performance/modules/global").PREFS;
this._prefs.registerObserver();
this._prefs.on("pref-changed", this._onPrefChanged);
ToolbarView.on(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
RecordingsView.on(
EVENTS.UI_RECORDING_SELECTED,
this._onRecordingSelectFromView
);
DetailsView.on(
EVENTS.UI_DETAILS_VIEW_SELECTED,
this._onDetailsViewSelected
);
addThemeObserver(this._onThemeChanged);
},
/**
* Remove events handled by the PerformanceController
*/
destroy: function() {
this._prefs.off("pref-changed", this._onPrefChanged);
this._prefs.unregisterObserver();
ToolbarView.off(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
RecordingsView.off(
EVENTS.UI_RECORDING_SELECTED,
this._onRecordingSelectFromView
);
DetailsView.off(
EVENTS.UI_DETAILS_VIEW_SELECTED,
this._onDetailsViewSelected
);
removeThemeObserver(this._onThemeChanged);
this._telemetry.destroy();
},
updateFronts(targetFront, performanceFront) {
this.target = targetFront;
this.front = performanceFront;
this.enableFrontEventListeners();
},
/**
* Enables front event listeners.
*
* The rationale behind this is given by the async intialization of all the
* frontend components. Even though the panel is considered "open" only after
* both the controller and the view are created, and even though their
* initialization is sequential (controller, then view), the controller might
* start handling backend events before the view finishes if the event
* listeners are added too soon.
*/
enableFrontEventListeners: function() {
this.front.on("profiler-status", this._onProfilerStatus);
this.front.on("recording-started", this._onRecordingStarted);
this.front.on("recording-stopping", this._onRecordingStopping);
this.front.on("recording-stopped", this._onRecordingStopped);
},
/**
* Disables front event listeners.
*/
disableFrontEventListeners: function() {
this.front.off("profiler-status", this._onProfilerStatus);
this.front.off("recording-started", this._onRecordingStarted);
this.front.off("recording-stopping", this._onRecordingStopping);
this.front.off("recording-stopped", this._onRecordingStopped);
},
/**
* Returns the current devtools theme.
*/
getTheme: function() {
return getTheme();
},
/**
* Get a boolean preference setting from `prefName` via the underlying
* OptionsView in the ToolbarView. This preference is guaranteed to be
* displayed in the UI.
*
* @param string prefName
* @return boolean
*/
getOption: function(prefName) {
return ToolbarView.optionsView.getPref(prefName);
},
/**
* Get a preference setting from `prefName`. This preference can be of
* any type and might not be displayed in the UI.
*
* @param string prefName
* @return any
*/
getPref: function(prefName) {
return this._prefs[prefName];
},
/**
* Set a preference setting from `prefName`. This preference can be of
* any type and might not be displayed in the UI.
*
* @param string prefName
* @param any prefValue
*/
setPref: function(prefName, prefValue) {
this._prefs[prefName] = prefValue;
},
/**
* Checks whether or not a new recording is supported by the PerformanceFront.
* @return Promise:boolean
*/
async canCurrentlyRecord() {
const hasActor = await this.target.hasActor("performance");
if (!hasActor) {
return true;
}
return (await this.front.canCurrentlyRecord()).success;
},
/**
* Starts recording with the PerformanceFront.
*/
async startRecording() {
const options = {
withMarkers: true,
withTicks: this.getOption("enable-framerate"),
withMemory: this.getOption("enable-memory"),
withFrames: true,
withGCEvents: true,
withAllocations: this.getOption("enable-allocations"),
allocationsSampleProbability: this.getPref("memory-sample-probability"),
allocationsMaxLogLength: this.getPref("memory-max-log-length"),
bufferSize: this.getPref("profiler-buffer-size"),
sampleFrequency: this.getPref("profiler-sample-frequency"),
};
const recordingStarted = await this.front.startRecording(options);
// In some cases, like when the target has a private browsing tab,
// recording is not currently supported because of the profiler module.
// Present a notification in this case alerting the user of this issue.
if (!recordingStarted) {
this.emit(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START);
PerformanceView.setState("unavailable");
} else {
this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_START);
}
},
/**
* Stops recording with the PerformanceFront.
*/
async stopRecording() {
const recording = this.getLatestManualRecording();
if (!this.front.isDestroyed()) {
await this.front.stopRecording(recording);
} else {
// As the front was destroyed, we do stop sequence manually without the actor.
recording._recording = false;
recording._completed = true;
await this._onRecordingStopped(recording);
}
this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_STOP);
},
/**
* Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED`
* when the file was saved.
*
* @param PerformanceRecording recording
* The model that holds the recording data.
* @param nsIFile file
* The file to stream the data into.
*/
async exportRecording(recording, file) {
await recording.exportRecording(file);
this.emit(EVENTS.RECORDING_EXPORTED, recording, file);
},
/**
* Clears all completed recordings from the list as well as the current non-console
* recording. Emits `EVENTS.RECORDING_DELETED` when complete so other components can
* clean up.
*/
async clearRecordings() {
for (let i = this._recordings.length - 1; i >= 0; i--) {
const model = this._recordings[i];
if (!model.isConsole() && model.isRecording()) {
await this.stopRecording();
}
// If last recording is not recording, but finalizing itself,
// wait for that to finish
if (!model.isRecording() && !model.isCompleted()) {
await this.waitForStateChangeOnRecording(model, "recording-stopped");
}
// If recording is completed,
// clean it up from UI and remove it from the _recordings array.
if (model.isCompleted()) {
this.emit(EVENTS.RECORDING_DELETED, model);
this._recordings.splice(i, 1);
}
}
if (this._recordings.length > 0) {
if (!this._recordings.includes(this.getCurrentRecording())) {
this.setCurrentRecording(this._recordings[0]);
}
} else {
this.setCurrentRecording(null);
}
},
/**
* Loads a recording from a file, adding it to the recordings list. Emits
* `EVENTS.RECORDING_IMPORTED` when the file was loaded.
*
* @param nsIFile file
* The file to import the data from.
*/
async importRecording(file) {
const recording = await this.front.importRecording(file);
this._addRecordingIfUnknown(recording);
this.emit(EVENTS.RECORDING_IMPORTED, recording);
},
/**
* Sets the currently active PerformanceRecording. Should rarely be called directly,
* as RecordingsView handles this when manually selected a recording item. Exceptions
* are when clearing the view.
* @param PerformanceRecording recording
*/
setCurrentRecording: function(recording) {
if (this._currentRecording !== recording) {
this._currentRecording = recording;
this.emit(EVENTS.RECORDING_SELECTED, recording);
}
},
/**
* Gets the currently active PerformanceRecording.
* @return PerformanceRecording
*/
getCurrentRecording: function() {
return this._currentRecording;
},
/**
* Get most recently added recording that was triggered manually (via UI).
* @return PerformanceRecording
*/
getLatestManualRecording: function() {
for (let i = this._recordings.length - 1; i >= 0; i--) {
const model = this._recordings[i];
if (!model.isConsole() && !model.isImported()) {
return this._recordings[i];
}
}
return null;
},
/**
* Fired from RecordingsView, we listen on the PerformanceController so we can
* set it here and re-emit on the controller, where all views can listen.
*/
_onRecordingSelectFromView: function(recording) {
this.setCurrentRecording(recording);
},
/**
* Fired when the ToolbarView fires a PREF_CHANGED event.
* with the value.
*/
_onPrefChanged: function(prefName, prefValue) {
this.emit(EVENTS.PREF_CHANGED, prefName, prefValue);
},
/*
* Called when the developer tools theme changes.
*/
_onThemeChanged: function(newValue) {
this.emit(EVENTS.THEME_CHANGED, newValue);
},
_onProfilerStatus: function(status) {
this.emit(EVENTS.RECORDING_PROFILER_STATUS_UPDATE, status);
},
_emitRecordingStateChange(eventName, recordingModel) {
this._addRecordingIfUnknown(recordingModel);
this.emit(EVENTS.RECORDING_STATE_CHANGE, eventName, recordingModel);
},
/**
* Stores a recording internally.
*
* @param {PerformanceRecordingFront} recording
*/
_addRecordingIfUnknown: function(recording) {
if (!this._recordings.includes(recording)) {
this._recordings.push(recording);
this.emit(EVENTS.RECORDING_ADDED, recording);
}
},
/**
* Takes a recording and returns a value between 0 and 1 indicating how much
* of the buffer is used.
*/
getBufferUsageForRecording: function(recording) {
return this.front.getBufferUsageForRecording(recording);
},
/**
* Returns a boolean indicating if any recordings are currently in progress or not.
*/
isRecording: function() {
return this._recordings.some(r => r.isRecording());
},
/**
* Returns the internal store of recording models.
*/
getRecordings: function() {
return this._recordings;
},
/**
* Returns traits from the front.
*/
getTraits: function() {
return this.front.traits;
},
viewSourceInDebugger(url, line, column) {
// Currently, the line and column values are strings, so we have to convert
// them to numbers before passing them on to the toolbox.
return this.toolbox.viewSourceInDebugger(url, +line, +column);
},
/**
* Utility method taking a string or an array of strings of feature names (like
* "withAllocations" or "withMarkers"), and returns whether or not the current
* recording supports that feature, based off of UI preferences and server support.
*
* @option {Array<string>|string} features
* A string or array of strings indicating what configuration is needed on the
* recording model, like `withTicks`, or `withMemory`.
*
* @return boolean
*/
isFeatureSupported: function(features) {
if (!features) {
return true;
}
const recording = this.getCurrentRecording();
if (!recording) {
return false;
}
const config = recording.getConfiguration();
return [].concat(features).every(f => config[f]);
},
/**
* Takes an array of PerformanceRecordingFronts and adds them to the internal
* store of the UI. Used by the toolbox to lazily seed recordings that
* were observed before the panel was loaded in the scenario where `console.profile()`
* is used before the tool is loaded.
*
* @param {Array<PerformanceRecordingFront>} recordings
*/
populateWithRecordings: function(recordings = []) {
for (const recording of recordings) {
PerformanceController._addRecordingIfUnknown(recording);
}
this.emit(EVENTS.RECORDINGS_SEEDED);
},
/**
* Returns an object with `supported` and `enabled` properties indicating
* whether or not the platform is capable of turning on e10s and whether or not
* it's already enabled, respectively.
*
* @return {object}
*/
getMultiprocessStatus: function() {
// If testing, set enabled to true so we have realtime rendering tests
// in non-e10s. This function is overridden wholesale in tests
// when we want to test multiprocess support
// specifically.
if (flags.testing) {
return { enabled: true };
}
// This is only checked on tool startup -- requires a restart if
// e10s subsequently enabled.
const enabled = this._e10s;
return { enabled };
},
/**
* Takes a PerformanceRecording and a state, and waits for
* the event to be emitted from the front for that recording.
*
* @param {PerformanceRecordingFront} recording
* @param {string} expectedState
* @return {Promise}
*/
async waitForStateChangeOnRecording(recording, expectedState) {
await new Promise(resolve => {
this.on(EVENTS.RECORDING_STATE_CHANGE, function handler(state, model) {
if (state === expectedState && model === recording) {
this.off(EVENTS.RECORDING_STATE_CHANGE, handler);
resolve();
}
});
});
},
/**
* Called on init, sets an `e10s` attribute on the main view container with
* "disabled" if e10s is possible on the platform and just not on, or "unsupported"
* if e10s is not possible on the platform. If e10s is on, no attribute is set.
*/
_setMultiprocessAttributes: function() {
const { enabled } = this.getMultiprocessStatus();
if (!enabled) {
$("#performance-view").setAttribute("e10s", "disabled");
}
},
/**
* Pipes EVENTS.UI_DETAILS_VIEW_SELECTED to the PerformanceController.
*/
_onDetailsViewSelected: function(...data) {
this.emit(EVENTS.UI_DETAILS_VIEW_SELECTED, ...data);
},
toString: () => "[object PerformanceController]",
};
/**
* Convenient way of emitting events from the controller.
*/
EventEmitter.decorate(PerformanceController);
exports.PerformanceController = PerformanceController;

View File

@ -1,490 +0,0 @@
/* 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/. */
/* globals $, $$, PerformanceController */
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const RecordingControls = React.createFactory(
require("devtools/client/performance/components/RecordingControls")
);
const RecordingButton = React.createFactory(
require("devtools/client/performance/components/RecordingButton")
);
const EVENTS = require("devtools/client/performance/events");
const PerformanceUtils = require("devtools/client/performance/modules/utils");
const { DetailsView } = require("devtools/client/performance/views/details");
const { OverviewView } = require("devtools/client/performance/views/overview");
const {
RecordingsView,
} = require("devtools/client/performance/views/recordings");
const { ToolbarView } = require("devtools/client/performance/views/toolbar");
const { L10N } = require("devtools/client/performance/modules/global");
/**
* Master view handler for the performance tool.
*/
var PerformanceView = {
_state: null,
// Set to true if the front emits a "buffer-status" event, indicating
// that the server has support for determining buffer status.
_bufferStatusSupported: false,
// Mapping of state to selectors for different properties and their values,
// from the main profiler view. Used in `PerformanceView.setState()`
states: {
unavailable: [
{
sel: "#performance-view",
opt: "selectedPanel",
val: () => $("#unavailable-notice"),
},
{
sel: "#performance-view-content",
opt: "hidden",
val: () => true,
},
],
empty: [
{
sel: "#performance-view",
opt: "selectedPanel",
val: () => $("#empty-notice"),
},
{
sel: "#performance-view-content",
opt: "hidden",
val: () => true,
},
],
recording: [
{
sel: "#performance-view",
opt: "selectedPanel",
val: () => $("#performance-view-content"),
},
{
sel: "#performance-view-content",
opt: "hidden",
val: () => false,
},
{
sel: "#details-pane-container",
opt: "selectedPanel",
val: () => $("#recording-notice"),
},
],
"console-recording": [
{
sel: "#performance-view",
opt: "selectedPanel",
val: () => $("#performance-view-content"),
},
{
sel: "#performance-view-content",
opt: "hidden",
val: () => false,
},
{
sel: "#details-pane-container",
opt: "selectedPanel",
val: () => $("#console-recording-notice"),
},
],
recorded: [
{
sel: "#performance-view",
opt: "selectedPanel",
val: () => $("#performance-view-content"),
},
{
sel: "#performance-view-content",
opt: "hidden",
val: () => false,
},
{
sel: "#details-pane-container",
opt: "selectedPanel",
val: () => $("#details-pane"),
},
],
loading: [
{
sel: "#performance-view",
opt: "selectedPanel",
val: () => $("#performance-view-content"),
},
{
sel: "#performance-view-content",
opt: "hidden",
val: () => false,
},
{
sel: "#details-pane-container",
opt: "selectedPanel",
val: () => $("#loading-notice"),
},
],
},
/**
* Sets up the view with event binding and main subviews.
*/
async initialize() {
this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
this._onImportButtonClick = this._onImportButtonClick.bind(this);
this._onClearButtonClick = this._onClearButtonClick.bind(this);
this._onRecordingSelected = this._onRecordingSelected.bind(this);
this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
this._onNewRecordingFailed = this._onNewRecordingFailed.bind(this);
// Bind to controller events to unlock the record button
PerformanceController.on(
EVENTS.RECORDING_SELECTED,
this._onRecordingSelected
);
PerformanceController.on(
EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
this._onProfilerStatusUpdated
);
PerformanceController.on(
EVENTS.RECORDING_STATE_CHANGE,
this._onRecordingStateChange
);
PerformanceController.on(
EVENTS.RECORDING_ADDED,
this._onRecordingStateChange
);
PerformanceController.on(
EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
this._onNewRecordingFailed
);
if (await PerformanceController.canCurrentlyRecord()) {
this.setState("empty");
} else {
this.setState("unavailable");
}
// Initialize the ToolbarView first, because other views may need access
// to the OptionsView via the controller, to read prefs.
await ToolbarView.initialize();
await RecordingsView.initialize();
await OverviewView.initialize();
await DetailsView.initialize();
// DE-XUL: Begin migrating the toolbar to React. Temporarily hold state here.
this._recordingControlsState = {
onRecordButtonClick: this._onRecordButtonClick,
onImportButtonClick: this._onImportButtonClick,
onClearButtonClick: this._onClearButtonClick,
isRecording: false,
isDisabled: false,
};
// Mount to an HTML element.
const { createHtmlMount } = PerformanceUtils;
this._recordingControlsMount = createHtmlMount(
$("#recording-controls-mount")
);
this._recordingButtonsMounts = Array.from(
$$(".recording-button-mount")
).map(createHtmlMount);
this._renderRecordingControls();
},
/**
* DE-XUL: Render the recording controls and buttons using React.
*/
_renderRecordingControls: function() {
ReactDOM.render(
RecordingControls(this._recordingControlsState),
this._recordingControlsMount
);
for (const button of this._recordingButtonsMounts) {
ReactDOM.render(RecordingButton(this._recordingControlsState), button);
}
},
/**
* Unbinds events and destroys subviews.
*/
async destroy() {
PerformanceController.off(
EVENTS.RECORDING_SELECTED,
this._onRecordingSelected
);
PerformanceController.off(
EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
this._onProfilerStatusUpdated
);
PerformanceController.off(
EVENTS.RECORDING_STATE_CHANGE,
this._onRecordingStateChange
);
PerformanceController.off(
EVENTS.RECORDING_ADDED,
this._onRecordingStateChange
);
PerformanceController.off(
EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
this._onNewRecordingFailed
);
await ToolbarView.destroy();
await RecordingsView.destroy();
await OverviewView.destroy();
await DetailsView.destroy();
},
/**
* Sets the state of the profiler view. Possible options are "unavailable",
* "empty", "recording", "console-recording", "recorded".
*/
setState: function(state) {
// Make sure that the focus isn't captured on a hidden iframe. This fixes a
// XUL bug where shortcuts stop working.
const iframes = window.document.querySelectorAll("iframe");
for (const iframe of iframes) {
iframe.blur();
}
window.focus();
const viewConfig = this.states[state];
if (!viewConfig) {
throw new Error(`Invalid state for PerformanceView: ${state}`);
}
for (const { sel, opt, val } of viewConfig) {
for (const el of $$(sel)) {
el[opt] = val();
}
}
this._state = state;
if (state === "console-recording") {
const recording = PerformanceController.getCurrentRecording();
let label = recording.getLabel() || "";
// Wrap the label in quotes if it exists for the commands.
label = label ? `"${label}"` : "";
const startCommand = $(
".console-profile-recording-notice .console-profile-command"
);
const stopCommand = $(
".console-profile-stop-notice .console-profile-command"
);
startCommand.value = `console.profile(${label})`;
stopCommand.value = `console.profileEnd(${label})`;
}
this.updateBufferStatus();
this.emit(EVENTS.UI_STATE_CHANGED, state);
},
/**
* Returns the state of the PerformanceView.
*/
getState: function() {
return this._state;
},
/**
* Reset the displayed buffer status.
* Called for every target-switching.
*/
resetBufferStatus() {
this._bufferStatusSupported = false;
$("#details-pane-container").removeAttribute("buffer-status");
},
/**
* Updates the displayed buffer status.
*/
updateBufferStatus: function() {
// If we've never seen a "buffer-status" event from the front, ignore
// and keep the buffer elements hidden.
if (!this._bufferStatusSupported) {
return;
}
const recording = PerformanceController.getCurrentRecording();
if (!recording || !recording.isRecording()) {
return;
}
const bufferUsage =
PerformanceController.getBufferUsageForRecording(recording) || 0;
// Normalize to a percentage value
const percent = Math.floor(bufferUsage * 100);
const $container = $("#details-pane-container");
const $bufferLabel = $(".buffer-status-message", $container.selectedPanel);
// Be a little flexible on the buffer status, although not sure how
// this could happen, as RecordingModel clamps.
if (percent >= 99) {
$container.setAttribute("buffer-status", "full");
} else {
$container.setAttribute("buffer-status", "in-progress");
}
$bufferLabel.value = L10N.getFormatStr("profiler.bufferFull", percent);
this.emit(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED, percent);
},
/**
* Toggles the `locked` attribute on the record buttons based
* on `lock`.
*
* @param {boolean} lock
*/
_lockRecordButtons: function(lock) {
this._recordingControlsState.isLocked = lock;
this._renderRecordingControls();
},
/*
* Toggles the `checked` attribute on the record buttons based
* on `activate`.
*
* @param {boolean} activate
*/
_toggleRecordButtons: function(activate) {
this._recordingControlsState.isRecording = activate;
this._renderRecordingControls();
},
/**
* When a recording has started.
*/
_onRecordingStateChange: function() {
const currentRecording = PerformanceController.getCurrentRecording();
const recordings = PerformanceController.getRecordings();
this._toggleRecordButtons(
!!recordings.find(r => !r.isConsole() && r.isRecording())
);
this._lockRecordButtons(
!!recordings.find(r => !r.isConsole() && r.isFinalizing())
);
if (currentRecording && currentRecording.isFinalizing()) {
this.setState("loading");
}
if (currentRecording && currentRecording.isCompleted()) {
this.setState("recorded");
}
if (currentRecording && currentRecording.isRecording()) {
this.updateBufferStatus();
}
},
/**
* When starting a recording has failed.
*/
_onNewRecordingFailed: function() {
this._lockRecordButtons(false);
this._toggleRecordButtons(false);
},
/**
* Handler for clicking the clear button.
*/
_onClearButtonClick: function(e) {
this.emit(EVENTS.UI_CLEAR_RECORDINGS);
},
/**
* Handler for clicking the record button.
*/
_onRecordButtonClick: function(e) {
if (this._recordingControlsState.isRecording) {
this.emit(EVENTS.UI_STOP_RECORDING);
} else {
this._lockRecordButtons(true);
this._toggleRecordButtons(true);
this.emit(EVENTS.UI_START_RECORDING);
}
},
/**
* Handler for clicking the import button.
*/
_onImportButtonClick: function(e) {
const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(
window,
L10N.getStr("recordingsList.importDialogTitle"),
Ci.nsIFilePicker.modeOpen
);
fp.appendFilter(
L10N.getStr("recordingsList.saveDialogJSONFilter"),
"*.json"
);
fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
fp.open(rv => {
if (rv == Ci.nsIFilePicker.returnOK) {
this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
}
});
},
/**
* Fired when a recording is selected. Used to toggle the profiler view state.
*/
_onRecordingSelected: function(recording) {
if (!recording) {
this.setState("empty");
} else if (recording.isRecording() && recording.isConsole()) {
this.setState("console-recording");
} else if (recording.isRecording()) {
this.setState("recording");
} else {
this.setState("recorded");
}
},
/**
* Fired when the controller has updated information on the buffer's status.
* Update the buffer status display if shown.
*/
_onProfilerStatusUpdated: function(profilerStatus) {
// We only care about buffer status here, so check to see
// if it has position.
if (!profilerStatus || profilerStatus.position === void 0) {
return;
}
// If this is our first buffer event, set the status and add a class
if (!this._bufferStatusSupported) {
this._bufferStatusSupported = true;
$("#details-pane-container").setAttribute("buffer-status", "in-progress");
}
if (!this.getState("recording") && !this.getState("console-recording")) {
return;
}
this.updateBufferStatus();
},
toString: () => "[object PerformanceView]",
};
/**
* Convenient way of emitting events from the view.
*/
EventEmitter.decorate(PerformanceView);
exports.PerformanceView = PerformanceView;

View File

@ -1,7 +0,0 @@
"use strict";
// General rule from /.eslintrc.js only accept folders matching **/test*/browser*/
// where is this folder doesn't match, so manually apply browser test config
module.exports = {
extends: ["plugin:mozilla/browser-test"],
};

View File

@ -1,146 +0,0 @@
[DEFAULT]
tags = devtools
subsuite = devtools
skip-if = os == 'linux' && e10s && (asan || debug) # Bug 1254821
support-files =
doc_allocs.html
doc_innerHTML.html
doc_markers.html
doc_simple-test.html
doc_worker.html
js_simpleWorker.js
head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
[browser_aaa-run-first-leaktest.js]
[browser_perf-button-states.js]
[browser_perf-calltree-js-categories.js]
skip-if = (os == 'win' && os_version == '10.0' && bits == 64 && !asan) # Bug 1466377
[browser_perf-calltree-js-columns.js]
[browser_perf-calltree-js-events.js]
fail-if = a11y_checks # bug 1687790 call-tree-item is not accessible
[browser_perf-calltree-memory-columns.js]
[browser_perf-console-record-01.js]
[browser_perf-console-record-02.js]
[browser_perf-console-record-03.js]
[browser_perf-console-record-04.js]
[browser_perf-console-record-05.js]
[browser_perf-console-record-06.js]
[browser_perf-console-record-07.js]
[browser_perf-console-record-08.js]
[browser_perf-console-record-09.js]
[browser_perf-details-01-toggle.js]
[browser_perf-details-02-utility-fun.js]
[browser_perf-details-03-without-allocations.js]
[browser_perf-details-04-toolbar-buttons.js]
[browser_perf-details-05-preserve-view.js]
[browser_perf-details-06-rerender-on-selection.js]
[browser_perf-details-07-bleed-events.js]
[browser_perf-details-render-00-waterfall.js]
[browser_perf-details-render-01-js-calltree.js]
[browser_perf-details-render-02-js-flamegraph.js]
[browser_perf-details-render-03-memory-calltree.js]
[browser_perf-details-render-04-memory-flamegraph.js]
[browser_perf-docload.js]
[browser_perf-fission-switch-target.js]
[browser_perf-highlighted.js]
[browser_perf-loading-01.js]
[browser_perf-loading-02.js]
[browser_perf-marker-details.js]
disabled=TODO bug 1256350
[browser_perf-options-01-toggle-throw.js]
[browser_perf-options-02-toggle-throw-alt.js]
[browser_perf-options-03-toggle-meta.js]
[browser_perf-options-enable-framerate-01.js]
[browser_perf-options-enable-framerate-02.js]
[browser_perf-options-enable-memory-01.js]
[browser_perf-options-enable-memory-02.js]
[browser_perf-options-flatten-tree-recursion-01.js]
[browser_perf-options-flatten-tree-recursion-02.js]
[browser_perf-options-invert-call-tree-01.js]
[browser_perf-options-invert-call-tree-02.js]
[browser_perf-options-invert-flame-graph-01.js]
[browser_perf-options-invert-flame-graph-02.js]
[browser_perf-options-propagate-allocations.js]
[browser_perf-options-propagate-profiler.js]
[browser_perf-options-show-idle-blocks-01.js]
[browser_perf-options-show-idle-blocks-02.js]
[browser_perf-options-show-jit-optimizations.js]
disabled=TODO bug 1256350
[browser_perf-options-show-platform-data-01.js]
[browser_perf-options-show-platform-data-02.js]
[browser_perf-overview-render-01.js]
[browser_perf-overview-render-02.js]
[browser_perf-overview-render-03.js]
[browser_perf-overview-render-04.js]
[browser_perf-overview-selection-01.js]
[browser_perf-overview-selection-02.js]
[browser_perf-overview-selection-03.js]
[browser_perf-overview-time-interval.js]
[browser_perf-range-changed-render.js]
[browser_perf-recording-notices-01.js]
[browser_perf-recording-notices-02.js]
[browser_perf-recording-notices-03.js]
skip-if = (debug && (bits == 32)) # debug 32 bit: bug 1273374
[browser_perf-recording-notices-04.js]
[browser_perf-recording-notices-05.js]
[browser_perf-recording-selected-01.js]
[browser_perf-recording-selected-02.js]
[browser_perf-recording-selected-03.js]
[browser_perf-recording-selected-04.js]
[browser_perf-recordings-clear-01.js]
[browser_perf-recordings-clear-02.js]
[browser_perf-recordings-io-01.js]
disabled=TODO bug 1256350
[browser_perf-recordings-io-02.js]
disabled=TODO bug 1256350
[browser_perf-recordings-io-03.js]
disabled=TODO bug 1256350
[browser_perf-recordings-io-04.js]
disabled=TODO bug 1256350
[browser_perf-recordings-io-05.js]
disabled=TODO bug 1256350
[browser_perf-recordings-io-06.js]
disabled=TODO bug 1256350
[browser_perf-refresh.js]
[browser_perf-states.js]
[browser_perf-telemetry-01.js]
[browser_perf-telemetry-02.js]
[browser_perf-telemetry-03.js]
[browser_perf-telemetry-04.js]
[browser_perf-theme-toggle.js]
disabled=TODO bug 1256350
[browser_perf-tree-abstract-01.js]
skip-if = (verify && debug && (os == 'win' || os == 'mac'))
[browser_perf-tree-abstract-02.js]
skip-if = (verify && debug && (os == 'win' || os == 'mac'))
[browser_perf-tree-abstract-03.js]
skip-if = (verify && debug && (os == 'win' || os == 'mac'))
[browser_perf-tree-abstract-04.js]
skip-if = (verify && debug && (os == 'win' || os == 'mac'))
[browser_perf-tree-abstract-05.js]
[browser_perf-tree-view-01.js]
[browser_perf-tree-view-02.js]
[browser_perf-tree-view-03.js]
[browser_perf-tree-view-04.js]
[browser_perf-tree-view-05.js]
[browser_perf-tree-view-06.js]
[browser_perf-tree-view-07.js]
[browser_perf-tree-view-08.js]
[browser_perf-tree-view-09.js]
[browser_perf-tree-view-10.js]
[browser_perf-tree-view-11.js]
disabled=TODO bug 1256350
[browser_perf-ui-recording.js]
[browser_timeline-filters-01.js]
disabled=TODO bug 1256350
[browser_timeline-filters-02.js]
disabled=TODO bug 1256350
[browser_timeline-waterfall-background.js]
[browser_timeline-waterfall-generic.js]
[browser_timeline-waterfall-rerender.js]
disabled=TODO bug 1256350
[browser_timeline-waterfall-sidebar.js]
disabled=TODO bug 1256350
[browser_timeline-waterfall-workers.js]
disabled=TODO bug 1256350

View File

@ -1,27 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if the performance tool leaks on initialization and sudden destruction.
* You can also use this initialization format as a template for other tests.
*/
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
add_task(async function() {
const { target, toolbox, panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
ok(target, "Should have a target available.");
ok(toolbox, "Should have a toolbox available.");
ok(panel, "Should have a panel available.");
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,105 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the recording button states are set as expected.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
$,
$$,
EVENTS,
PerformanceController,
PerformanceView,
} = panel.panelWin;
const recordButton = $("#main-record-button");
checkRecordButtonsStates(false, false);
const uiStartClick = once(PerformanceView, EVENTS.UI_START_RECORDING);
const recordingStarted = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-started"],
}
);
const backendStartReady = once(
PerformanceController,
EVENTS.BACKEND_READY_AFTER_RECORDING_START
);
const uiStateRecording = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
expectedArgs: ["recording"],
});
click(recordButton);
await uiStartClick;
checkRecordButtonsStates(true, true);
await recordingStarted;
checkRecordButtonsStates(true, false);
await backendStartReady;
await uiStateRecording;
const uiStopClick = once(PerformanceView, EVENTS.UI_STOP_RECORDING);
const recordingStopped = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-stopped"],
}
);
const backendStopReady = once(
PerformanceController,
EVENTS.BACKEND_READY_AFTER_RECORDING_STOP
);
const uiStateRecorded = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
expectedArgs: ["recorded"],
});
click(recordButton);
await uiStopClick;
await recordingStopped;
checkRecordButtonsStates(false, false);
await backendStopReady;
await uiStateRecorded;
await teardownToolboxAndRemoveTab(panel);
function checkRecordButtonsStates(checked, locked) {
for (const button of $$(".record-button")) {
is(
button.classList.contains("checked"),
checked,
"The record button checked state should be " + checked
);
is(
button.disabled,
locked,
"The record button locked state should be " + locked
);
}
}
});

View File

@ -1,82 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the categories are shown in the js call tree when
* platform data is enabled.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_SHOW_PLATFORM_DATA_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
busyWait,
} = require("devtools/client/performance/test/helpers/wait-utils");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
// Enable platform data to show the categories in the tree.
Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
await startRecording(panel);
// To show the `Gecko` category in the tree.
await busyWait(100);
await stopRecording(panel);
const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
await DetailsView.selectView("js-calltree");
await rendered;
is(
$(".call-tree-cells-container").hasAttribute("categories-hidden"),
false,
"The call tree cells container should show the categories now."
);
ok(
geckoCategoryPresent($$),
"A category node with the text `Gecko` is displayed in the tree."
);
// Disable platform data to hide the categories.
Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
is(
$(".call-tree-cells-container").getAttribute("categories-hidden"),
"",
"The call tree cells container should hide the categories now."
);
ok(
!geckoCategoryPresent($$),
"A category node with the text `Gecko` doesn't exist in the tree anymore."
);
await teardownToolboxAndRemoveTab(panel);
});
function geckoCategoryPresent($$) {
for (const elem of $$(".call-tree-category")) {
if (elem.textContent.trim() == "Gecko") {
return true;
}
}
return false;
}

View File

@ -1,87 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the js call tree view renders the correct columns.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_SHOW_PLATFORM_DATA_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
busyWait,
} = require("devtools/client/performance/test/helpers/wait-utils");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
// Enable platform data to show the platform functions in the tree.
Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
await startRecording(panel);
// To show the `busyWait` function in the tree.
await busyWait(100);
await stopRecording(panel);
const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
await DetailsView.selectView("js-calltree");
await rendered;
ok(
DetailsView.isViewSelected(JsCallTreeView),
"The call tree is now selected."
);
testCells($, $$, {
duration: true,
percentage: true,
allocations: false,
"self-duration": true,
"self-percentage": true,
"self-allocations": false,
samples: true,
function: true,
});
await teardownToolboxAndRemoveTab(panel);
});
function testCells($, $$, visibleCells) {
for (const cell in visibleCells) {
if (visibleCells[cell]) {
ok(
$(`.call-tree-cell[type=${cell}]`),
`At least one ${cell} column was visible in the tree.`
);
} else {
ok(
!$(`.call-tree-cell[type=${cell}]`),
`No ${cell} columns were visible in the tree.`
);
}
}
is(
$$(".call-tree-cell", $(".call-tree-item")).length,
Object.keys(visibleCells).filter(e => visibleCells[e]).length,
"The correct number of cells were found in the tree."
);
}

View File

@ -1,79 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the call tree up/down events work for js calltrees.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
synthesizeProfile,
} = require("devtools/client/performance/test/helpers/synth-utils");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
ThreadNode,
} = require("devtools/client/performance/modules/logic/tree-model");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
EVENTS,
$,
DetailsView,
OverviewView,
JsCallTreeView,
} = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
await DetailsView.selectView("js-calltree");
await rendered;
// Mock the profile used so we can get a deterministic tree created.
const profile = synthesizeProfile();
const threadNode = new ThreadNode(
profile.threads[0],
OverviewView.getTimeInterval()
);
JsCallTreeView._populateCallTree(threadNode);
JsCallTreeView.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
const firstTreeItem = $("#js-calltree-view .call-tree-item");
// DE-XUL: There are focus issues with XUL. Focus first, then synthesize the clicks
// so that keyboard events work correctly.
firstTreeItem.focus();
let count = 0;
const onFocus = () => count++;
JsCallTreeView.on("focus", onFocus);
click(firstTreeItem);
key("VK_DOWN");
key("VK_DOWN");
key("VK_DOWN");
key("VK_DOWN");
JsCallTreeView.off("focus", onFocus);
is(count, 4, "Several focus events are fired for the calltree.");
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,91 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the memory call tree view renders the correct columns.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_ALLOCATIONS_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, $, $$, DetailsView, MemoryCallTreeView } = panel.panelWin;
// Enable allocations to test.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
await startRecording(panel);
await stopRecording(panel);
const rendered = once(
MemoryCallTreeView,
EVENTS.UI_MEMORY_CALL_TREE_RENDERED
);
await DetailsView.selectView("memory-calltree");
await rendered;
ok(
DetailsView.isViewSelected(MemoryCallTreeView),
"The call tree is now selected."
);
testCells($, $$, {
duration: false,
percentage: false,
count: true,
"count-percentage": true,
size: true,
"size-percentage": true,
"self-duration": false,
"self-percentage": false,
"self-count": true,
"self-count-percentage": true,
"self-size": true,
"self-size-percentage": true,
samples: false,
function: true,
});
await teardownToolboxAndRemoveTab(panel);
});
function testCells($, $$, visibleCells) {
for (const cell in visibleCells) {
if (visibleCells[cell]) {
ok(
$(`.call-tree-cell[type=${cell}]`),
`At least one ${cell} column was visible in the tree.`
);
} else {
ok(
!$(`.call-tree-cell[type=${cell}]`),
`No ${cell} columns were visible in the tree.`
);
}
}
is(
$$(".call-tree-cell", $(".call-tree-item")).length,
Object.keys(visibleCells).filter(e => visibleCells[e]).length,
"The correct number of cells were found in the tree."
);
}

View File

@ -1,57 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if the profiler is populated by console recordings that have finished
* before it was opened.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
waitUntil,
} = require("devtools/client/performance/test/helpers/wait-utils");
const {
getSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
await console.profile("rust");
await console.profileEnd("rust");
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { PerformanceController, WaterfallView } = panel.panelWin;
await waitUntil(() => PerformanceController.getRecordings().length == 1);
await waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
const recordings = PerformanceController.getRecordings();
is(recordings.length, 1, "One recording found in the performance panel.");
is(recordings[0].isConsole(), true, "Recording came from console.profile.");
is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
const selected = getSelectedRecording(panel);
is(
selected,
recordings[0],
"The profile from console should be selected as it's the only one."
);
is(
selected.getLabel(),
"rust",
"The profile label for the first recording is correct."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,104 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if the profiler is populated by in-progress console recordings
* when it is opened.
*/
const { Constants } = require("devtools/client/performance/modules/constants");
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
waitForRecordingStoppedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
waitUntil,
} = require("devtools/client/performance/test/helpers/wait-utils");
const {
times,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
getSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
await console.profile("rust");
await console.profile("rust2");
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
await waitUntil(() => PerformanceController.getRecordings().length == 2);
const recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "Two recordings found in the performance panel.");
is(
recordings[0].isConsole(),
true,
"Recording came from console.profile (1)."
);
is(
recordings[0].getLabel(),
"rust",
"Correct label in the recording model (1)."
);
is(recordings[0].isRecording(), true, "Recording is still recording (1).");
is(
recordings[1].isConsole(),
true,
"Recording came from console.profile (2)."
);
is(
recordings[1].getLabel(),
"rust2",
"Correct label in the recording model (2)."
);
is(recordings[1].isRecording(), true, "Recording is still recording (2).");
const selected = getSelectedRecording(panel);
is(
selected,
recordings[0],
"The first console recording should be selected."
);
is(
selected.getLabel(),
"rust",
"The profile label for the first recording is correct."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
let stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profileEnd("rust");
await stopped;
stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
});
await console.profileEnd("rust2");
await stopped;
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,90 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if the profiler is populated by in-progress console recordings, and
* also console recordings that have finished before it was opened.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
waitForRecordingStoppedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
waitUntil,
} = require("devtools/client/performance/test/helpers/wait-utils");
const {
getSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
await console.profile("rust");
await console.profileEnd("rust");
await console.profile("rust2");
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { PerformanceController, WaterfallView } = panel.panelWin;
await waitUntil(() => PerformanceController.getRecordings().length == 2);
await waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
const recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "Two recordings found in the performance panel.");
is(
recordings[0].isConsole(),
true,
"Recording came from console.profile (1)."
);
is(
recordings[0].getLabel(),
"rust",
"Correct label in the recording model (1)."
);
is(recordings[0].isRecording(), false, "Recording is still recording (1).");
is(
recordings[1].isConsole(),
true,
"Recording came from console.profile (2)."
);
is(
recordings[1].getLabel(),
"rust2",
"Correct label in the recording model (2)."
);
is(recordings[1].isRecording(), true, "Recording is still recording (2).");
const selected = getSelectedRecording(panel);
is(
selected,
recordings[0],
"The first console recording should be selected."
);
is(
selected.getLabel(),
"rust",
"The profile label for the first recording is correct."
);
const stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
});
await console.profileEnd("rust2");
await stopped;
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,75 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the profiler can handle creation and stopping of console profiles
* after being opened.
*/
const { Constants } = require("devtools/client/performance/modules/constants");
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
waitForRecordingStartedEvents,
waitForRecordingStoppedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
times,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
getSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
const started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profile("rust");
await started;
const recordings = PerformanceController.getRecordings();
is(recordings.length, 1, "One recording found in the performance panel.");
is(recordings[0].isConsole(), true, "Recording came from console.profile.");
is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
is(recordings[0].isRecording(), true, "Recording is still recording.");
const selected = getSelectedRecording(panel);
is(
selected,
recordings[0],
"The profile from console should be selected as it's the only one."
);
is(
selected.getLabel(),
"rust",
"The profile label for the first recording is correct."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
const stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profileEnd("rust");
await stopped;
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,131 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that multiple recordings with the same label (non-overlapping) appear
* in the recording list.
*/
const { Constants } = require("devtools/client/performance/modules/constants");
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
waitForRecordingStartedEvents,
waitForRecordingStoppedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
times,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
getSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
let started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profile("rust");
await started;
let recordings = PerformanceController.getRecordings();
is(recordings.length, 1, "One recording found in the performance panel.");
is(
recordings[0].isConsole(),
true,
"Recording came from console.profile (1)."
);
is(
recordings[0].getLabel(),
"rust",
"Correct label in the recording model (1)."
);
is(recordings[0].isRecording(), true, "Recording is still recording (1).");
let selected = getSelectedRecording(panel);
is(
selected,
recordings[0],
"The profile from console should be selected as it's the only one."
);
is(
selected.getLabel(),
"rust",
"The profile label for the first recording is correct."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
let stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profileEnd("rust");
await stopped;
started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when an in-progress recording is selected
skipWaitingForOverview: true,
// the view state won't switch to "console-recording" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profile("rust");
await started;
recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "Two recordings found in the performance panel.");
is(
recordings[1].isConsole(),
true,
"Recording came from console.profile (2)."
);
is(
recordings[1].getLabel(),
"rust",
"Correct label in the recording model (2)."
);
is(recordings[1].isRecording(), true, "Recording is still recording (2).");
selected = getSelectedRecording(panel);
is(
selected,
recordings[0],
"The profile from console should still be selected"
);
is(
selected.getLabel(),
"rust",
"The profile label for the first recording is correct."
);
stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
});
await console.profileEnd("rust");
await stopped;
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,125 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that console recordings can overlap (not completely nested).
*/
const { Constants } = require("devtools/client/performance/modules/constants");
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
waitForRecordingStartedEvents,
waitForRecordingStoppedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
times,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
getSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
let started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profile("rust");
await started;
let recordings = PerformanceController.getRecordings();
is(recordings.length, 1, "A recording found in the performance panel.");
is(
getSelectedRecording(panel),
recordings[0],
"The first console recording should be selected."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when an in-progress recording is selected
skipWaitingForOverview: true,
// the view state won't switch to "console-recording" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profile("golang");
await started;
recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "Two recordings found in the performance panel.");
is(
getSelectedRecording(panel),
recordings[0],
"The first console recording should still be selected."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
let stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profileEnd("rust");
await stopped;
recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "Two recordings found in the performance panel.");
is(
getSelectedRecording(panel),
recordings[0],
"The first console recording should still be selected."
);
is(
recordings[0].isRecording(),
false,
"The first console recording should no longer be recording."
);
stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
});
await console.profileEnd("golang");
await stopped;
recordings = PerformanceController.getRecordings();
is(recordings.length, 2, "Two recordings found in the performance panel.");
is(
getSelectedRecording(panel),
recordings[0],
"The first console recording should still be selected."
);
is(
recordings[1].isRecording(),
false,
"The second console recording should no longer be recording."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,247 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that a call to console.profileEnd() with no label ends the
* most recent console recording, and console.profileEnd() with a label that
* does not match any pending recordings does nothing.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
waitForRecordingStartedEvents,
waitForRecordingStoppedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
idleWait,
} = require("devtools/client/performance/test/helpers/wait-utils");
const {
getSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { PerformanceController } = panel.panelWin;
let started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profile();
await started;
started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when an in-progress recording is selected
skipWaitingForOverview: true,
// the view state won't switch to "console-recording" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profile("1");
await started;
started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when an in-progress recording is selected
skipWaitingForOverview: true,
// the view state won't switch to "console-recording" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profile("2");
await started;
let recordings = PerformanceController.getRecordings();
let selected = getSelectedRecording(panel);
is(recordings.length, 3, "Three recordings found in the performance panel.");
is(recordings[0].getLabel(), "", "Checking label of recording 1");
is(recordings[1].getLabel(), "1", "Checking label of recording 2");
is(recordings[2].getLabel(), "2", "Checking label of recording 3");
is(
selected,
recordings[0],
"The first console recording should be selected."
);
is(
recordings[0].isRecording(),
true,
"All recordings should now be started. (1)"
);
is(
recordings[1].isRecording(),
true,
"All recordings should now be started. (2)"
);
is(
recordings[2].isRecording(),
true,
"All recordings should now be started. (3)"
);
let stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
// the view state won't switch to "recorded" unless the new
// finished recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profileEnd();
await stopped;
selected = getSelectedRecording(panel);
recordings = PerformanceController.getRecordings();
is(recordings.length, 3, "Three recordings found in the performance panel.");
is(
selected,
recordings[0],
"The first console recording should still be selected."
);
is(
recordings[0].isRecording(),
true,
"The not most recent recording should not stop " +
"when calling console.profileEnd with no args."
);
is(
recordings[1].isRecording(),
true,
"The not most recent recording should not stop " +
"when calling console.profileEnd with no args."
);
is(
recordings[2].isRecording(),
false,
"Only the most recent recording should stop " +
"when calling console.profileEnd with no args."
);
info("Trying to `profileEnd` a non-existent console recording.");
console.profileEnd("fxos");
await idleWait(1000);
selected = getSelectedRecording(panel);
recordings = PerformanceController.getRecordings();
is(recordings.length, 3, "Three recordings found in the performance panel.");
is(
selected,
recordings[0],
"The first console recording should still be selected."
);
is(
recordings[0].isRecording(),
true,
"The first recording should not be ended yet."
);
is(
recordings[1].isRecording(),
true,
"The second recording should not be ended yet."
);
is(
recordings[2].isRecording(),
false,
"The third recording should still be ended."
);
stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
// the view state won't switch to "recorded" unless the new
// finished recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profileEnd();
await stopped;
selected = getSelectedRecording(panel);
recordings = PerformanceController.getRecordings();
is(recordings.length, 3, "Three recordings found in the performance panel.");
is(
selected,
recordings[0],
"The first console recording should still be selected."
);
is(
recordings[0].isRecording(),
true,
"The first recording should not be ended yet."
);
is(
recordings[1].isRecording(),
false,
"The second recording should not be ended yet."
);
is(
recordings[2].isRecording(),
false,
"The third recording should still be ended."
);
stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profileEnd();
await stopped;
selected = getSelectedRecording(panel);
recordings = PerformanceController.getRecordings();
is(recordings.length, 3, "Three recordings found in the performance panel.");
is(
selected,
recordings[0],
"The first console recording should be selected."
);
is(
recordings[0].isRecording(),
false,
"All recordings should now be ended. (1)"
);
is(
recordings[1].isRecording(),
false,
"All recordings should now be ended. (2)"
);
is(
recordings[2].isRecording(),
false,
"All recordings should now be ended. (3)"
);
info("Trying to `profileEnd` with no pending recordings.");
console.profileEnd();
await idleWait(1000);
ok(
true,
"Calling console.profileEnd() with no argument and no pending recordings " +
"does not throw."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,311 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if the profiler can correctly handle simultaneous console and manual
* recordings (via `console.profile` and clicking the record button).
*/
const { Constants } = require("devtools/client/performance/modules/constants");
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
waitForRecordingStartedEvents,
waitForRecordingStoppedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
times,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
setSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
/**
* The following are bit flag constants that are used to represent the state of a
* recording.
*/
// Represents a manually recorded profile, if a user hit the record button.
const MANUAL = 0;
// Represents a recorded profile from console.profile().
const CONSOLE = 1;
// Represents a profile that is currently recording.
const RECORDING = 2;
// Represents a profile that is currently selected.
const SELECTED = 4;
/**
* Utility function to provide a meaningful inteface for testing that the bits
* match for the recording state.
* @param {integer} expected - The expected bit values packed in an integer.
* @param {integer} actual - The actual bit values packed in an integer.
*/
function hasBitFlag(expected, actual) {
return !!(expected & actual);
}
add_task(async function() {
// This test seems to take a very long time to finish on Linux VMs.
requestLongerTimeout(4);
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
info("Recording 1 - Starting console.profile()...");
let started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
});
await console.profile("rust");
await started;
testRecordings(PerformanceController, [CONSOLE + SELECTED + RECORDING]);
info("Recording 2 - Starting manual recording...");
await startRecording(panel);
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL + RECORDING + SELECTED,
]);
info('Recording 3 - Starting console.profile("3")...');
started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when an in-progress recording is selected
skipWaitingForOverview: true,
// the view state won't switch to "console-recording" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profile("3");
await started;
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL + RECORDING + SELECTED,
CONSOLE + RECORDING,
]);
info('Recording 4 - Starting console.profile("4")...');
started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when an in-progress recording is selected
skipWaitingForOverview: true,
// the view state won't switch to "console-recording" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profile("4");
await started;
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL + RECORDING + SELECTED,
CONSOLE + RECORDING,
CONSOLE + RECORDING,
]);
info("Recording 4 - Ending console.profileEnd()...");
let stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
// the view state won't switch to "recorded" unless the new
// finished recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profileEnd();
await stopped;
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL + RECORDING + SELECTED,
CONSOLE + RECORDING,
CONSOLE,
]);
info("Recording 4 - Select last recording...");
let recordingSelected = once(
PerformanceController,
EVENTS.RECORDING_SELECTED
);
setSelectedRecording(panel, 3);
await recordingSelected;
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL + RECORDING,
CONSOLE + RECORDING,
CONSOLE + SELECTED,
]);
ok(
!OverviewView.isRendering(),
"Stop rendering overview when a completed recording is selected."
);
info("Recording 2 - Stop manual recording.");
await stopRecording(panel);
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL + SELECTED,
CONSOLE + RECORDING,
CONSOLE,
]);
ok(
!OverviewView.isRendering(),
"Stop rendering overview when a completed recording is selected."
);
info("Recording 1 - Select first recording.");
recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
setSelectedRecording(panel, 0);
await recordingSelected;
testRecordings(PerformanceController, [
CONSOLE + RECORDING + SELECTED,
MANUAL,
CONSOLE + RECORDING,
CONSOLE,
]);
ok(
OverviewView.isRendering(),
"Should be rendering overview a recording in progress is selected."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
info("Ending console.profileEnd()...");
stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
// the view state won't switch to "recorded" unless the new
// finished recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profileEnd();
await stopped;
testRecordings(PerformanceController, [
CONSOLE + RECORDING + SELECTED,
MANUAL,
CONSOLE,
CONSOLE,
]);
ok(
OverviewView.isRendering(),
"Should be rendering overview a recording in progress is selected."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
info("Recording 5 - Start one more manual recording.");
await startRecording(panel);
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL,
CONSOLE,
CONSOLE,
MANUAL + RECORDING + SELECTED,
]);
ok(
OverviewView.isRendering(),
"Should be rendering overview a recording in progress is selected."
);
// Ensure overview is still rendering.
await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
});
info("Recording 5 - Stop manual recording.");
await stopRecording(panel);
testRecordings(PerformanceController, [
CONSOLE + RECORDING,
MANUAL,
CONSOLE,
CONSOLE,
MANUAL + SELECTED,
]);
ok(
!OverviewView.isRendering(),
"Stop rendering overview when a completed recording is selected."
);
info("Recording 1 - Ending console.profileEnd()...");
stopped = waitForRecordingStoppedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when a finished recording is selected
skipWaitingForOverview: true,
skipWaitingForSubview: true,
// the view state won't switch to "recorded" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profileEnd();
await stopped;
testRecordings(PerformanceController, [
CONSOLE,
MANUAL,
CONSOLE,
CONSOLE,
MANUAL + SELECTED,
]);
ok(
!OverviewView.isRendering(),
"Stop rendering overview when a completed recording is selected."
);
await teardownToolboxAndRemoveTab(panel);
});
function testRecordings(controller, expectedBitFlags) {
const recordings = controller.getRecordings();
const current = controller.getCurrentRecording();
is(
recordings.length,
expectedBitFlags.length,
"Expected number of recordings."
);
recordings.forEach((recording, i) => {
const expected = expectedBitFlags[i];
is(
recording.isConsole(),
hasBitFlag(expected, CONSOLE),
`Recording ${i + 1} has expected console state.`
);
is(
recording.isRecording(),
hasBitFlag(expected, RECORDING),
`Recording ${i + 1} has expected console state.`
);
is(
recording == current,
hasBitFlag(expected, SELECTED),
`Recording ${i + 1} has expected selected state.`
);
});
}

View File

@ -1,83 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that an error is not thrown when clearing out the recordings if there's
* an in-progress console profile and that console profiles are not cleared
* if in progress.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
waitForRecordingStartedEvents,
} = require("devtools/client/performance/test/helpers/actions");
const {
idleWait,
} = require("devtools/client/performance/test/helpers/wait-utils");
add_task(async function() {
const { target, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
const { panel } = await initPerformanceInTab({ tab: target.localTab });
const { PerformanceController } = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
info("Starting console.profile()...");
const started = waitForRecordingStartedEvents(panel, {
// only emitted for manual recordings
skipWaitingForBackendReady: true,
// only emitted when an in-progress recording is selected
skipWaitingForOverview: true,
// the view state won't switch to "console-recording" unless the new
// in-progress recording is selected, which won't happen
skipWaitingForViewState: true,
});
await console.profile();
await started;
await PerformanceController.clearRecordings();
let recordings = PerformanceController.getRecordings();
is(recordings.length, 1, "One recording found in the performance panel.");
is(recordings[0].isConsole(), true, "Recording came from console.profile.");
is(recordings[0].getLabel(), "", "Correct label in the recording model.");
is(
PerformanceController.getCurrentRecording(),
recordings[0],
"There current recording should be the first one."
);
info("Attempting to end console.profileEnd()...");
await console.profileEnd();
await idleWait(1000);
ok(
true,
"Stopping an in-progress console profile after clearing recordings does not throw."
);
await PerformanceController.clearRecordings();
recordings = PerformanceController.getRecordings();
is(recordings.length, 0, "No recordings found");
is(
PerformanceController.getCurrentRecording(),
null,
"There should be no current recording."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,89 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the details view toggles subviews.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
command,
} = require("devtools/client/performance/test/helpers/input-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, $, DetailsView } = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
info("Checking views on startup.");
checkViews(DetailsView, $, "waterfall");
// Select calltree view.
let viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, {
spreadArgs: true,
});
command($("toolbarbutton[data-view='js-calltree']"));
let [viewName] = await viewChanged;
is(viewName, "js-calltree", "UI_DETAILS_VIEW_SELECTED fired with view name");
checkViews(DetailsView, $, "js-calltree");
// Select js flamegraph view.
viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, {
spreadArgs: true,
});
command($("toolbarbutton[data-view='js-flamegraph']"));
[viewName] = await viewChanged;
is(
viewName,
"js-flamegraph",
"UI_DETAILS_VIEW_SELECTED fired with view name"
);
checkViews(DetailsView, $, "js-flamegraph");
// Select waterfall view.
viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, {
spreadArgs: true,
});
command($("toolbarbutton[data-view='waterfall']"));
[viewName] = await viewChanged;
is(viewName, "waterfall", "UI_DETAILS_VIEW_SELECTED fired with view name");
checkViews(DetailsView, $, "waterfall");
await teardownToolboxAndRemoveTab(panel);
});
function checkViews(DetailsView, $, currentView) {
for (const viewName in DetailsView.components) {
const button = $(`toolbarbutton[data-view="${viewName}"]`);
is(
DetailsView.el.selectedPanel.id,
DetailsView.components[currentView].id,
`DetailsView correctly has ${currentView} selected.`
);
if (viewName == currentView) {
ok(button.getAttribute("checked"), `${viewName} button checked.`);
} else {
ok(!button.getAttribute("checked"), `${viewName} button not checked.`);
}
}
}

View File

@ -1,75 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the details view utility functions work as advertised.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
EVENTS,
DetailsView,
WaterfallView,
JsCallTreeView,
JsFlameGraphView,
} = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is selected by default in the details view."
);
// Select js calltree view.
let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
await DetailsView.selectView("js-calltree");
await selected;
ok(
DetailsView.isViewSelected(JsCallTreeView),
"The js calltree view is now selected in the details view."
);
// Select js flamegraph view.
selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
await DetailsView.selectView("js-flamegraph");
await selected;
ok(
DetailsView.isViewSelected(JsFlameGraphView),
"The js flamegraph view is now selected in the details view."
);
// Select waterfall view.
selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
await DetailsView.selectView("waterfall");
await selected;
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is now selected in the details view."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,176 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the details view hides the allocations buttons when a recording
* does not have allocations data ("withAllocations": false), and that when an
* allocations panel is selected to a panel that does not have allocations goes
* to a default panel instead.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_ALLOCATIONS_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
setSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
EVENTS,
$,
DetailsView,
WaterfallView,
MemoryCallTreeView,
MemoryFlameGraphView,
} = panel.panelWin;
const flameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
const callBtn = $("toolbarbutton[data-view='memory-calltree']");
// Disable allocations to prevent recording them.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, false);
await startRecording(panel);
await stopRecording(panel);
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is selected by default in the details view."
);
// Re-enable allocations to test.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
// The toolbar buttons will always be hidden when a recording isn't available,
// so make sure we have one that's finished.
await startRecording(panel);
await stopRecording(panel);
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is still selected in the details view."
);
is(
callBtn.hidden,
false,
"The `memory-calltree` button is shown when recording has memory data."
);
is(
flameBtn.hidden,
false,
"The `memory-flamegraph` button is shown when recording has memory data."
);
let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
DetailsView.selectView("memory-calltree");
await selected;
await rendered;
ok(
DetailsView.isViewSelected(MemoryCallTreeView),
"The memory call tree view can now be selected."
);
selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
DetailsView.selectView("memory-flamegraph");
await selected;
await rendered;
ok(
DetailsView.isViewSelected(MemoryFlameGraphView),
"The memory flamegraph view can now be selected."
);
// Select the first recording with no memory data.
selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
setSelectedRecording(panel, 0);
await selected;
await rendered;
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is now selected " +
"when switching back to a recording that does not have memory data."
);
is(
callBtn.hidden,
true,
"The `memory-calltree` button is hidden when recording has no memory data."
);
is(
flameBtn.hidden,
true,
"The `memory-flamegraph` button is hidden when recording has no memory data."
);
// Go back to the recording with memory data.
rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
setSelectedRecording(panel, 1);
await rendered;
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is still selected in the details view."
);
is(
callBtn.hidden,
false,
"The `memory-calltree` button is shown when recording has memory data."
);
is(
flameBtn.hidden,
false,
"The `memory-flamegraph` button is shown when recording has memory data."
);
selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
DetailsView.selectView("memory-calltree");
await selected;
await rendered;
ok(
DetailsView.isViewSelected(MemoryCallTreeView),
"The memory call tree view can be " +
"selected again after going back to the view with memory data."
);
selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
DetailsView.selectView("memory-flamegraph");
await selected;
await rendered;
ok(
DetailsView.isViewSelected(MemoryFlameGraphView),
"The memory flamegraph view can " +
"be selected again after going back to the view with memory data."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,253 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the details view hides the toolbar buttons when a recording
* doesn't exist or is in progress.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
setSelectedRecording,
getSelectedRecordingIndex,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, $, PerformanceController, WaterfallView } = panel.panelWin;
const waterfallBtn = $("toolbarbutton[data-view='waterfall']");
const jsFlameBtn = $("toolbarbutton[data-view='js-flamegraph']");
const jsCallBtn = $("toolbarbutton[data-view='js-calltree']");
const memFlameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
const memCallBtn = $("toolbarbutton[data-view='memory-calltree']");
is(
waterfallBtn.hidden,
true,
"The `waterfall` button is hidden when tool starts."
);
is(
jsFlameBtn.hidden,
true,
"The `js-flamegraph` button is hidden when tool starts."
);
is(
jsCallBtn.hidden,
true,
"The `js-calltree` button is hidden when tool starts."
);
is(
memFlameBtn.hidden,
true,
"The `memory-flamegraph` button is hidden when tool starts."
);
is(
memCallBtn.hidden,
true,
"The `memory-calltree` button is hidden when tool starts."
);
await startRecording(panel);
is(
waterfallBtn.hidden,
true,
"The `waterfall` button is hidden when recording starts."
);
is(
jsFlameBtn.hidden,
true,
"The `js-flamegraph` button is hidden when recording starts."
);
is(
jsCallBtn.hidden,
true,
"The `js-calltree` button is hidden when recording starts."
);
is(
memFlameBtn.hidden,
true,
"The `memory-flamegraph` button is hidden when recording starts."
);
is(
memCallBtn.hidden,
true,
"The `memory-calltree` button is hidden when recording starts."
);
await stopRecording(panel);
is(
waterfallBtn.hidden,
false,
"The `waterfall` button is visible when recording ends."
);
is(
jsFlameBtn.hidden,
false,
"The `js-flamegraph` button is visible when recording ends."
);
is(
jsCallBtn.hidden,
false,
"The `js-calltree` button is visible when recording ends."
);
is(
memFlameBtn.hidden,
true,
"The `memory-flamegraph` button is hidden when recording ends."
);
is(
memCallBtn.hidden,
true,
"The `memory-calltree` button is hidden when recording ends."
);
await startRecording(panel);
is(
waterfallBtn.hidden,
true,
"The `waterfall` button is hidden when another recording starts."
);
is(
jsFlameBtn.hidden,
true,
"The `js-flamegraph` button is hidden when another recording starts."
);
is(
jsCallBtn.hidden,
true,
"The `js-calltree` button is hidden when another recording starts."
);
is(
memFlameBtn.hidden,
true,
"The `memory-flamegraph` button is hidden when another recording starts."
);
is(
memCallBtn.hidden,
true,
"The `memory-calltree` button is hidden when another recording starts."
);
let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
setSelectedRecording(panel, 0);
await selected;
await rendered;
let selectedIndex = getSelectedRecordingIndex(panel);
is(selectedIndex, 0, "The first recording was selected again.");
is(
waterfallBtn.hidden,
false,
"The `waterfall` button is visible when first recording selected."
);
is(
jsFlameBtn.hidden,
false,
"The `js-flamegraph` button is visible when first recording selected."
);
is(
jsCallBtn.hidden,
false,
"The `js-calltree` button is visible when first recording selected."
);
is(
memFlameBtn.hidden,
true,
"The `memory-flamegraph` button is hidden when first recording selected."
);
is(
memCallBtn.hidden,
true,
"The `memory-calltree` button is hidden when first recording selected."
);
selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
setSelectedRecording(panel, 1);
await selected;
selectedIndex = getSelectedRecordingIndex(panel);
is(selectedIndex, 1, "The second recording was selected again.");
is(
waterfallBtn.hidden,
true,
"The `waterfall button` still is hidden when second recording selected."
);
is(
jsFlameBtn.hidden,
true,
"The `js-flamegraph button` still is hidden when second recording selected."
);
is(
jsCallBtn.hidden,
true,
"The `js-calltree button` still is hidden when second recording selected."
);
is(
memFlameBtn.hidden,
true,
"The `memory-flamegraph button` still is hidden when second recording selected."
);
is(
memCallBtn.hidden,
true,
"The `memory-calltree button` still is hidden when second recording selected."
);
rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
await stopRecording(panel);
await rendered;
selectedIndex = getSelectedRecordingIndex(panel);
is(selectedIndex, 1, "The second recording is still selected.");
is(
waterfallBtn.hidden,
false,
"The `waterfall` button is visible when second recording finished."
);
is(
jsFlameBtn.hidden,
false,
"The `js-flamegraph` button is visible when second recording finished."
);
is(
jsCallBtn.hidden,
false,
"The `js-calltree` button is visible when second recording finished."
);
is(
memFlameBtn.hidden,
true,
"The `memory-flamegraph` button is hidden when second recording finished."
);
is(
memCallBtn.hidden,
true,
"The `memory-calltree` button is hidden when second recording finished."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,68 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the same details view is selected after recordings are cleared
* and a new recording starts.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
EVENTS,
PerformanceController,
DetailsView,
JsCallTreeView,
} = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
const selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
await DetailsView.selectView("js-calltree");
await selected;
await rendered;
ok(
DetailsView.isViewSelected(JsCallTreeView),
"The js calltree view is now selected in the details view."
);
const cleared = once(PerformanceController, EVENTS.RECORDING_SELECTED, {
expectedArgs: [null],
});
await PerformanceController.clearRecordings();
await cleared;
await startRecording(panel);
await stopRecording(panel, {
expectedViewClass: "JsCallTreeView",
expectedViewEvent: "UI_JS_CALL_TREE_RENDERED",
});
ok(
DetailsView.isViewSelected(JsCallTreeView),
"The js calltree view is still selected in the details view."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,105 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that when flame chart views scroll to change selection,
* other detail views are rerendered.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
scrollCanvasGraph,
HORIZONTAL_AXIS,
} = require("devtools/client/performance/test/helpers/input-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
EVENTS,
OverviewView,
DetailsView,
WaterfallView,
JsCallTreeView,
JsFlameGraphView,
} = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
const waterfallRendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
OverviewView.setTimeInterval({ startTime: 10, endTime: 20 });
await waterfallRendered;
// Select the call tree to make sure it's initialized and ready to receive
// redrawing requests once reselected.
const callTreeRendered = once(
JsCallTreeView,
EVENTS.UI_JS_CALL_TREE_RENDERED
);
await DetailsView.selectView("js-calltree");
await callTreeRendered;
// Switch to the flamegraph and perform a scroll over the visualization.
// The waterfall and call tree should get rerendered when reselected.
const flamegraphRendered = once(
JsFlameGraphView,
EVENTS.UI_JS_FLAMEGRAPH_RENDERED
);
await DetailsView.selectView("js-flamegraph");
await flamegraphRendered;
const overviewRangeSelected = once(
OverviewView,
EVENTS.UI_OVERVIEW_RANGE_SELECTED
);
const waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
const callTreeRerendered = once(
JsCallTreeView,
EVENTS.UI_JS_CALL_TREE_RENDERED
);
once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED).then(() => {
ok(
false,
"FlameGraphView should not publicly rerender, the internal state " +
"and drawing should be handled by the underlying widget."
);
});
// Reset the range to full view, trigger a "selection" event as if
// our mouse has done this
scrollCanvasGraph(JsFlameGraphView.graph, {
axis: HORIZONTAL_AXIS,
wheel: 200,
x: 10,
});
await overviewRangeSelected;
ok(true, "Overview range was changed.");
await DetailsView.selectView("waterfall");
await waterfallRerendered;
ok(true, "Waterfall rerendered by flame graph changing interval.");
await DetailsView.selectView("js-calltree");
await callTreeRerendered;
ok(true, "CallTree rerendered by flame graph changing interval.");
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,63 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that events don't bleed between detail views.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
// The waterfall should render by default, and we want to make
// sure that the render events don't bleed between detail views
// so test that's the case after both views have been created.
const callTreeRendered = once(
JsCallTreeView,
EVENTS.UI_JS_CALL_TREE_RENDERED
);
await DetailsView.selectView("js-calltree");
await callTreeRendered;
const waterfallSelected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
await DetailsView.selectView("waterfall");
await waterfallSelected;
once(JsCallTreeView, EVENTS.UI_WATERFALL_RENDERED).then(() =>
ok(false, "JsCallTreeView should not receive UI_WATERFALL_RENDERED event.")
);
await startRecording(panel);
await stopRecording(panel);
const callTreeRerendered = once(
JsCallTreeView,
EVENTS.UI_JS_CALL_TREE_RENDERED
);
await DetailsView.selectView("js-calltree");
await callTreeRerendered;
ok(true, "Test passed.");
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,53 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the waterfall view renders content after recording.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { DetailsView, WaterfallView } = panel.panelWin;
await startRecording(panel);
// Already waits for EVENTS.UI_WATERFALL_RENDERED.
await stopRecording(panel);
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is selected by default in the details view."
);
ok(true, "WaterfallView rendered after recording is stopped.");
await startRecording(panel);
// Already waits for EVENTS.UI_WATERFALL_RENDERED.
await stopRecording(panel);
ok(
DetailsView.isViewSelected(WaterfallView),
"The waterfall view is still selected in the details view."
);
ok(
true,
"WaterfallView rendered again after recording completed a second time."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,51 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the js call tree view renders content after recording.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
await DetailsView.selectView("js-calltree");
await rendered;
ok(true, "JsCallTreeView rendered after recording is stopped.");
await startRecording(panel);
await stopRecording(panel, {
expectedViewClass: "JsCallTreeView",
expectedViewEvent: "UI_JS_CALL_TREE_RENDERED",
});
ok(
true,
"JsCallTreeView rendered again after recording completed a second time."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,51 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the js flamegraph view renders content after recording.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
await startRecording(panel);
await stopRecording(panel);
const rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
await DetailsView.selectView("js-flamegraph");
await rendered;
ok(true, "JsFlameGraphView rendered after recording is stopped.");
await startRecording(panel);
await stopRecording(panel, {
expectedViewClass: "JsFlameGraphView",
expectedViewEvent: "UI_JS_FLAMEGRAPH_RENDERED",
});
ok(
true,
"JsFlameGraphView rendered again after recording completed a second time."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,60 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the memory call tree view renders content after recording.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_ALLOCATIONS_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
// Enable allocations to test.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
await startRecording(panel);
await stopRecording(panel);
const rendered = once(
MemoryCallTreeView,
EVENTS.UI_MEMORY_CALL_TREE_RENDERED
);
await DetailsView.selectView("memory-calltree");
await rendered;
ok(true, "MemoryCallTreeView rendered after recording is stopped.");
await startRecording(panel);
await stopRecording(panel, {
expectedViewClass: "MemoryCallTreeView",
expectedViewEvent: "UI_MEMORY_CALL_TREE_RENDERED",
});
ok(
true,
"MemoryCallTreeView rendered again after recording completed a second time."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,60 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the memory call tree view renders content after recording.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_ALLOCATIONS_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
// Enable allocations to test.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
await startRecording(panel);
await stopRecording(panel);
const rendered = once(
MemoryFlameGraphView,
EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED
);
await DetailsView.selectView("memory-flamegraph");
await rendered;
ok(true, "MemoryFlameGraphView rendered after recording is stopped.");
await startRecording(panel);
await stopRecording(panel, {
expectedViewClass: "MemoryFlameGraphView",
expectedViewEvent: "UI_MEMORY_FLAMEGRAPH_RENDERED",
});
ok(
true,
"MemoryFlameGraphView rendered again after recording completed a second time."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,66 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if the sidebar is updated with "DOMContentLoaded" and "load" markers.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
reload,
} = require("devtools/client/performance/test/helpers/actions");
const {
waitUntil,
} = require("devtools/client/performance/test/helpers/wait-utils");
add_task(async function() {
// Run this test without server targets as reload would introduce a target switch
// with a new profile record which wouldn't introduce the DOM load events.
// This panel has now been deprecated, we might later get rid of this test
// once removing client side targets (bug 1721852)
await SpecialPowers.pushPrefEnv({
set: [["devtools.target-switching.server.enabled", false]],
});
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { PerformanceController } = panel.panelWin;
await startRecording(panel);
await reload(panel);
await waitUntil(() => {
// Wait until we get the necessary markers.
const markers = PerformanceController.getCurrentRecording().getMarkers();
if (
!markers.some(m => m.name == "document::DOMContentLoaded") ||
!markers.some(m => m.name == "document::Load")
) {
return false;
}
ok(
markers.filter(m => m.name == "document::DOMContentLoaded").length == 1,
"There should only be one `DOMContentLoaded` marker."
);
ok(
markers.filter(m => m.name == "document::Load").length == 1,
"There should only be one `load` marker."
);
return true;
});
await stopRecording(panel);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,58 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Test behavior while target-switching.
*/
const {
MAIN_PROCESS_URL,
SIMPLE_URL: CONTENT_PROCESS_URL,
} = require("devtools/client/performance/test/helpers/urls");
const {
BrowserTestUtils,
} = require("resource://testing-common/BrowserTestUtils.jsm");
const {
addTab,
} = require("devtools/client/performance/test/helpers/tab-utils");
const {
initPerformanceInTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
add_task(async function() {
info("Open a page running on content process");
const tab = await addTab({
url: CONTENT_PROCESS_URL,
win: window,
});
info("Open the performance panel");
const { panel } = await initPerformanceInTab({ tab });
const { PerformanceController, PerformanceView, EVENTS } = panel.panelWin;
info("Start recording");
await startRecording(panel);
await PerformanceView.once(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED);
info("Navigate to a page running on main process");
BrowserTestUtils.loadURI(tab.linkedBrowser, MAIN_PROCESS_URL);
await PerformanceView.once(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED);
info("Return to a page running on content process again");
BrowserTestUtils.loadURI(tab.linkedBrowser, CONTENT_PROCESS_URL);
await PerformanceView.once(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED);
info("Stop recording");
await stopRecording(panel);
const recordings = PerformanceController.getRecordings();
is(recordings.length, 3, "Have a record for every target-switching");
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,146 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* eslint-disable */
/**
* Tests that the marker details on GC markers displays allocation
* buttons and snaps to the correct range
*/
async function spawnTest() {
let { panel } = await initPerformance(ALLOCS_URL);
let { $, $$, EVENTS, PerformanceController, OverviewView, DetailsView, WaterfallView, MemoryCallTreeView } = panel.panelWin;
let EPSILON = 0.00001;
Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
await startRecording(panel);
await idleWait(1000);
await stopRecording(panel);
injectGCMarkers(PerformanceController, WaterfallView);
// Select everything
let rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
await rendered;
let bars = $$(".waterfall-marker-bar");
let gcMarkers = PerformanceController.getCurrentRecording().getMarkers();
ok(gcMarkers.length === 9, "should have 9 GC markers");
ok(bars.length === 9, "should have 9 GC markers rendered");
/**
* Check when it's the second marker of the first GC cycle.
*/
let targetMarker = gcMarkers[1];
let targetBar = bars[1];
info(`Clicking GC Marker of type ${targetMarker.causeName} ${targetMarker.start}:${targetMarker.end}`);
EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
let showAllocsButton;
// On slower machines this can not be found immediately?
await waitUntil(() => showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']"));
ok(showAllocsButton, "GC buttons when allocations are enabled");
rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
await rendered;
is(OverviewView.getTimeInterval().startTime, 0, "When clicking first GC, should use 0 as start time");
within(OverviewView.getTimeInterval().endTime, targetMarker.start, EPSILON, "Correct end time range");
let duration = PerformanceController.getCurrentRecording().getDuration();
rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
OverviewView.setTimeInterval({ startTime: 0, endTime: duration });
await DetailsView.selectView("waterfall");
await rendered;
/**
* Check when there is a previous GC cycle
*/
bars = $$(".waterfall-marker-bar");
targetMarker = gcMarkers[4];
targetBar = bars[4];
info(`Clicking GC Marker of type ${targetMarker.causeName} ${targetMarker.start}:${targetMarker.end}`);
EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
// On slower machines this can not be found immediately?
await waitUntil(() => showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']"));
ok(showAllocsButton, "GC buttons when allocations are enabled");
rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
await rendered;
within(OverviewView.getTimeInterval().startTime, gcMarkers[2].end, EPSILON,
"selection start range is last marker from previous GC cycle.");
within(OverviewView.getTimeInterval().endTime, targetMarker.start, EPSILON,
"selection end range is current GC marker's start time");
/**
* Now with allocations disabled
*/
// Reselect the entire recording -- due to bug 1196945, the new recording
// won't reset the selection
duration = PerformanceController.getCurrentRecording().getDuration();
rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
OverviewView.setTimeInterval({ startTime: 0, endTime: duration });
await rendered;
Services.prefs.setBoolPref(ALLOCATIONS_PREF, false);
await startRecording(panel);
rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
await stopRecording(panel);
await rendered;
injectGCMarkers(PerformanceController, WaterfallView);
// Select everything
rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
await rendered;
ok(true, "WaterfallView rendered after recording is stopped.");
bars = $$(".waterfall-marker-bar");
gcMarkers = PerformanceController.getCurrentRecording().getMarkers();
EventUtils.sendMouseEvent({ type: "mousedown" }, bars[0]);
showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']");
ok(!showAllocsButton, "No GC buttons when allocations are disabled");
await teardown(panel);
finish();
}
function injectGCMarkers(controller, waterfall) {
// Push some fake GC markers into the recording
let realMarkers = controller.getCurrentRecording().getMarkers();
// Invalidate marker cache
waterfall._cache.delete(realMarkers);
realMarkers.length = 0;
for (let gcMarker of GC_MARKERS) {
realMarkers.push(gcMarker);
}
}
var GC_MARKERS = [
{ causeName: "TOO_MUCH_MALLOC", cycle: 1 },
{ causeName: "TOO_MUCH_MALLOC", cycle: 1 },
{ causeName: "TOO_MUCH_MALLOC", cycle: 1 },
{ causeName: "ALLOC_TRIGGER", cycle: 2 },
{ causeName: "ALLOC_TRIGGER", cycle: 2 },
{ causeName: "ALLOC_TRIGGER", cycle: 2 },
{ causeName: "SET_NEW_DOCUMENT", cycle: 3 },
{ causeName: "SET_NEW_DOCUMENT", cycle: 3 },
{ causeName: "SET_NEW_DOCUMENT", cycle: 3 },
].map((marker, i) => {
marker.name = "GarbageCollection";
marker.start = 50 + (i * 10);
marker.end = marker.start + 9;
return marker;
});
/* eslint-enable */

View File

@ -1,66 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the toolbox tab for performance is highlighted when recording,
* whether already loaded, or via console.profile with an unloaded performance tools.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInTab,
initConsoleInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
waitUntil,
} = require("devtools/client/performance/test/helpers/wait-utils");
add_task(async function() {
const { target, toolbox, console } = await initConsoleInNewTab({
url: SIMPLE_URL,
win: window,
});
const tab = toolbox.doc.getElementById("toolbox-tab-performance");
await console.profile("rust");
await waitUntil(() => tab.classList.contains("highlighted"));
ok(
tab.classList.contains("highlighted"),
"Performance tab is highlighted during " +
"recording from console.profile when unloaded."
);
await console.profileEnd("rust");
await waitUntil(() => !tab.classList.contains("highlighted"));
ok(
!tab.classList.contains("highlighted"),
"Performance tab is no longer highlighted when console.profile recording finishes."
);
const { panel } = await initPerformanceInTab({ tab: target.localTab });
await startRecording(panel);
ok(
tab.classList.contains("highlighted"),
"Performance tab is highlighted during recording while in performance tool."
);
await stopRecording(panel);
ok(
!tab.classList.contains("highlighted"),
"Performance tab is no longer highlighted when recording finishes."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,80 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the recordings view shows the right label while recording, after
* recording, and once the record has loaded.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
getSelectedRecording,
getDurationLabelText,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, PerformanceController } = panel.panelWin;
const { L10N } = require("devtools/client/performance/modules/global");
await startRecording(panel);
is(
getDurationLabelText(panel, 0),
L10N.getStr("recordingsList.recordingLabel"),
"The duration node should show the 'recording' message while recording"
);
const recordingStopping = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-stopping"],
}
);
const recordingStopped = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-stopped"],
}
);
const everythingStopped = stopRecording(panel);
await recordingStopping;
is(
getDurationLabelText(panel, 0),
L10N.getStr("recordingsList.loadingLabel"),
"The duration node should show the 'loading' message while stopping"
);
await recordingStopped;
const selected = getSelectedRecording(panel);
is(
getDurationLabelText(panel, 0),
L10N.getFormatStr(
"recordingsList.durationLabel",
selected.getDuration().toFixed(0)
),
"The duration node should show the duration after the record has stopped"
);
await everythingStopped;
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,129 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the details view is locked after recording has stopped and before
* the recording has finished loading.
* Also test that the details view isn't locked if the recording that is being
* stopped isn't the active one.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
const {
getSelectedRecordingIndex,
setSelectedRecording,
} = require("devtools/client/performance/test/helpers/recording-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, $, PerformanceController } = panel.panelWin;
const detailsContainer = $("#details-pane-container");
const recordingNotice = $("#recording-notice");
const loadingNotice = $("#loading-notice");
const detailsPane = $("#details-pane");
await startRecording(panel);
is(
detailsContainer.selectedPanel,
recordingNotice,
"The recording-notice is shown while recording."
);
let recordingStopping = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-stopping"],
}
);
let recordingStopped = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-stopped"],
}
);
let everythingStopped = stopRecording(panel);
await recordingStopping;
is(
detailsContainer.selectedPanel,
loadingNotice,
"The loading-notice is shown while the record is stopping."
);
await recordingStopped;
is(
detailsContainer.selectedPanel,
detailsPane,
"The details panel is shown after the record has stopped."
);
await everythingStopped;
await startRecording(panel);
info("While the 2nd record is still going, switch to the first one.");
const recordingSelected = once(
PerformanceController,
EVENTS.RECORDING_SELECTED
);
setSelectedRecording(panel, 0);
await recordingSelected;
recordingStopping = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-stopping"],
}
);
recordingStopped = once(
PerformanceController,
EVENTS.RECORDING_STATE_CHANGE,
{
expectedArgs: ["recording-stopped"],
}
);
everythingStopped = stopRecording(panel);
await recordingStopping;
is(
detailsContainer.selectedPanel,
detailsPane,
"The details panel is still shown while the 2nd record is being stopped."
);
is(
getSelectedRecordingIndex(panel),
0,
"The first record is still selected."
);
await recordingStopped;
is(
detailsContainer.selectedPanel,
detailsPane,
"The details panel is still shown after the 2nd record has stopped."
);
is(getSelectedRecordingIndex(panel), 1, "The second record is now selected.");
await everythingStopped;
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,143 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* eslint-disable */
/**
* Tests if the Marker Details view renders all properties expected
* for each marker.
*/
async function spawnTest() {
let { target, panel } = await initPerformance(MARKERS_URL);
let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
// Hijack the markers massaging part of creating the waterfall view,
// to prevent collapsing markers and allowing this test to verify
// everything individually. A better solution would be to just expand
// all markers first and then skip the meta nodes, but I'm lazy.
WaterfallView._prepareWaterfallTree = markers => {
return { submarkers: markers };
};
const MARKER_TYPES = [
"Styles", "Reflow", "ConsoleTime", "TimeStamp"
];
await startRecording(panel);
ok(true, "Recording has started.");
await waitUntil(() => {
// Wait until we get all the different markers.
let markers = PerformanceController.getCurrentRecording().getMarkers();
return MARKER_TYPES.every(type => markers.some(m => m.name === type));
});
await stopRecording(panel);
ok(true, "Recording has ended.");
info("No need to select everything in the timeline.");
info("All the markers should be displayed by default.");
let bars = Array.prototype.filter.call($$(".waterfall-marker-bar"),
(bar) => MARKER_TYPES.includes(bar.getAttribute("type")));
let markers = PerformanceController.getCurrentRecording().getMarkers()
.filter(m => MARKER_TYPES.includes(m.name));
info(`Got ${bars.length} bars and ${markers.length} markers.`);
info("Markers types from datasrc: " + Array.from(markers, e => e.name));
info("Markers names from sidebar: " + Array.from(bars, e => e.parentNode.parentNode.querySelector(".waterfall-marker-name").getAttribute("value")));
ok(bars.length >= MARKER_TYPES.length, `Got at least ${MARKER_TYPES.length} markers (1)`);
ok(markers.length >= MARKER_TYPES.length, `Got at least ${MARKER_TYPES.length} markers (2)`);
// Sanity check that markers are in chronologically ascending order
markers.reduce((previous, m) => {
if (m.start <= previous) {
ok(false, "Markers are not in order");
info(markers);
}
return m.start;
}, 0);
// Override the timestamp marker's stack with our own recursive stack, which
// can happen for unknown reasons (bug 1246555); we should not cause a crash
// when attempting to render a recursive stack trace
let timestampMarker = markers.find(m => m.name === "ConsoleTime");
ok(typeof timestampMarker.stack === "number", "ConsoleTime marker has a stack before overwriting.");
let frames = PerformanceController.getCurrentRecording().getFrames();
let frameIndex = timestampMarker.stack = frames.length;
frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 1});
frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 2 });
frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex });
const tests = {
ConsoleTime: function (marker) {
info("Got `ConsoleTime` marker with data: " + JSON.stringify(marker));
ok(marker.stack === frameIndex, "Should have the ConsoleTime marker with recursive stack");
shouldHaveStack($, "startStack", marker);
shouldHaveStack($, "endStack", marker);
shouldHaveLabel($, "Timer Name:", "!!!", marker);
return true;
},
TimeStamp: function (marker) {
info("Got `TimeStamp` marker with data: " + JSON.stringify(marker));
shouldHaveLabel($, "Label:", "go", marker);
shouldHaveStack($, "stack", marker);
return true;
},
Styles: function (marker) {
info("Got `Styles` marker with data: " + JSON.stringify(marker));
if (marker.stack) {
shouldHaveStack($, "stack", marker);
return true;
}
},
Reflow: function (marker) {
info("Got `Reflow` marker with data: " + JSON.stringify(marker));
if (marker.stack) {
shouldHaveStack($, "stack", marker);
return true;
}
}
};
// Keep track of all marker tests that are finished so we only
// run through each marker test once, so we don't spam 500 redundant
// tests.
let testsDone = [];
for (let i = 0; i < bars.length; i++) {
let bar = bars[i];
let m = markers[i];
EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
if (tests[m.name]) {
if (!testsDone.includes(m.name)) {
let fullTestComplete = tests[m.name](m);
if (fullTestComplete) {
testsDone.push(m.name);
}
}
} else {
throw new Error(`No tests for ${m.name} -- should be filtered out.`);
}
if (testsDone.length === Object.keys(tests).length) {
break;
}
}
await teardown(panel);
finish();
}
function shouldHaveStack($, type, marker) {
ok($(`#waterfall-details .marker-details-stack[type=${type}]`), `${marker.name} has a stack: ${type}`);
}
function shouldHaveLabel($, name, value, marker) {
let $name = $(`#waterfall-details .marker-details-labelcontainer .marker-details-labelname[value="${name}"]`);
let $value = $name.parentNode.querySelector(".marker-details-labelvalue");
is($value.getAttribute("value"), value, `${marker.name} has correct label for ${name}:${value}`);
}
/* eslint-enable */

View File

@ -1,40 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that toggling preferences before there are any recordings does not throw.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { DetailsView, JsCallTreeView } = panel.panelWin;
await DetailsView.selectView("js-calltree");
// Manually call the _onPrefChanged function so we can catch an error.
try {
JsCallTreeView._onPrefChanged(null, "invert-call-tree", true);
ok(
true,
"Toggling preferences before there are any recordings should not fail."
);
} catch (e) {
ok(
false,
"Toggling preferences before there are any recordings should not fail."
);
}
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,44 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that toggling preferences during a recording does not throw.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { DetailsView, JsCallTreeView } = panel.panelWin;
await DetailsView.selectView("js-calltree");
await startRecording(panel);
// Manually call the _onPrefChanged function so we can catch an error.
try {
JsCallTreeView._onPrefChanged(null, "invert-call-tree", true);
ok(true, "Toggling preferences during a recording should not fail.");
} catch (e) {
ok(false, "Toggling preferences during a recording should not fail.");
}
await stopRecording(panel, {
expectedViewClass: "JsCallTreeView",
expectedViewEvent: "UI_JS_CALL_TREE_RENDERED",
});
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,51 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that toggling meta option prefs change visibility of other options.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_EXPERIMENTAL_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
add_task(async function() {
Services.prefs.setBoolPref(UI_EXPERIMENTAL_PREF, false);
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { $ } = panel.panelWin;
const $body = $(".theme-body");
const $menu = $("#performance-options-menupopup");
ok(
!$body.classList.contains("experimental-enabled"),
"The body node does not have `experimental-enabled` on start."
);
ok(
!$menu.classList.contains("experimental-enabled"),
"The menu popup does not have `experimental-enabled` on start."
);
Services.prefs.setBoolPref(UI_EXPERIMENTAL_PREF, true);
ok(
$body.classList.contains("experimental-enabled"),
"The body node has `experimental-enabled` after toggle."
);
ok(
$menu.classList.contains("experimental-enabled"),
"The menu popup has `experimental-enabled` after toggle."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,77 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that `enable-framerate` toggles the visibility of the fps graph,
* as well as enabling ticks data on the PerformanceFront.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_FRAMERATE_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
isVisible,
} = require("devtools/client/performance/test/helpers/dom-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { $, PerformanceController } = panel.panelWin;
// Disable framerate to test.
Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
await startRecording(panel);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withTicks,
false,
"PerformanceFront started without ticks recording."
);
ok(
!isVisible($("#time-framerate")),
"The fps graph is hidden when ticks disabled."
);
// Re-enable framerate.
Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, true);
is(
PerformanceController.getCurrentRecording().getConfiguration().withTicks,
false,
"PerformanceFront still marked without ticks recording."
);
ok(
!isVisible($("#time-framerate")),
"The fps graph is still hidden if recording does not contain ticks."
);
await startRecording(panel);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withTicks,
true,
"PerformanceFront started with ticks recording."
);
ok(
isVisible($("#time-framerate")),
"The fps graph is not hidden when ticks enabled before recording."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,57 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that toggling `enable-memory` during a recording doesn't change that
* recording's state and does not break.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_FRAMERATE_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { PerformanceController } = panel.panelWin;
// Test starting without framerate, and stopping with it.
Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
await startRecording(panel);
Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, true);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withTicks,
false,
"The recording finished without tracking framerate."
);
// Test starting with framerate, and stopping without it.
await startRecording(panel);
Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withTicks,
true,
"The recording finished with tracking framerate."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,96 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that `enable-memory` toggles the visibility of the memory graph,
* as well as enabling memory data on the PerformanceFront.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_MEMORY_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
isVisible,
} = require("devtools/client/performance/test/helpers/dom-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { $, PerformanceController } = panel.panelWin;
// Disable memory to test.
Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
await startRecording(panel);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withMemory,
false,
"PerformanceFront started without memory recording."
);
is(
PerformanceController.getCurrentRecording().getConfiguration()
.withAllocations,
false,
"PerformanceFront started without allocations recording."
);
ok(
!isVisible($("#memory-overview")),
"The memory graph is hidden when memory disabled."
);
// Re-enable memory.
Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
is(
PerformanceController.getCurrentRecording().getConfiguration().withMemory,
false,
"PerformanceFront still marked without memory recording."
);
is(
PerformanceController.getCurrentRecording().getConfiguration()
.withAllocations,
false,
"PerformanceFront still marked without allocations recording."
);
ok(
!isVisible($("#memory-overview")),
"memory graph is still hidden after enabling " +
"if recording did not start recording memory"
);
await startRecording(panel);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withMemory,
true,
"PerformanceFront started with memory recording."
);
is(
PerformanceController.getCurrentRecording().getConfiguration()
.withAllocations,
false,
"PerformanceFront did not record with allocations."
);
ok(
isVisible($("#memory-overview")),
"The memory graph is not hidden when memory enabled before recording."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,69 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that toggling `enable-memory` during a recording doesn't change that
* recording's state and does not break.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_MEMORY_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { PerformanceController } = panel.panelWin;
// Test starting without memory, and stopping with it.
Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
await startRecording(panel);
Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withMemory,
false,
"The recording finished without tracking memory."
);
is(
PerformanceController.getCurrentRecording().getConfiguration()
.withAllocations,
false,
"The recording finished without tracking allocations."
);
// Test starting with memory, and stopping without it.
await startRecording(panel);
Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
await stopRecording(panel);
is(
PerformanceController.getCurrentRecording().getConfiguration().withMemory,
true,
"The recording finished with tracking memory."
);
is(
PerformanceController.getCurrentRecording().getConfiguration()
.withAllocations,
false,
"The recording still is not recording allocations."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,101 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the js flamegraphs get rerendered when toggling `flatten-tree-recursion`.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_FLATTEN_RECURSION_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
EVENTS,
PerformanceController,
DetailsView,
JsFlameGraphView,
} = panel.panelWin;
const {
FlameGraphUtils,
} = require("devtools/client/shared/widgets/FlameGraph");
Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
await startRecording(panel);
await stopRecording(panel);
let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
await DetailsView.selectView("js-flamegraph");
await rendered;
const thread1 = PerformanceController.getCurrentRecording().getProfile()
.threads[0];
const rendering1 = FlameGraphUtils._cache.get(thread1);
ok(thread1, "The samples were retrieved from the controller.");
ok(rendering1, "The rendering data was cached.");
rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, false);
await rendered;
ok(true, "JsFlameGraphView rerendered when toggling flatten-tree-recursion.");
const thread2 = PerformanceController.getCurrentRecording().getProfile()
.threads[0];
const rendering2 = FlameGraphUtils._cache.get(thread2);
is(
thread1,
thread2,
"The same samples data should be retrieved from the controller (1)."
);
isnot(
rendering1,
rendering2,
"The rendering data should be different because other options were used (1)."
);
rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
await rendered;
ok(
true,
"JsFlameGraphView rerendered when toggling back flatten-tree-recursion."
);
const thread3 = PerformanceController.getCurrentRecording().getProfile()
.threads[0];
const rendering3 = FlameGraphUtils._cache.get(thread3);
is(
thread2,
thread3,
"The same samples data should be retrieved from the controller (2)."
);
isnot(
rendering2,
rendering3,
"The rendering data should be different because other options were used (2)."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,124 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the memory flamegraphs get rerendered when toggling
* `flatten-tree-recursion`.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_FLATTEN_RECURSION_PREF,
UI_ENABLE_ALLOCATIONS_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const {
EVENTS,
PerformanceController,
DetailsView,
MemoryFlameGraphView,
} = panel.panelWin;
const {
FlameGraphUtils,
} = require("devtools/client/shared/widgets/FlameGraph");
const RecordingUtils = require("devtools/shared/performance/recording-utils");
// Enable memory to test
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
await startRecording(panel);
await stopRecording(panel);
let rendered = once(
MemoryFlameGraphView,
EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED
);
await DetailsView.selectView("memory-flamegraph");
await rendered;
const allocations1 = PerformanceController.getCurrentRecording().getAllocations();
const thread1 = RecordingUtils.getProfileThreadFromAllocations(allocations1);
const rendering1 = FlameGraphUtils._cache.get(thread1);
ok(allocations1, "The allocations were retrieved from the controller.");
ok(thread1, "The allocations profile was synthesized by the utility funcs.");
ok(rendering1, "The rendering data was cached.");
rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, false);
await rendered;
ok(
true,
"MemoryFlameGraphView rerendered when toggling flatten-tree-recursion."
);
const allocations2 = PerformanceController.getCurrentRecording().getAllocations();
const thread2 = RecordingUtils.getProfileThreadFromAllocations(allocations2);
const rendering2 = FlameGraphUtils._cache.get(thread2);
is(
allocations1,
allocations2,
"The same allocations data should be retrieved from the controller (1)."
);
is(
thread1,
thread2,
"The same allocations profile should be retrieved from the utility funcs. (1)."
);
isnot(
rendering1,
rendering2,
"The rendering data should be different because other options were used (1)."
);
rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
await rendered;
ok(
true,
"MemoryFlameGraphView rerendered when toggling back flatten-tree-recursion."
);
const allocations3 = PerformanceController.getCurrentRecording().getAllocations();
const thread3 = RecordingUtils.getProfileThreadFromAllocations(allocations3);
const rendering3 = FlameGraphUtils._cache.get(thread3);
is(
allocations2,
allocations3,
"The same allocations data should be retrieved from the controller (2)."
);
is(
thread2,
thread3,
"The same allocations profile should be retrieved from the utility funcs. (2)."
);
isnot(
rendering2,
rendering3,
"The rendering data should be different because other options were used (2)."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,53 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the js call tree views get rerendered when toggling `invert-call-tree`.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_INVERT_CALL_TREE_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
await startRecording(panel);
await stopRecording(panel);
let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
await DetailsView.selectView("js-calltree");
await rendered;
rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
await rendered;
ok(true, "JsCallTreeView rerendered when toggling invert-call-tree.");
rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
await rendered;
ok(true, "JsCallTreeView rerendered when toggling back invert-call-tree.");
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,59 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the memory call tree views get rerendered when toggling `invert-call-tree`.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_ALLOCATIONS_PREF,
UI_INVERT_CALL_TREE_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
// Enable allocations to test.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
await startRecording(panel);
await stopRecording(panel);
let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
await DetailsView.selectView("memory-calltree");
await rendered;
rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
await rendered;
ok(true, "MemoryCallTreeView rerendered when toggling invert-call-tree.");
rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
await rendered;
ok(
true,
"MemoryCallTreeView rerendered when toggling back invert-call-tree."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,53 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the js flamegraphs views get rerendered when toggling `invert-flame-graph`.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_INVERT_FLAME_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
await startRecording(panel);
await stopRecording(panel);
let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
await DetailsView.selectView("js-flamegraph");
await rendered;
rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
await rendered;
ok(true, "JsFlameGraphView rerendered when toggling invert-call-tree.");
rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
await rendered;
ok(true, "JsFlameGraphView rerendered when toggling back invert-call-tree.");
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,63 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that the memory flamegraphs views get rerendered when toggling
* `invert-flame-graph`.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
UI_ENABLE_ALLOCATIONS_PREF,
UI_INVERT_FLAME_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
const {
once,
} = require("devtools/client/performance/test/helpers/event-utils");
add_task(async function() {
const { panel } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
const { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
// Enable allocations to test.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
await startRecording(panel);
await stopRecording(panel);
let rendered = once(
MemoryFlameGraphView,
EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED
);
await DetailsView.selectView("memory-flamegraph");
await rendered;
rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
await rendered;
ok(true, "MemoryFlameGraphView rerendered when toggling invert-call-tree.");
rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
await rendered;
ok(
true,
"MemoryFlameGraphView rerendered when toggling back invert-call-tree."
);
await teardownToolboxAndRemoveTab(panel);
});

View File

@ -1,56 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that setting the `devtools.performance.memory.` prefs propagate to
* the memory actor.
*/
const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
const {
MEMORY_SAMPLE_PROB_PREF,
MEMORY_MAX_LOG_LEN_PREF,
UI_ENABLE_ALLOCATIONS_PREF,
} = require("devtools/client/performance/test/helpers/prefs");
const {
initPerformanceInNewTab,
teardownToolboxAndRemoveTab,
} = require("devtools/client/performance/test/helpers/panel-utils");
const {
startRecording,
stopRecording,
} = require("devtools/client/performance/test/helpers/actions");
add_task(async function() {
const { panel, toolbox } = await initPerformanceInNewTab({
url: SIMPLE_URL,
win: window,
});
// Enable allocations to test.
Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
Services.prefs.setCharPref(MEMORY_SAMPLE_PROB_PREF, "0.213");
Services.prefs.setIntPref(MEMORY_MAX_LOG_LEN_PREF, 777777);
await startRecording(panel);
const performanceFront = await toolbox.target.getFront("performance");
const {
probability,
maxLogLength,
} = await performanceFront.getConfiguration();
await stopRecording(panel);
is(
probability,
0.213,
"The allocations probability option is set on memory actor."
);
is(
maxLogLength,
777777,
"The allocations max log length option is set on memory actor."
);
await teardownToolboxAndRemoveTab(panel);
});

Some files were not shown because too many files have changed in this diff Show More