merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2015-01-14 14:25:18 +01:00
commit 9a34b72c5f
76 changed files with 1984 additions and 513 deletions

View File

@ -14,6 +14,7 @@ Aaron Nowack <anowack@mimiru.net>
Aaron Reed <aaronr@us.ibm.com>
Aaron Spangler <aaron@spangler.ods.org>
Aaron Train <aaron.train@gmail.com>
Abdelrhman Ahmed <a.ahmed1026@gmail.com>
Achim Hasenmueller <achimha@innotek.de>
ActiveState Tool Corp.
Adam Barth <hk9565@gmail.com>

View File

@ -26,13 +26,6 @@ pref("app.update.url.details", "https://www.mozilla.org/firefox/aurora/");
// app.update.checkInstallTime is true.
pref("app.update.checkInstallTime.days", 2);
// code usage depends on contracts, please contact the Firefox module owner if you have questions
pref("browser.search.param.yahoo-fr", "moz35");
pref("browser.search.param.yahoo-fr-ja", "mozff");
#ifdef MOZ_METRO
pref("browser.search.param.yahoo-fr-metro", "");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 5);

View File

@ -24,13 +24,6 @@ pref("app.update.url.details", "https://nightly.mozilla.org");
// app.update.checkInstallTime is true.
pref("app.update.checkInstallTime.days", 2);
// code usage depends on contracts, please contact the Firefox module owner if you have questions
pref("browser.search.param.yahoo-fr", "moz35");
pref("browser.search.param.yahoo-fr-ja", "mozff");
#ifdef MOZ_METRO
pref("browser.search.param.yahoo-fr-metro", "");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 5);

View File

@ -23,14 +23,6 @@ pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/notes")
// app.update.checkInstallTime is true.
pref("app.update.checkInstallTime.days", 63);
// code usage depends on contracts, please contact the Firefox module owner if you have questions
pref("browser.search.param.yahoo-fr", "moz35");
pref("browser.search.param.yahoo-fr-ja", "mozff");
#ifdef MOZ_METRO
pref("browser.search.param.ms-pc-metro", "MOZW");
pref("browser.search.param.yahoo-fr-metro", "mozilla_metro_search");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 0);

View File

@ -23,13 +23,6 @@ pref("app.update.url.details", "https://nightly.mozilla.org");
// app.update.checkInstallTime is true.
pref("app.update.checkInstallTime.days", 2);
// code usage depends on contracts, please contact the Firefox module owner if you have questions
pref("browser.search.param.yahoo-fr", "moz35");
pref("browser.search.param.yahoo-fr-ja", "mozff");
#ifdef MOZ_METRO
pref("browser.search.param.yahoo-fr-metro", "");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 0);

View File

@ -3969,8 +3969,12 @@ OverflowableToolbar.prototype = {
},
onOverflow: function(aEvent) {
// The rangeParent check is here because of bug 1111986 and ensuring that
// overflow events from the bookmarks toolbar items or similar things that
// manage their own overflow don't trigger an overflow on the entire toolbar
if (!this._enabled ||
(aEvent && aEvent.target != this._toolbar.customizationTarget))
(aEvent && aEvent.target != this._toolbar.customizationTarget) ||
(aEvent && aEvent.rangeParent))
return;
let child = this._target.lastChild;

View File

@ -85,6 +85,8 @@ browser.jar:
content/browser/devtools/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js)
content/browser/devtools/webaudioeditor/views/context.js (webaudioeditor/views/context.js)
content/browser/devtools/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js)
content/browser/devtools/webaudioeditor/views/properties.js (webaudioeditor/views/properties.js)
content/browser/devtools/webaudioeditor/views/automation.js (webaudioeditor/views/automation.js)
content/browser/devtools/profiler.xul (profiler/profiler.xul)
content/browser/devtools/profiler.js (profiler/profiler.js)
content/browser/devtools/ui-recordings.js (profiler/ui-recordings.js)
@ -98,6 +100,7 @@ browser.jar:
content/browser/devtools/performance/views/details-call-tree.js (performance/views/details-call-tree.js)
content/browser/devtools/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
content/browser/devtools/performance/views/details-flamegraph.js (performance/views/details-flamegraph.js)
content/browser/devtools/performance/views/recordings.js (performance/views/recordings.js)
#endif
content/browser/devtools/responsivedesign/resize-commands.js (responsivedesign/resize-commands.js)
content/browser/devtools/commandline.css (commandline/commandline.css)

View File

@ -0,0 +1,248 @@
/* 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 { PerformanceIO } = require("devtools/performance/io");
const RECORDING_IN_PROGRESS = exports.RECORDING_IN_PROGRESS = -1;
const RECORDING_UNAVAILABLE = exports.RECORDING_UNAVAILABLE = null;
/**
* Model for a wholistic profile, containing start/stop times, profiling data, frames data,
* timeline (marker, tick, memory) data, and methods to start/stop recording.
*/
const RecordingModel = function (options={}) {
this._front = options.front;
this._performance = options.performance;
this._label = options.label || "";
};
RecordingModel.prototype = {
_localStartTime: RECORDING_UNAVAILABLE,
_startTime: RECORDING_UNAVAILABLE,
_endTime: RECORDING_UNAVAILABLE,
_markers: [],
_frames: [],
_ticks: [],
_memory: [],
_profilerData: {},
_label: "",
_imported: false,
_isRecording: false,
/**
* Loads a recording from a file.
*
* @param nsILocalFile file
* The file to import the data form.
*/
importRecording: Task.async(function *(file) {
let recordingData = yield PerformanceIO.loadRecordingFromFile(file);
this._imported = true;
this._label = recordingData.profilerData.profilerLabel || "";
this._startTime = recordingData.interval.startTime;
this._endTime = recordingData.interval.endTime;
this._markers = recordingData.markers;
this._frames = recordingData.frames;
this._memory = recordingData.memory;
this._ticks = recordingData.ticks;
this._profilerData = recordingData.profilerData;
return recordingData;
}),
/**
* Saves the current recording to a file.
*
* @param nsILocalFile file
* The file to stream the data into.
*/
exportRecording: Task.async(function *(file) {
let recordingData = this.getAllData();
yield PerformanceIO.saveRecordingToFile(recordingData, file);
}),
/**
* Starts recording with the PerformanceFront, storing the start times
* on the model.
*/
startRecording: Task.async(function *() {
// Times must come from the actor in order to be self-consistent.
// However, we also want to update the view with the elapsed time
// even when the actor is not generating data. To do this we get
// the local time and use it to compute a reasonable elapsed time.
this._localStartTime = this._performance.now();
let { startTime } = yield this._front.startRecording({
withTicks: true,
withMemory: true
});
this._isRecording = true;
this._startTime = startTime;
this._endTime = RECORDING_IN_PROGRESS;
this._markers = [];
this._frames = [];
this._memory = [];
this._ticks = [];
}),
/**
* Stops recording with the PerformanceFront, storing the end times
* on the model.
*/
stopRecording: Task.async(function *() {
let results = yield this._front.stopRecording();
this._isRecording = false;
// If `endTime` is not yielded from timeline actor (< Fx36), fake it.
if (!results.endTime) {
results.endTime = this._startTime + this.getLocalElapsedTime();
}
this._endTime = results.endTime;
this._profilerData = results.profilerData;
this._markers = this._markers.sort((a,b) => (a.start > b.start));
return results;
}),
/**
* Returns the profile's label, from `console.profile(LABEL)`.
*/
getLabel: function () {
return this._label;
},
/**
* Gets the amount of time elapsed locally after starting a recording.
*/
getLocalElapsedTime: function () {
return this._performance.now() - this._localStartTime;
},
/**
* Returns duration of this recording, in milliseconds.
*/
getDuration: function () {
let { startTime, endTime } = this.getInterval();
return endTime - startTime;
},
/**
* Gets the time interval for the current recording.
* @return object
*/
getInterval: function() {
let startTime = this._startTime;
let endTime = this._endTime;
// Compute an approximate ending time for the current recording. This is
// needed to ensure that the view updates even when new data is
// not being generated.
if (endTime == RECORDING_IN_PROGRESS) {
endTime = startTime + this.getLocalElapsedTime();
}
return { startTime, endTime };
},
/**
* Gets the accumulated markers in the current recording.
* @return array
*/
getMarkers: function() {
return this._markers;
},
/**
* Gets the accumulated stack frames in the current recording.
* @return array
*/
getFrames: function() {
return this._frames;
},
/**
* Gets the accumulated memory measurements in this recording.
* @return array
*/
getMemory: function() {
return this._memory;
},
/**
* Gets the accumulated refresh driver ticks in this recording.
* @return array
*/
getTicks: function() {
return this._ticks;
},
/**
* Gets the profiler data in this recording.
* @return array
*/
getProfilerData: function() {
return this._profilerData;
},
/**
* Gets all the data in this recording.
*/
getAllData: function() {
let interval = this.getInterval();
let markers = this.getMarkers();
let frames = this.getFrames();
let memory = this.getMemory();
let ticks = this.getTicks();
let profilerData = this.getProfilerData();
return { interval, markers, frames, memory, ticks, profilerData };
},
/**
* Returns a boolean indicating whether or not this recording model
* is recording.
*/
isRecording: function () {
return this._isRecording;
},
/**
* Fired whenever the PerformanceFront emits markers, memory or ticks.
*/
addTimelineData: function (eventName, ...data) {
// If this model isn't currently recording,
// ignore the timeline data.
if (!this.isRecording()) {
return;
}
switch (eventName) {
// Accumulate markers into an array.
case "markers":
let [markers] = data;
Array.prototype.push.apply(this._markers, markers);
break;
// Accumulate stack frames into an array.
case "frames":
let [, frames] = data;
Array.prototype.push.apply(this._frames, frames);
break;
// Accumulate memory measurements into an array.
case "memory":
let [delta, measurement] = data;
this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
break;
// Save the accumulated refresh driver ticks.
case "ticks":
let [, timestamps] = data;
this._ticks = timestamps;
break;
}
}
};
exports.RecordingModel = RecordingModel;

View File

@ -6,6 +6,7 @@
EXTRA_JS_MODULES.devtools.performance += [
'modules/front.js',
'modules/io.js',
'modules/recording-model.js',
'panel.js'
]

View File

@ -41,6 +41,11 @@ devtools.lazyImporter(this, "CanvasGraphUtils",
"resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "LineGraphWidget",
"resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "SideMenuWidget",
"resource:///modules/devtools/SideMenuWidget.jsm");
const { RecordingModel, RECORDING_IN_PROGRESS, RECORDING_UNAVAILABLE } =
devtools.require("devtools/performance/recording-model");
devtools.lazyImporter(this, "FlameGraphUtils",
"resource:///modules/devtools/FlameGraph.jsm");
@ -49,12 +54,17 @@ devtools.lazyImporter(this, "FlameGraph",
// Events emitted by various objects in the panel.
const EVENTS = {
// Emitted by the PerformanceController or RecordingView
// when a recording model is selected
RECORDING_SELECTED: "Performance:RecordingSelected",
// 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 or export button click
// Emitted by the PerformanceView on import button click
UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
// Emitted by the RecordingsView on export button click
UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
// When a recording is started or stopped via the PerformanceController
@ -96,11 +106,6 @@ const EVENTS = {
FLAMEGRAPH_RENDERED: "Performance:UI:FlameGraphRendered"
};
// Constant defining the end time for a recording that hasn't finished
// or is not yet available.
const RECORDING_IN_PROGRESS = -1;
const RECORDING_UNAVAILABLE = null;
/**
* The current target and the profiler connection, set by this tool's host.
*/
@ -150,18 +155,8 @@ let PrefObserver = {
* UI interaction.
*/
let PerformanceController = {
/**
* Permanent storage for the markers and the memory measurements streamed by
* the backend, along with the start and end timestamps.
*/
_localStartTime: RECORDING_UNAVAILABLE,
_startTime: RECORDING_UNAVAILABLE,
_endTime: RECORDING_UNAVAILABLE,
_markers: [],
_frames: [],
_memory: [],
_ticks: [],
_profilerData: {},
_recordings: [],
_currentRecording: null,
/**
* Listen for events emitted by the current tab target and
@ -173,11 +168,13 @@ let PerformanceController = {
this.importRecording = this.importRecording.bind(this);
this.exportRecording = this.exportRecording.bind(this);
this._onTimelineData = this._onTimelineData.bind(this);
this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
gFront.on("ticks", this._onTimelineData); // framerate
gFront.on("markers", this._onTimelineData); // timeline markers
@ -191,8 +188,9 @@ let PerformanceController = {
destroy: function() {
PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
PerformanceView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
gFront.off("ticks", this._onTimelineData);
gFront.off("markers", this._onTimelineData);
@ -205,25 +203,11 @@ let PerformanceController = {
* when the front has started to record.
*/
startRecording: Task.async(function *() {
// Times must come from the actor in order to be self-consistent.
// However, we also want to update the view with the elapsed time
// even when the actor is not generating data. To do this we get
// the local time and use it to compute a reasonable elapsed time.
this._localStartTime = performance.now();
let model = this.createNewRecording();
this.setCurrentRecording(model);
yield model.startRecording();
let { startTime } = yield gFront.startRecording({
withTicks: true,
withMemory: true
});
this._startTime = startTime;
this._endTime = RECORDING_IN_PROGRESS;
this._markers = [];
this._frames = [];
this._memory = [];
this._ticks = [];
this.emit(EVENTS.RECORDING_STARTED);
this.emit(EVENTS.RECORDING_STARTED, model);
}),
/**
@ -231,63 +215,76 @@ let PerformanceController = {
* when the front has stopped recording.
*/
stopRecording: Task.async(function *() {
let results = yield gFront.stopRecording();
let recording = this._getLatest();
yield recording.stopRecording();
// If `endTime` is not yielded from timeline actor (< Fx36), fake it.
if (!results.endTime) {
results.endTime = this._startTime + this.getLocalElapsedTime();
}
this._endTime = results.endTime;
this._profilerData = results.profilerData;
this._markers = this._markers.sort((a,b) => (a.start > b.start));
this.emit(EVENTS.RECORDING_STOPPED);
this.emit(EVENTS.RECORDING_STOPPED, recording);
}),
/**
* Saves the current recording to a file.
*
* @param RecordingModel recording
* The model that holds the recording data.
* @param nsILocalFile file
* The file to stream the data into.
*/
exportRecording: Task.async(function*(_, file) {
let recordingData = this.getAllData();
exportRecording: Task.async(function*(_, recording, file) {
let recordingData = recording.getAllData();
yield PerformanceIO.saveRecordingToFile(recordingData, file);
this.emit(EVENTS.RECORDING_EXPORTED, recordingData);
}),
/**
* Loads a recording from a file, replacing the current one.
* XXX: Handle multiple recordings, bug 1111004.
* Loads a recording from a file, adding it to the recordings list.
*
* @param nsILocalFile file
* The file to import the data from.
*/
importRecording: Task.async(function*(_, file) {
let recordingData = yield PerformanceIO.loadRecordingFromFile(file);
let model = this.createNewRecording();
yield model.importRecording(file);
this._startTime = recordingData.interval.startTime;
this._endTime = recordingData.interval.endTime;
this._markers = recordingData.markers;
this._frames = recordingData.frames;
this._memory = recordingData.memory;
this._ticks = recordingData.ticks;
this._profilerData = recordingData.profilerData;
this.emit(EVENTS.RECORDING_IMPORTED, recordingData);
// Flush the current recording.
this.emit(EVENTS.RECORDING_STARTED);
this.emit(EVENTS.RECORDING_STOPPED);
this.emit(EVENTS.RECORDING_IMPORTED, model.getAllData(), model);
}),
/**
* Creates a new RecordingModel, fires events and stores it
* internally in the controller.
*/
createNewRecording: function () {
let model = new RecordingModel({
front: gFront,
performance: performance
});
this._recordings.push(model);
this.emit(EVENTS.RECORDING_CREATED, model);
return model;
},
/**
* Sets the active RecordingModel to `recording`.
*/
setCurrentRecording: function (recording) {
if (this._currentRecording !== recording) {
this._currentRecording = recording;
this.emit(EVENTS.RECORDING_SELECTED, recording);
}
},
/**
* Return the current active RecordingModel.
*/
getCurrentRecording: function () {
return this._currentRecording;
},
/**
* Gets the amount of time elapsed locally after starting a recording.
*/
getLocalElapsedTime: function() {
return performance.now() - this._localStartTime;
getLocalElapsedTime: function () {
return this.getCurrentRecording().getLocalElapsedTime;
},
/**
@ -295,17 +292,7 @@ let PerformanceController = {
* @return object
*/
getInterval: function() {
let startTime = this._startTime;
let endTime = this._endTime;
// Compute an approximate ending time for the current recording. This is
// needed to ensure that the view updates even when new data is
// not being generated.
if (endTime == RECORDING_IN_PROGRESS) {
endTime = startTime + this.getLocalElapsedTime();
}
return { startTime, endTime };
return this.getCurrentRecording().getInterval();
},
/**
@ -313,7 +300,7 @@ let PerformanceController = {
* @return array
*/
getMarkers: function() {
return this._markers;
return this.getCurrentRecording().getMarkers();
},
/**
@ -321,7 +308,7 @@ let PerformanceController = {
* @return array
*/
getFrames: function() {
return this._frames;
return this.getCurrentRecording().getFrames();
},
/**
@ -329,7 +316,7 @@ let PerformanceController = {
* @return array
*/
getMemory: function() {
return this._memory;
return this.getCurrentRecording().getMemory();
},
/**
@ -337,7 +324,7 @@ let PerformanceController = {
* @return array
*/
getTicks: function() {
return this._ticks;
return this.getCurrentRecording().getTicks();
},
/**
@ -345,48 +332,41 @@ let PerformanceController = {
* @return array
*/
getProfilerData: function() {
return this._profilerData;
return this.getCurrentRecording().getProfilerData();
},
/**
* Gets all the data in this recording.
*/
getAllData: function() {
let interval = this.getInterval();
let markers = this.getMarkers();
let frames = this.getFrames();
let memory = this.getMemory();
let ticks = this.getTicks();
let profilerData = this.getProfilerData();
return { interval, markers, frames, memory, ticks, profilerData };
return this.getCurrentRecording().getAllData();
},
/**
/**
* Get most recently added profile that was triggered manually (via UI)
*/
_getLatest: function () {
for (let i = this._recordings.length - 1; i >= 0; i--) {
return this._recordings[i];
}
return null;
},
/**
* Fired whenever the PerformanceFront emits markers, memory or ticks.
*/
_onTimelineData: function (eventName, ...data) {
// Accumulate markers into an array.
if (eventName == "markers") {
let [markers] = data;
Array.prototype.push.apply(this._markers, markers);
}
// Accumulate stack frames into an array.
else if (eventName == "frames") {
let [delta, frames] = data;
Array.prototype.push.apply(this._frames, frames);
}
// Accumulate memory measurements into an array.
else if (eventName == "memory") {
let [delta, measurement] = data;
this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
}
// Save the accumulated refresh driver ticks.
else if (eventName == "ticks") {
let [delta, timestamps] = data;
this._ticks = timestamps;
}
_onTimelineData: function (...data) {
this._recordings.forEach(profile => profile.addTimelineData.apply(profile, data));
this.emit(EVENTS.TIMELINE_DATA, ...data);
},
this.emit(EVENTS.TIMELINE_DATA, eventName, ...data);
/**
* 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);
}
};

View File

@ -13,23 +13,21 @@ let PerformanceView = {
initialize: function () {
this._recordButton = $("#record-button");
this._importButton = $("#import-button");
this._exportButton = $("#export-button");
this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
this._onImportButtonClick = this._onImportButtonClick.bind(this);
this._onExportButtonClick = this._onExportButtonClick.bind(this);
this._lockRecordButton = this._lockRecordButton.bind(this);
this._unlockRecordButton = this._unlockRecordButton.bind(this);
this._recordButton.addEventListener("click", this._onRecordButtonClick);
this._importButton.addEventListener("click", this._onImportButtonClick);
this._exportButton.addEventListener("click", this._onExportButtonClick);
// Bind to controller events to unlock the record button
PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
return promise.all([
RecordingsView.initialize(),
OverviewView.initialize(),
DetailsView.initialize()
]);
@ -41,12 +39,12 @@ let PerformanceView = {
destroy: function () {
this._recordButton.removeEventListener("click", this._onRecordButtonClick);
this._importButton.removeEventListener("click", this._onImportButtonClick);
this._exportButton.removeEventListener("click", this._onExportButtonClick);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
return promise.all([
RecordingsView.destroy(),
OverviewView.destroy(),
DetailsView.destroy()
]);
@ -94,23 +92,6 @@ let PerformanceView = {
if (fp.show() == Ci.nsIFilePicker.returnOK) {
this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
}
},
/**
* Handler for clicking the export button.
*/
_onExportButtonClick: function(e) {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
fp.defaultString = "profile.json";
fp.open({ done: result => {
if (result != Ci.nsIFilePicker.returnCancel) {
this.emit(EVENTS.UI_EXPORT_RECORDING, fp.file);
}
}});
}
};

View File

@ -16,93 +16,96 @@
<script src="chrome://browser/content/devtools/theme-switching.js"/>
<script type="application/javascript" src="performance/performance-controller.js"/>
<script type="application/javascript" src="performance/performance-view.js"/>
<script type="application/javascript" src="performance/recording-model.js"/>
<script type="application/javascript" src="performance/views/overview.js"/>
<script type="application/javascript" src="performance/views/details.js"/>
<script type="application/javascript" src="performance/views/details-call-tree.js"/>
<script type="application/javascript" src="performance/views/details-waterfall.js"/>
<script type="application/javascript" src="performance/views/details-flamegraph.js"/>
<script type="application/javascript" src="performance/views/recordings.js"/>
<vbox class="theme-body" flex="1">
<toolbar id="performance-toolbar" class="devtools-toolbar">
<hbox id="performance-toolbar-controls-recordings" class="devtools-toolbarbutton-group">
<toolbarbutton id="record-button"
class="devtools-toolbarbutton"
tooltiptext="&profilerUI.recordButton.tooltip;"/>
</hbox>
<hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
<toolbarbutton id="select-waterfall-view"
class="devtools-toolbarbutton"
data-view="waterfall" />
<toolbarbutton id="select-calltree-view"
class="devtools-toolbarbutton"
data-view="calltree" />
<toolbarbutton id="select-flamegraph-view"
class="devtools-toolbarbutton"
data-view="flamegraph" />
</hbox>
<spacer flex="1"></spacer>
<hbox id="performance-toolbar-controls-storage" class="devtools-toolbarbutton-group">
<toolbarbutton id="import-button"
class="devtools-toolbarbutton"
label="&profilerUI.importButton;"/>
<toolbarbutton id="export-button"
class="devtools-toolbarbutton"
label="&profilerUI.exportButton;"/>
<toolbarbutton id="clear-button"
class="devtools-toolbarbutton"
label="&profilerUI.clearButton;"/>
</hbox>
</toolbar>
<vbox id="overview-pane">
<hbox id="markers-overview"/>
<hbox id="memory-overview"/>
<hbox id="time-framerate"/>
</vbox>
<deck id="details-pane" flex="1">
<hbox id="waterfall-view" flex="1">
<vbox id="waterfall-breakdown" flex="1" />
<splitter class="devtools-side-splitter"/>
<vbox id="waterfall-details"
class="theme-sidebar"
width="150"
height="150"/>
</hbox>
<vbox id="calltree-view" flex="1">
<hbox class="call-tree-headers-container">
<label class="plain call-tree-header"
type="duration"
crop="end"
value="&profilerUI.table.totalDuration;"/>
<label class="plain call-tree-header"
type="percentage"
crop="end"
value="&profilerUI.table.totalPercentage;"/>
<label class="plain call-tree-header"
type="self-duration"
crop="end"
value="&profilerUI.table.selfDuration;"/>
<label class="plain call-tree-header"
type="self-percentage"
crop="end"
value="&profilerUI.table.selfPercentage;"/>
<label class="plain call-tree-header"
type="samples"
crop="end"
value="&profilerUI.table.samples;"/>
<label class="plain call-tree-header"
type="function"
crop="end"
value="&profilerUI.table.function;"/>
<hbox class="theme-body" flex="1">
<vbox id="recordings-pane">
<toolbar id="recordings-toolbar"
class="devtools-toolbar">
<hbox id="recordings-controls"
class="devtools-toolbarbutton-group">
<toolbarbutton id="record-button"
class="devtools-toolbarbutton"
tooltiptext="&profilerUI.recordButton.tooltip;"/>
<toolbarbutton id="import-button"
class="devtools-toolbarbutton"
label="&profilerUI.importButton;"/>
<toolbarbutton id="clear-button"
class="devtools-toolbarbutton"
label="&profilerUI.clearButton;"/>
</hbox>
<vbox class="call-tree-cells-container" flex="1"/>
</vbox>
</toolbar>
<vbox id="recordings-list" flex="1"/>
</vbox>
<vbox flex="1">
<toolbar id="performance-toolbar" class="devtools-toolbar">
<hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
<toolbarbutton id="select-waterfall-view"
class="devtools-toolbarbutton"
data-view="waterfall" />
<toolbarbutton id="select-calltree-view"
class="devtools-toolbarbutton"
data-view="calltree" />
<toolbarbutton id="select-flamegraph-view"
class="devtools-toolbarbutton"
data-view="flamegraph" />
</hbox>
<spacer flex="1"></spacer>
</toolbar>
<hbox id="flamegraph-view" flex="1">
</hbox>
</deck>
</vbox>
<vbox id="overview-pane">
<hbox id="markers-overview"/>
<hbox id="memory-overview"/>
<hbox id="time-framerate"/>
</vbox>
<deck id="details-pane" flex="1">
<hbox id="waterfall-view" flex="1">
<vbox id="waterfall-breakdown" flex="1" />
<splitter class="devtools-side-splitter"/>
<vbox id="waterfall-details"
class="theme-sidebar"
width="150"
height="150"/>
</hbox>
<vbox id="calltree-view" flex="1">
<hbox class="call-tree-headers-container">
<label class="plain call-tree-header"
type="duration"
crop="end"
value="&profilerUI.table.totalDuration;"/>
<label class="plain call-tree-header"
type="percentage"
crop="end"
value="&profilerUI.table.totalPercentage;"/>
<label class="plain call-tree-header"
type="self-duration"
crop="end"
value="&profilerUI.table.selfDuration;"/>
<label class="plain call-tree-header"
type="self-percentage"
crop="end"
value="&profilerUI.table.selfPercentage;"/>
<label class="plain call-tree-header"
type="samples"
crop="end"
value="&profilerUI.table.samples;"/>
<label class="plain call-tree-header"
type="function"
crop="end"
value="&profilerUI.table.function;"/>
</hbox>
<vbox class="call-tree-cells-container" flex="1"/>
</vbox>
<hbox id="flamegraph-view" flex="1">
</hbox>
</deck>
</vbox>
</hbox>
</window>

View File

@ -39,4 +39,7 @@ support-files =
[browser_perf_recordings-io-01.js]
[browser_perf_recordings-io-02.js]
[browser_perf_recordings-io-03.js]
[browser_perf-recording-selected-01.js]
[browser_perf-recording-selected-02.js]
[browser_perf-recording-selected-03.js]
[browser_perf_recordings-io-04.js]

View File

@ -0,0 +1,35 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the profiler correctly handles multiple recordings and can
* successfully switch between them.
*/
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
yield startRecording(panel);
yield stopRecording(panel);
yield startRecording(panel);
yield stopRecording(panel);
is(RecordingsView.itemCount, 2,
"There should be two recordings visible.");
is(RecordingsView.selectedIndex, 1,
"The second recording item should be selected.");
let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
RecordingsView.selectedIndex = 0;
yield select;
is(RecordingsView.itemCount, 2,
"There should still be two recordings visible.");
is(RecordingsView.selectedIndex, 0,
"The first recording item should be selected.");
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the profiler correctly handles multiple recordings and can
* successfully switch between them, even when one of them is in progress.
*/
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
yield startRecording(panel);
yield stopRecording(panel);
yield startRecording(panel);
is(RecordingsView.itemCount, 2,
"There should be two recordings visible.");
is(RecordingsView.selectedIndex, 1,
"The new recording item should be selected.");
let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
RecordingsView.selectedIndex = 0;
yield select;
is(RecordingsView.itemCount, 2,
"There should still be two recordings visible.");
is(RecordingsView.selectedIndex, 0,
"The first recording item should be selected now.");
select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
RecordingsView.selectedIndex = 1;
yield select;
is(RecordingsView.itemCount, 2,
"There should still be two recordings visible.");
is(RecordingsView.selectedIndex, 1,
"The second recording item should be selected again.");
yield stopRecording(panel);
yield teardown(panel);
finish();
});

View File

@ -0,0 +1,33 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the profiler UI does not forget that recording is active when
* selected recording changes. Bug 1060885.
*/
let test = Task.async(function*() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { $, EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
yield startRecording(panel);
yield stopRecording(panel);
yield startRecording(panel);
info("Selecting recording #0 and waiting for it to be displayed.");
let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
RecordingsView.selectedIndex = 0;
yield select;
ok($("#record-button").hasAttribute("checked"),
"Button is still checked after selecting another item.");
ok(!$("#record-button").hasAttribute("locked"),
"Button is not locked after selecting another item.");
yield stopRecording(panel);
yield teardown(panel);
finish();
});

View File

@ -23,7 +23,7 @@ let test = Task.async(function*() {
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
yield PerformanceController.exportRecording("", file);
yield PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
yield exported;
ok(true, "The recording data appears to have been successfully saved.");

View File

@ -13,10 +13,12 @@ let CallTreeView = {
initialize: function () {
this._callTree = $(".call-tree-cells-container");
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingSelected = this._onRecordingSelected.bind(this);
this._onRangeChange = this._onRangeChange.bind(this);
this._onLink = this._onLink.bind(this);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
},
@ -26,6 +28,7 @@ let CallTreeView = {
*/
destroy: function () {
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
},
@ -51,6 +54,19 @@ let CallTreeView = {
this.render(profilerData);
},
/**
* Called when a recording has been selected.
*/
_onRecordingSelected: function (_, recording) {
// If not recording, then this recording is done and we can render all of it
// Otherwise, TODO in bug 1120699 will hide the details view altogether if
// this is still recording.
if (!recording.isRecording()) {
let profilerData = recording.getProfilerData();
this.render(profilerData);
}
},
/**
* Fired when a range is selected or cleared in the OverviewView.
*/

View File

@ -13,6 +13,7 @@ let WaterfallView = {
initialize: Task.async(function *() {
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingSelected = this._onRecordingSelected.bind(this);
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this);
@ -25,6 +26,7 @@ let WaterfallView = {
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
this.waterfall.recalculateBounds();
}),
@ -39,6 +41,7 @@ let WaterfallView = {
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
},
/**
@ -67,6 +70,15 @@ let WaterfallView = {
this.render();
},
/**
* Called when a recording is selected.
*/
_onRecordingSelected: function (_, recording) {
if (!recording.isRecording()) {
this.render();
}
},
/**
* Called when a marker is selected in the waterfall view,
* updating the markers detail view.

View File

@ -30,6 +30,7 @@ let OverviewView = {
initialize: Task.async(function *() {
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingSelected = this._onRecordingSelected.bind(this);
this._onRecordingTick = this._onRecordingTick.bind(this);
this._onGraphMouseUp = this._onGraphMouseUp.bind(this);
this._onGraphScroll = this._onGraphScroll.bind(this);
@ -47,6 +48,7 @@ let OverviewView = {
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
}),
/**
@ -63,6 +65,7 @@ let OverviewView = {
clearNamedTimeout("graph-scroll");
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
},
/**
@ -185,27 +188,42 @@ let OverviewView = {
/**
* Called when recording starts.
*/
_onRecordingStarted: function () {
_onRecordingStarted: function (_, recording) {
this._checkSelection(recording);
this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
this.framerateGraph.dropSelection();
this.framerateGraph.selectionEnabled = false;
this.markersOverview.selectionEnabled = false;
this.memoryOverview.selectionEnabled = false;
},
/**
* Called when recording stops.
*/
_onRecordingStopped: function () {
_onRecordingStopped: function (_, recording) {
this._checkSelection(recording);
clearTimeout(this._timeoutId);
this._timeoutId = null;
this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
},
this.framerateGraph.selectionEnabled = true;
this.markersOverview.selectionEnabled = true;
this.memoryOverview.selectionEnabled = true;
/**
* Called when a new recording is selected.
*/
_onRecordingSelected: function (_, recording) {
this.framerateGraph.dropSelection();
this._checkSelection(recording);
// If timeout exists, we have something recording, so
// this will still tick away at rendering. Otherwise, force a render.
if (!this._timeoutId) {
this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
}
},
_checkSelection: function (recording) {
let selectionEnabled = !recording.isRecording();
this.framerateGraph.selectionEnabled = selectionEnabled;
this.markersOverview.selectionEnabled = selectionEnabled;
this.memoryOverview.selectionEnabled = selectionEnabled;
}
};

View File

@ -0,0 +1,237 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Functions handling the recordings UI.
*/
let RecordingsView = Heritage.extend(WidgetMethods, {
/**
* Initialization function, called when the tool is started.
*/
initialize: function() {
this.widget = new SideMenuWidget($("#recordings-list"));
this._onSelect = this._onSelect.bind(this);
this._onRecordingStarted = this._onRecordingStarted.bind(this);
this._onRecordingStopped = this._onRecordingStopped.bind(this);
this._onRecordingImported = this._onRecordingImported.bind(this);
this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
this.emptyText = L10N.getStr("noRecordingsText");
PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.on(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
this.widget.addEventListener("select", this._onSelect, false);
},
/**
* Destruction function, called when the tool is closed.
*/
destroy: function() {
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
this.widget.removeEventListener("select", this._onSelect, false);
},
/**
* Adds an empty recording to this container.
*
* @param RecordingModel recording
* A model for the new recording item created.
*/
addEmptyRecording: function (recording) {
let titleNode = document.createElement("label");
titleNode.className = "plain recording-item-title";
titleNode.setAttribute("value", recording.getLabel() ||
L10N.getFormatStr("recordingsList.itemLabel", this.itemCount + 1));
let durationNode = document.createElement("label");
durationNode.className = "plain recording-item-duration";
durationNode.setAttribute("value",
L10N.getStr("recordingsList.recordingLabel"));
let saveNode = document.createElement("label");
saveNode.className = "plain recording-item-save";
saveNode.addEventListener("click", this._onSaveButtonClick);
let hspacer = document.createElement("spacer");
hspacer.setAttribute("flex", "1");
let footerNode = document.createElement("hbox");
footerNode.className = "recording-item-footer";
footerNode.appendChild(durationNode);
footerNode.appendChild(hspacer);
footerNode.appendChild(saveNode);
let vspacer = document.createElement("spacer");
vspacer.setAttribute("flex", "1");
let contentsNode = document.createElement("vbox");
contentsNode.className = "recording-item";
contentsNode.setAttribute("flex", "1");
contentsNode.appendChild(titleNode);
contentsNode.appendChild(vspacer);
contentsNode.appendChild(footerNode);
// Append a recording item to this container.
return this.push([contentsNode], {
// Store the recording model that contains all the data to be
// rendered in the item.
attachment: recording
});
},
/**
* Signals that a recording session has started.
*
* @param RecordingModel recording
* Model of the recording that was started.
*/
_onRecordingStarted: function (_, recording) {
// Insert a "dummy" recording item, to hint that recording has now started.
let recordingItem;
// If a label is specified (e.g due to a call to `console.profile`),
// then try reusing a pre-existing recording item, if there is one.
// This is symmetrical to how `this.handleRecordingEnded` works.
if (recording.getLabel()) {
recordingItem = this.getItemForAttachment(e =>
e.getLabel() === recording.getLabel());
}
// Otherwise, create a new empty recording item.
if (!recordingItem) {
recordingItem = this.addEmptyRecording(recording);
}
// Mark the corresponding item as being a "record in progress".
recordingItem.isRecording = true;
// If this is a manual recording, immediately select it.
if (!recording.getLabel()) {
this.selectedItem = recordingItem;
}
this.emit(EVENTS.RECORDING_SELECTED, recording);
},
/**
* Signals that a recording session has ended.
*
* @param RecordingModel recording
* The model of the recording that just stopped.
*/
_onRecordingStopped: function (_, recording) {
let profileLabel = recording.getLabel();
let recordingItem;
// If a label is specified (e.g due to a call to `console.profileEnd`),
// then try reusing a pre-existing recording item, if there is one.
// This is symmetrical to how `this.handleRecordingStarted` works.
if (profileLabel) {
recordingItem = this.getItemForAttachment(e =>
e.profilerData.profileLabel == profileLabel);
}
// Otherwise, just use the first available recording item.
if (!recordingItem) {
recordingItem = this.getItemForPredicate(e => e.isRecording);
}
// Mark the corresponding item as being a "finished recording".
recordingItem.isRecording = false;
// Render the recording item with finalized information (timing, etc)
this.finalizeRecording(recordingItem);
this.forceSelect(recordingItem);
},
/**
* Signals that a recording has been imported.
*
* @param object recordingData
* The profiler and refresh driver ticks data received from the front.
* @param RecordingModel model
* The recording model containing data on the recording session.
*/
_onRecordingImported: function (_, recordingData, model) {
let recordingItem = this.addEmptyRecording(model);
recordingItem.isRecording = false;
// Immediately select the imported recording
this.selectedItem = recordingItem;
// Render the recording item with finalized information (timing, etc)
this.finalizeRecording(recordingItem);
// Fire the selection and allow to propogate.
this.emit(EVENTS.RECORDING_SELECTED, model);
},
/**
* Adds recording data to a recording item in this container.
*
* @param Item recordingItem
* An item inserted via `RecordingsView.addEmptyRecording`.
*/
finalizeRecording: function (recordingItem) {
let model = recordingItem.attachment;
let saveNode = $(".recording-item-save", recordingItem.target);
saveNode.setAttribute("value",
L10N.getStr("recordingsList.saveLabel"));
let durationMillis = model.getDuration().toFixed(0);
let durationNode = $(".recording-item-duration", recordingItem.target);
durationNode.setAttribute("value",
L10N.getFormatStr("recordingsList.durationLabel", durationMillis));
},
/**
* The select listener for this container.
*/
_onSelect: Task.async(function*({ detail: recordingItem }) {
// TODO 1120699
// show appropriate empty/recording panels for several scenarios below
if (!recordingItem) {
return;
}
let model = recordingItem.attachment;
// If recording, don't abort completely, as we still want to fire an event
// for selection so we can continue repainting the overview graphs.
if (recordingItem.isRecording) {
this.emit(EVENTS.RECORDING_SELECTED, model);
return;
}
this.emit(EVENTS.RECORDING_SELECTED, model);
}),
/**
* The click listener for the "save" button of each item in this container.
*/
_onSaveButtonClick: function (e) {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
fp.defaultString = "profile.json";
fp.open({ done: result => {
if (result == Ci.nsIFilePicker.returnCancel) {
return;
}
let recordingItem = this.getItemForElement(e.target);
this.emit(EVENTS.UI_EXPORT_RECORDING, recordingItem.attachment, fp.file);
}});
}
});
/**
* Convenient way of emitting events from the RecordingsView.
*/
EventEmitter.decorate(RecordingsView);

View File

@ -755,7 +755,7 @@ var ProjectEditor = Class({
if (this.hasUnsavedResources) {
return confirm(
getLocalizedString("projecteditor.confirmUnsavedTitle"),
getLocalizedString("projecteditor.confirmUnsavedLabel")
getLocalizedString("projecteditor.confirmUnsavedLabel2")
);
}

View File

@ -15,7 +15,9 @@ function startupWebAudioEditor() {
return all([
WebAudioEditorController.initialize(),
ContextView.initialize(),
InspectorView.initialize()
InspectorView.initialize(),
PropertiesView.initialize(),
AutomationView.initialize()
]);
}
@ -27,6 +29,8 @@ function shutdownWebAudioEditor() {
WebAudioEditorController.destroy(),
ContextView.destroy(),
InspectorView.destroy(),
PropertiesView.destroy(),
AutomationView.destroy()
]);
}
@ -83,6 +87,7 @@ let WebAudioEditorController = {
$("#content").hidden = true;
ContextView.resetUI();
InspectorView.resetUI();
PropertiesView.resetUI();
},
// Since node create and connect are probably executed back to back,

View File

@ -10,7 +10,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource:///modules/devtools/gDevTools.jsm");
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
const { require } = devtools;
let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
let { EventTarget } = require("sdk/event/target");
@ -21,6 +22,8 @@ const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties"
const L10N = new ViewHelpers.L10N(STRINGS_URI);
const Telemetry = require("devtools/shared/telemetry");
const telemetry = new Telemetry();
devtools.lazyImporter(this, "LineGraphWidget",
"resource:///modules/devtools/Graphs.jsm");
// Override DOM promises with Promise.jsm helpers
const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
@ -53,11 +56,17 @@ const EVENTS = {
// When an audio node is finished loading in the Properties tab.
UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered",
// When an audio node is finished loading in the Automation tab.
UI_AUTOMATION_TAB_RENDERED: "WebAudioEditor:UIAutomationTabRendered",
// When the Audio Context graph finishes rendering.
// Is called with two arguments, first representing number of nodes
// rendered, second being the number of edge connections rendering (not counting
// param edges), followed by the count of the param edges rendered.
UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered"
UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered",
// Called when the inspector splitter is moved and resized.
UI_INSPECTOR_RESIZE: "WebAudioEditor:UIInspectorResize"
};
/**

View File

@ -85,6 +85,17 @@ const AudioNodeModel = Class({
return this.actor.getParams();
},
/**
* Returns a promise that resolves to an object containing an
* array of event information and an array of automation data.
*
* @param String paramName
* @return Promise->Array
*/
getAutomationData: function (paramName) {
return this.actor.getAutomationData(paramName);
},
/**
* Takes a `dagreD3.Digraph` object and adds this node to
* the graph to be rendered.

View File

@ -61,3 +61,6 @@ skip-if = true # bug 1010423
[browser_wa_properties-view-media-nodes.js]
[browser_wa_properties-view-params.js]
[browser_wa_properties-view-params-objects.js]
[browser_wa_automation-view-01.js]
[browser_wa_automation-view-02.js]

View File

@ -39,8 +39,6 @@ add_task(function*() {
}
else if (param === "curve") {
is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
} else {
is(Object.keys(flags), 0, type + "-" + param + " has no flags set")
}
}
}

View File

@ -36,8 +36,6 @@ add_task(function*() {
}
else if (param === "curve") {
is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
} else {
is(Object.keys(flags), 0, type + "-" + param + " has no flags set")
}
});
});

View File

@ -0,0 +1,55 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that automation view shows the correct view depending on if events
* or params exist.
*/
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(AUTOMATION_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS } = panelWin;
let started = once(gFront, "start-context");
reload(target);
let [actors] = yield Promise.all([
get3(gFront, "create-node"),
waitForGraphRendered(panelWin, 3, 2)
]);
let nodeIds = actors.map(actor => actor.actorID);
// Oscillator node
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield waitForInspectorRender(panelWin, EVENTS);
click(panelWin, $("#automation-tab"));
ok(isVisible($("#automation-graph-container")), "graph container should be visible");
ok(isVisible($("#automation-content")), "automation content should be visible");
ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
ok(!isVisible($("#automation-empty")), "empty panel should not be visible");
// Gain node
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield waitForInspectorRender(panelWin, EVENTS);
click(panelWin, $("#automation-tab"));
ok(!isVisible($("#automation-graph-container")), "graph container should be visible");
ok(isVisible($("#automation-content")), "automation content should not be visible");
ok(isVisible($("#automation-no-events")), "no-events panel should be visible");
ok(!isVisible($("#automation-empty")), "empty panel should not be visible");
// destination node
click(panelWin, findGraphNode(panelWin, nodeIds[0]));
yield waitForInspectorRender(panelWin, EVENTS);
click(panelWin, $("#automation-tab"));
ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
ok(!isVisible($("#automation-content")), "automation content should not be visible");
ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
ok(isVisible($("#automation-empty")), "empty panel should be visible");
yield teardown(target);
});

View File

@ -0,0 +1,56 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that automation view selects the first parameter by default and
* switching between AudioParam rerenders the graph.
*/
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(AUTOMATION_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, AutomationView } = panelWin;
let started = once(gFront, "start-context");
reload(target);
let [actors] = yield Promise.all([
get3(gFront, "create-node"),
waitForGraphRendered(panelWin, 3, 2)
]);
let nodeIds = actors.map(actor => actor.actorID);
// Oscillator node
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield waitForInspectorRender(panelWin, EVENTS);
click(panelWin, $("#automation-tab"));
ok(AutomationView._selectedParamName, "frequency",
"AutomatioView is set on 'frequency'");
ok($(".automation-param-button[data-param='frequency']").getAttribute("selected"),
"frequency param should be selected on load");
ok(!$(".automation-param-button[data-param='detune']").getAttribute("selected"),
"detune param should not be selected on load");
ok(isVisible($("#automation-content")), "automation content should be visible");
ok(isVisible($("#automation-graph-container")), "graph container should be visible");
ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
click(panelWin, $(".automation-param-button[data-param='detune']"));
yield once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED);
ok(true, "automation tab rerendered");
ok(AutomationView._selectedParamName, "detune",
"AutomatioView is set on 'detune'");
ok(!$(".automation-param-button[data-param='frequency']").getAttribute("selected"),
"frequency param should not be selected after clicking detune");
ok($(".automation-param-button[data-param='detune']").getAttribute("selected"),
"detune param should be selected after clicking detune");
ok(isVisible($("#automation-content")), "automation content should be visible");
ok(!isVisible($("#automation-graph-container")), "graph container should not be visible");
ok(isVisible($("#automation-no-events")), "no-events panel should be visible");
yield teardown(target);
});

View File

@ -51,8 +51,9 @@ add_task(function*() {
ok(!isVisible($("#web-audio-editor-tabs")),
"InspectorView tabs are still hidden.");
let nodeSet = once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield nodeSet;
ok(!isVisible($("#web-audio-editor-details-pane-empty")),
"Empty message hides even when loading node while open.");

View File

@ -30,12 +30,13 @@ add_task(function*() {
is($("#web-audio-inspector-title").value, "AudioNode Inspector",
"Inspector should have default title when empty.");
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
// Wait for the node to be set as well as the inspector to come fully into the view
yield Promise.all([
let nodeSet = Promise.all([
once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
]);
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield nodeSet;
ok(InspectorView.isVisible(), "InspectorView shown once node selected.");
ok(!isVisible($("#web-audio-editor-details-pane-empty")),
@ -49,8 +50,9 @@ add_task(function*() {
is($("#web-audio-editor-tabs").selectedIndex, 0,
"default tab selected should be the parameters tab.");
nodeSet = once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield nodeSet;
is($("#web-audio-inspector-title").value, "Gain",
"Inspector title updates when a new node is selected.");

View File

@ -8,8 +8,8 @@
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
let gVars = InspectorView._propsView;
let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
let gVars = PropertiesView._propsView;
let started = once(gFront, "start-context");
@ -24,7 +24,7 @@ add_task(function*() {
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
// Wait for the node to be set as well as the inspector to come fully into the view
yield Promise.all([
once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
waitForInspectorRender(panelWin, EVENTS),
once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
]);
@ -37,17 +37,17 @@ add_task(function*() {
}, "default loaded string");
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS),
checkVariableView(gVars, 0, {
"gain": 0
}, "default loaded number");
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS),
yield setAndCheck(0, "type", "square", "square", "sets string as string");
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS),
yield setAndCheck(0, "gain", "0.005", 0.005, "sets number as number");
yield setAndCheck(0, "gain", "0.1", 0.1, "sets float as float");
yield setAndCheck(0, "gain", ".2", 0.2, "sets float without leading zero as float");

View File

@ -8,8 +8,8 @@
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
let gVars = InspectorView._propsView;
let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
let gVars = PropertiesView._propsView;
let started = once(gFront, "start-context");
@ -24,7 +24,7 @@ add_task(function*() {
click(panelWin, findGraphNode(panelWin, nodeIds[3]));
// Wait for the node to be set as well as the inspector to come fully into the view
yield Promise.all([
once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
waitForInspectorRender(panelWin, EVENTS),
once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED),
]);

View File

@ -37,8 +37,8 @@ function waitForDeviceClosed() {
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(MEDIA_NODES_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
let gVars = InspectorView._propsView;
let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
let gVars = PropertiesView._propsView;
// Auto enable getUserMedia
let mediaPermissionPref = Services.prefs.getBoolPref(MEDIA_PERMISSION);
@ -59,7 +59,7 @@ add_task(function*() {
for (let i = 0; i < types.length; i++) {
click(panelWin, findGraphNode(panelWin, nodeIds[i]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS);
checkVariableView(gVars, 0, NODE_DEFAULT_VALUES[types[i]], types[i]);
}

View File

@ -9,8 +9,8 @@
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(BUFFER_AND_ARRAY_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
let gVars = InspectorView._propsView;
let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
let gVars = PropertiesView._propsView;
let started = once(gFront, "start-context");
@ -23,7 +23,7 @@ add_task(function*() {
let nodeIds = actors.map(actor => actor.actorID);
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS);
checkVariableView(gVars, 0, {
"curve": "Float32Array"
}, "WaveShaper's `curve` is listed as an `Float32Array`.");
@ -33,7 +33,7 @@ add_task(function*() {
ok(state, "Float32Array property should not have a dropdown.");
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS);
checkVariableView(gVars, 0, {
"buffer": "AudioBuffer"
}, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`.");

View File

@ -9,8 +9,8 @@
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(SIMPLE_NODES_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
let gVars = InspectorView._propsView;
let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
let gVars = PropertiesView._propsView;
let started = once(gFront, "start-context");
@ -30,7 +30,7 @@ add_task(function*() {
for (let i = 0; i < types.length; i++) {
click(panelWin, findGraphNode(panelWin, nodeIds[i]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS);
checkVariableView(gVars, 0, NODE_DEFAULT_VALUES[types[i]], types[i]);
}

View File

@ -8,8 +8,8 @@
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
let gVars = InspectorView._propsView;
let { gFront, $, $$, EVENTS, PropertiesView } = panelWin;
let gVars = PropertiesView._propsView;
let started = once(gFront, "start-context");
@ -23,19 +23,19 @@ add_task(function*() {
// Gain node
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS);
ok(isVisible($("#properties-tabpanel-content")), "Parameters shown when they exist.");
ok(!isVisible($("#properties-tabpanel-content-empty")),
ok(isVisible($("#properties-content")), "Parameters shown when they exist.");
ok(!isVisible($("#properties-empty")),
"Empty message hidden when AudioParams exist.");
// Destination node
click(panelWin, findGraphNode(panelWin, nodeIds[0]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
yield waitForInspectorRender(panelWin, EVENTS);
ok(!isVisible($("#properties-tabpanel-content")),
ok(!isVisible($("#properties-content")),
"Parameters hidden when they don't exist.");
ok(isVisible($("#properties-tabpanel-content-empty")),
ok(isVisible($("#properties-empty")),
"Empty message shown when no AudioParams exist.");
yield teardown(target);

View File

@ -35,12 +35,13 @@ add_task(function*() {
is(eventName, exp[0], "correct eventName in event");
is(paramName, "frequency", "correct paramName in event");
is(args.length, exp.length - 1, "correct length in args");
args.forEach((a, i) => {
// In the case of an array
if (typeof a === "object") {
a.forEach((f, j) => is(f, exp[i + 1][j], "correct argument in args"));
a.forEach((f, j) => is(f, exp[i + 1][j], `correct argument in Float32Array: ${f}`));
} else {
is(a, exp[i + 1], "correct argument in args");
is(a, exp[i + 1], `correct ${i+1}th argument in args: ${a}`);
}
});
events.push([eventName].concat(args));

View File

@ -231,7 +231,7 @@ function checkVariableView (view, index, hash, description = "") {
// If node shouldn't display any properties, ensure that the 'empty' message is
// visible
if (!variables.length) {
ok(isVisible(scope.window.$("#properties-tabpanel-content-empty")),
ok(isVisible(scope.window.$("#properties-empty")),
description + " should show the empty properties tab.");
return;
}
@ -413,10 +413,10 @@ function checkAutomationValue (values, time, expected) {
*/
function getValueAt (values, time) {
for (let i = 0; i < values.length; i++) {
if (values[i].t === time) {
if (values[i].delta === time) {
return values[i].value;
}
if (values[i].t > time) {
if (values[i].delta > time) {
return (values[i - 1].value + values[i].value) / 2;
}
}
@ -424,6 +424,16 @@ function checkAutomationValue (values, time, expected) {
}
}
/**
* Wait for all inspector tabs to complete rendering.
*/
function waitForInspectorRender (panelWin, EVENTS) {
return Promise.all([
once(panelWin, EVENTS.UI_PROPERTIES_TAB_RENDERED),
once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED)
]);
}
/**
* List of audio node properties to test against expectations of the AudioNode actor
*/

View File

@ -0,0 +1,159 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Functions handling the audio node inspector UI.
*/
let AutomationView = {
/**
* Initialization function called when the tool starts up.
*/
initialize: function () {
this._buttons = $("#automation-param-toolbar-buttons");
this.graph = new LineGraphWidget($("#automation-graph"), { avg: false });
this.graph.selectionEnabled = false;
this._onButtonClick = this._onButtonClick.bind(this);
this._onNodeSet = this._onNodeSet.bind(this);
this._onResize = this._onResize.bind(this);
this._buttons.addEventListener("click", this._onButtonClick);
window.on(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
},
/**
* Destruction function called when the tool cleans up.
*/
destroy: function () {
this._buttons.removeEventListener("click", this._onButtonClick);
window.off(EVENTS.UI_INSPECTOR_RESIZE, this._onResize);
window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
},
/**
* Empties out the props view.
*/
resetUI: function () {
this._currentNode = null;
},
/**
* On a new node selection, create the Automation panel for
* that specific node.
*/
build: Task.async(function* () {
let node = this._currentNode;
let props = yield node.getParams();
let params = props.filter(({ flags }) => flags && flags.param);
this._createParamButtons(params);
this._selectedParamName = params[0] ? params[0].param : null;
this.render();
}),
/**
* Renders the graph for specified `paramName`. Called when
* the parameter view is changed, or when new param data events
* are fired for the currently specified param.
*/
render: Task.async(function *() {
let node = this._currentNode;
let paramName = this._selectedParamName;
// Escape if either node or parameter name does not exist.
if (!node || !paramName) {
this._setState("no-params");
window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, null);
return;
}
let { values, events } = yield node.getAutomationData(paramName);
this._setState(events.length ? "show" : "no-events");
yield this.graph.setDataWhenReady(values);
window.emit(EVENTS.UI_AUTOMATION_TAB_RENDERED, node.id);
}),
/**
* Create the buttons for each AudioParam, that when clicked,
* render the graph for that AudioParam.
*/
_createParamButtons: function (params) {
this._buttons.innerHTML = "";
params.forEach((param, i) => {
let button = document.createElement("toolbarbutton");
button.setAttribute("class", "devtools-toolbarbutton automation-param-button");
button.setAttribute("data-param", param.param);
// Set label to the parameter name, should not be L10N'd
button.setAttribute("label", param.param);
// If first button, set to 'selected' for styling
if (i === 0) {
button.setAttribute("selected", true);
}
this._buttons.appendChild(button);
});
},
/**
* Internally sets the current audio node and rebuilds appropriate
* views.
*/
_setAudioNode: function (node) {
this._currentNode = node;
if (this._currentNode) {
this.build();
}
},
/**
* Toggles the subviews to display messages whether or not
* the audio node has no AudioParams, no automation events, or
* shows the graph.
*/
_setState: function (state) {
let contentView = $("#automation-content");
let emptyView = $("#automation-empty");
let graphView = $("#automation-graph-container");
let noEventsView = $("#automation-no-events");
contentView.hidden = state === "no-params";
emptyView.hidden = state !== "no-params";
graphView.hidden = state !== "show";
noEventsView.hidden = state !== "no-events";
},
/**
* Event handlers
*/
_onButtonClick: function (e) {
Array.forEach($$(".automation-param-button"), $btn => $btn.removeAttribute("selected"));
let paramName = e.target.getAttribute("data-param");
e.target.setAttribute("selected", true);
this._selectedParamName = paramName;
this.render();
},
/**
* Called when the inspector is resized.
*/
_onResize: function () {
this.graph.refresh();
},
/**
* Called when the inspector view determines a node is selected.
*/
_onNodeSet: function (_, id) {
this._setAudioNode(id != null ? gAudioNodes.get(id) : null);
}
};

View File

@ -38,7 +38,6 @@ let ContextView = {
initialize: function() {
this._onGraphNodeClick = this._onGraphNodeClick.bind(this);
this._onThemeChange = this._onThemeChange.bind(this);
this._onNodeSelect = this._onNodeSelect.bind(this);
this._onStartContext = this._onStartContext.bind(this);
this._onEvent = this._onEvent.bind(this);
@ -46,7 +45,6 @@ let ContextView = {
$('#graph-target').addEventListener('click', this._onGraphNodeClick, false);
window.on(EVENTS.THEME_CHANGE, this._onThemeChange);
window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect);
window.on(EVENTS.START_CONTEXT, this._onStartContext);
gAudioNodes.on("*", this._onEvent);
},
@ -62,7 +60,6 @@ let ContextView = {
}
$('#graph-target').removeEventListener('click', this._onGraphNodeClick, false);
window.off(EVENTS.THEME_CHANGE, this._onThemeChange);
window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSelect);
window.off(EVENTS.START_CONTEXT, this._onStartContext);
gAudioNodes.off("*", this._onEvent);
},
@ -112,7 +109,6 @@ let ContextView = {
* Makes the corresponding graph node appear "focused", removing
* focused styles from all other nodes. If no `actorID` specified,
* make all nodes appear unselected.
* Called from UI_INSPECTOR_NODE_SELECT.
*/
focusNode: function (actorID) {
// Remove class "selected" from all nodes
@ -272,10 +268,6 @@ let ContextView = {
}
},
_onNodeSelect: function (eventName, id) {
this.focusNode(id);
},
/**
* Fired when the devtools theme changes.
*/
@ -300,6 +292,9 @@ let ContextView = {
if (!node)
return;
window.emit(EVENTS.UI_SELECT_NODE, node.getAttribute("data-id"));
let id = node.getAttribute("data-id");
this.focusNode(id);
window.emit(EVENTS.UI_SELECT_NODE, id);
}
};

View File

@ -3,25 +3,13 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
Cu.import("resource:///modules/devtools/VariablesView.jsm");
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
// Strings for rendering
const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector");
const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector");
// Store width as a preference rather than hardcode
// TODO bug 1009056
const INSPECTOR_WIDTH = 300;
const GENERIC_VARIABLES_VIEW_SETTINGS = {
searchEnabled: false,
editableValueTooltip: "",
editableNameTooltip: "",
preventDisableOnChange: true,
preventDescriptorModifiers: false,
eval: () => {}
};
// Strings for rendering
const EXPAND_INSPECTOR_STRING = L10N.getStr("expandInspector");
const COLLAPSE_INSPECTOR_STRING = L10N.getStr("collapseInspector");
/**
* Functions handling the audio node inspector UI.
@ -41,10 +29,9 @@ let InspectorView = {
* Initialization function called when the tool starts up.
*/
initialize: function () {
this._tabsPane = $("#web-audio-editor-tabs");
// Set up view controller
this.el = $("#web-audio-inspector");
this.splitter = $("#inspector-splitter");
this.el.setAttribute("width", INSPECTOR_WIDTH);
this.button = $("#inspector-pane-toggle");
mixin(this, ToggleMixin);
@ -53,13 +40,11 @@ let InspectorView = {
// Hide inspector view on startup
this.hideImmediately();
this._onEval = this._onEval.bind(this);
this._onNodeSelect = this._onNodeSelect.bind(this);
this._onDestroyNode = this._onDestroyNode.bind(this);
this._onResize = this._onResize.bind(this);
this._propsView = new VariablesView($("#properties-tabpanel-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
this._propsView.eval = this._onEval;
this.splitter.addEventListener("mouseup", this._onResize);
window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
gAudioNodes.on("remove", this._onDestroyNode);
},
@ -69,12 +54,13 @@ let InspectorView = {
*/
destroy: function () {
this.unbindToggle();
this.splitter.removeEventListener("mouseup", this._onResize);
window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
gAudioNodes.off("remove", this._onDestroyNode);
this.el = null;
this.button = null;
this._tabsPane = null;
this.splitter = null;
},
/**
@ -96,8 +82,7 @@ let InspectorView = {
$("#web-audio-editor-details-pane-empty").setAttribute("hidden", "true");
$("#web-audio-editor-tabs").removeAttribute("hidden");
this._setTitle();
this._buildPropertiesView()
.then(() => window.emit(EVENTS.UI_INSPECTOR_NODE_SET, this._currentNode.id));
window.emit(EVENTS.UI_INSPECTOR_NODE_SET, this._currentNode.id);
}
},
@ -112,7 +97,6 @@ let InspectorView = {
* Empties out the props view.
*/
resetUI: function () {
this._propsView.empty();
// Set current node to empty to load empty view
this.setCurrentAudioNode();
@ -129,97 +113,10 @@ let InspectorView = {
$("#web-audio-inspector-title").setAttribute("value", title);
},
/**
* Reconstructs the `Properties` tab in the inspector
* with the `this._currentNode` as it's source.
*/
_buildPropertiesView: Task.async(function* () {
let propsView = this._propsView;
let node = this._currentNode;
propsView.empty();
let audioParamsScope = propsView.addScope("AudioParams");
let props = yield node.getParams();
// Disable AudioParams VariableView expansion
// when there are no props i.e. AudioDestinationNode
this._togglePropertiesView(!!props.length);
props.forEach(({ param, value, flags }) => {
let descriptor = {
value: value,
writable: !flags || !flags.readonly,
};
let item = audioParamsScope.addItem(param, descriptor);
// No items should currently display a dropdown
item.twisty = false;
});
audioParamsScope.expanded = true;
window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id);
}),
_togglePropertiesView: function (show) {
let propsView = $("#properties-tabpanel-content");
let emptyView = $("#properties-tabpanel-content-empty");
(show ? propsView : emptyView).removeAttribute("hidden");
(show ? emptyView : propsView).setAttribute("hidden", "true");
},
/**
* Returns the scope for AudioParams in the
* VariablesView.
*
* @return Scope
*/
_getAudioPropertiesScope: function () {
return this._propsView.getScopeAtIndex(0);
},
/**
* Event handlers
*/
/**
* Executed when an audio prop is changed in the UI.
*/
_onEval: Task.async(function* (variable, value) {
let ownerScope = variable.ownerView;
let node = this._currentNode;
let propName = variable.name;
let error;
if (!variable._initialDescriptor.writable) {
error = new Error("Variable " + propName + " is not writable.");
} else {
// Cast value to proper type
try {
let number = parseFloat(value);
if (!isNaN(number)) {
value = number;
} else {
value = JSON.parse(value);
}
error = yield node.actor.setParam(propName, value);
}
catch (e) {
error = e;
}
}
// TODO figure out how to handle and display set prop errors
// and enable `test/brorwser_wa_properties-view-edit.js`
// Bug 994258
if (!error) {
ownerScope.get(propName).setGrip(value);
window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
} else {
window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
}
}),
/**
* Called on EVENTS.UI_SELECT_NODE, and takes an actorID `id`
* and calls `setCurrentAudioNode` to scaffold the inspector view.
@ -231,6 +128,10 @@ let InspectorView = {
this.show();
},
_onResize: function () {
window.emit(EVENTS.UI_INSPECTOR_RESIZE);
},
/**
* Called when `DESTROY_NODE` is fired to remove the node from props view if
* it's currently selected.

View File

@ -0,0 +1,164 @@
/* 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";
Cu.import("resource:///modules/devtools/VariablesView.jsm");
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
const GENERIC_VARIABLES_VIEW_SETTINGS = {
searchEnabled: false,
editableValueTooltip: "",
editableNameTooltip: "",
preventDisableOnChange: true,
preventDescriptorModifiers: false,
eval: () => {}
};
/**
* Functions handling the audio node inspector UI.
*/
let PropertiesView = {
/**
* Initialization function called when the tool starts up.
*/
initialize: function () {
this._onEval = this._onEval.bind(this);
this._onNodeSet = this._onNodeSet.bind(this);
window.on(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
this._propsView = new VariablesView($("#properties-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
this._propsView.eval = this._onEval;
},
/**
* Destruction function called when the tool cleans up.
*/
destroy: function () {
window.off(EVENTS.UI_INSPECTOR_NODE_SET, this._onNodeSet);
this._propsView = null;
},
/**
* Empties out the props view.
*/
resetUI: function () {
this._propsView.empty();
this._currentNode = null;
},
/**
* Internally sets the current audio node and rebuilds appropriate
* views.
*/
_setAudioNode: function (node) {
this._currentNode = node;
if (this._currentNode) {
this._buildPropertiesView();
}
},
/**
* Reconstructs the `Properties` tab in the inspector
* with the `this._currentNode` as it's source.
*/
_buildPropertiesView: Task.async(function* () {
let propsView = this._propsView;
let node = this._currentNode;
propsView.empty();
let audioParamsScope = propsView.addScope("AudioParams");
let props = yield node.getParams();
// Disable AudioParams VariableView expansion
// when there are no props i.e. AudioDestinationNode
this._togglePropertiesView(!!props.length);
props.forEach(({ param, value, flags }) => {
let descriptor = {
value: value,
writable: !flags || !flags.readonly,
};
let item = audioParamsScope.addItem(param, descriptor);
// No items should currently display a dropdown
item.twisty = false;
});
audioParamsScope.expanded = true;
window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id);
}),
/**
* Toggles the display of the "empty" properties view when
* node has no properties to display.
*/
_togglePropertiesView: function (show) {
let propsView = $("#properties-content");
let emptyView = $("#properties-empty");
(show ? propsView : emptyView).removeAttribute("hidden");
(show ? emptyView : propsView).setAttribute("hidden", "true");
},
/**
* Returns the scope for AudioParams in the
* VariablesView.
*
* @return Scope
*/
_getAudioPropertiesScope: function () {
return this._propsView.getScopeAtIndex(0);
},
/**
* Event handlers
*/
/**
* Called when the inspector view determines a node is selected.
*/
_onNodeSet: function (_, id) {
this._setAudioNode(gAudioNodes.get(id));
},
/**
* Executed when an audio prop is changed in the UI.
*/
_onEval: Task.async(function* (variable, value) {
let ownerScope = variable.ownerView;
let node = this._currentNode;
let propName = variable.name;
let error;
if (!variable._initialDescriptor.writable) {
error = new Error("Variable " + propName + " is not writable.");
} else {
// Cast value to proper type
try {
let number = parseFloat(value);
if (!isNaN(number)) {
value = number;
} else {
value = JSON.parse(value);
}
error = yield node.actor.setParam(propName, value);
}
catch (e) {
error = e;
}
}
// TODO figure out how to handle and display set prop errors
// and enable `test/brorwser_wa_properties-view-edit.js`
// Bug 994258
if (!error) {
ownerScope.get(propName).setGrip(value);
window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
} else {
window.emit(EVENTS.UI_SET_PARAM_ERROR, node.id, propName, value);
}
})
};

View File

@ -25,6 +25,8 @@
<script type="application/javascript" src="webaudioeditor/views/utils.js"/>
<script type="application/javascript" src="webaudioeditor/views/context.js"/>
<script type="application/javascript" src="webaudioeditor/views/inspector.js"/>
<script type="application/javascript" src="webaudioeditor/views/properties.js"/>
<script type="application/javascript" src="webaudioeditor/views/automation.js"/>
<vbox class="theme-body" flex="1">
<hbox id="reload-notice"
@ -75,7 +77,7 @@
</vbox>
</box>
</hbox>
<splitter class="devtools-side-splitter"/>
<splitter id="inspector-splitter" class="devtools-side-splitter"/>
<vbox id="web-audio-inspector" hidden="true">
<hbox class="devtools-toolbar">
<label id="web-audio-inspector-title" value="&webAudioEditorUI.inspectorTitle;"></label>
@ -90,16 +92,39 @@
<tabs>
<tab id="properties-tab"
label="&webAudioEditorUI.tab.properties;"/>
<tab id="automation-tab"
label="&webAudioEditorUI.tab.automation;"/>
</tabs>
<tabpanels flex="1">
<!-- Properties Panel -->
<tabpanel id="properties-tabpanel"
class="tabpanel-content">
<vbox id="properties-tabpanel-content" flex="1">
<vbox id="properties-content" flex="1" hidden="true">
</vbox>
<vbox id="properties-tabpanel-content-empty" flex="1" hidden="true">
<vbox id="properties-empty" flex="1" hidden="true">
<label value="&webAudioEditorUI.propertiesEmpty;"></label>
</vbox>
</tabpanel>
<!-- Automation Panel -->
<tabpanel id="automation-tabpanel"
class="tabpanel-content">
<vbox id="automation-content" flex="1" hidden="true">
<toolbar id="automation-param-toolbar" class="devtools-toolbar">
<hbox id="automation-param-toolbar-buttons" class="devtools-toolbarbutton-group">
</hbox>
</toolbar>
<box id="automation-graph-container" flex="1">
<canvas id="automation-graph"></canvas>
</box>
<vbox id="automation-no-events" flex="1" hidden="true">
<label value="&webAudioEditorUI.automationNoEvents;"></label>
</vbox>
</vbox>
<vbox id="automation-empty" flex="1" hidden="true">
<label value="&webAudioEditorUI.automationEmpty;"></label>
</vbox>
</tabpanel>
</tabpanels>
</tabbox>
</deck>

View File

@ -118,7 +118,7 @@ let UI = {
this._telemetry.toolClosed("webide");
},
canWindowClose: function() {
canCloseProject: function() {
if (this.projecteditor) {
return this.projecteditor.confirmUnsaved();
}
@ -155,6 +155,11 @@ let UI = {
this.updateCommands();
this.updateConnectionTelemetry();
break;
case "before-project":
if (!this.canCloseProject()) {
details.cancel();
}
break;
case "project":
this._updatePromise = Task.spawn(function() {
UI.updateTitle();
@ -971,7 +976,7 @@ let UI = {
let Cmds = {
quit: function() {
if (UI.canWindowClose()) {
if (UI.canCloseProject()) {
window.close();
}
},

View File

@ -14,7 +14,7 @@
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://webide/skin/webide.css"?>
<window id="webide" onclose="return UI.canWindowClose();"
<window id="webide" onclose="return UI.canCloseProject();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
title="&windowTitle;"

View File

@ -276,6 +276,13 @@ let AppManager = exports.AppManager = {
// A regular comparison still sees a difference when equal in some cases
if (JSON.stringify(this._selectedProject) !==
JSON.stringify(value)) {
let cancelled = false;
this.update("before-project", { cancel: () => { cancelled = true; } });
if (cancelled) {
return;
}
this._selectedProject = value;
// Clear out tab store's selected state, if any
@ -303,6 +310,10 @@ let AppManager = exports.AppManager = {
removeSelectedProject: function() {
let location = this.selectedProject.location;
AppManager.selectedProject = null;
// If the user cancels the removeProject operation, don't remove the project
if (AppManager.selectedProject != null) {
return;
}
return AppProjects.remove(location);
},

View File

@ -13,13 +13,13 @@
# LOCALIZATION NOTE (projecteditor.confirmUnsavedTitle):
# This string is displayed as as the title of the confirm prompt that checks
# to make sure if the project editor can be closed without saving changes
# to make sure if the project can be closed/switched without saving changes
projecteditor.confirmUnsavedTitle=Unsaved Changes
# LOCALIZATION NOTE (projecteditor.confirmUnsavedLabel):
# LOCALIZATION NOTE (projecteditor.confirmUnsavedLabel2):
# This string is displayed as the message of the confirm prompt that checks
# to make sure if the project editor can be closed without saving changes
projecteditor.confirmUnsavedLabel=You have unsaved changes that will be lost if you exit. Are you sure you want to continue?
# to make sure if the project can be closed/switched without saving changes
projecteditor.confirmUnsavedLabel2=You have unsaved changes that will be lost. Are you sure you want to continue?
# LOCALIZATION NOTE (projecteditor.deleteLabel):
# This string is displayed as a context menu item for allowing the selected

View File

@ -27,6 +27,10 @@
- for the properties tab view. -->
<!ENTITY webAudioEditorUI.tab.properties "Parameters">
<!-- LOCALIZATION NOTE (webAudioEditorUI.tab.automation): This is the label shown
- for the automation tab view. -->
<!ENTITY webAudioEditorUI.tab.automation "Automation">
<!-- LOCALIZATION NOTE (webAudioEditorUI.inspectorTitle): This is the title for the
- AudioNode inspector view. -->
<!ENTITY webAudioEditorUI.inspectorTitle "AudioNode Inspector">
@ -38,3 +42,12 @@
<!-- LOCALIZATION NOTE (webAudioEditorUI.propertiesEmpty): This is the title for the
- AudioNode inspector view properties tab empty message. -->
<!ENTITY webAudioEditorUI.propertiesEmpty "Node does not have any properties.">
<!-- LOCALIZATION NOTE (webAudioEditorUI.automationEmpty): This is the title for the
- AudioNode inspector view automation tab empty message. -->
<!ENTITY webAudioEditorUI.automationEmpty "Node does not have any AudioParams.">
<!-- LOCALIZATION NOTE (webAudioEditorUI.automationNoEvents): This is the title for the
- AudioNode inspector view automation tab message when there are no automation
- events. -->
<!ENTITY webAudioEditorUI.automationNoEvents "AudioParam does not have any automation events.">

View File

@ -362,3 +362,33 @@
.marker-details-duration {
font-weight: bold;
}
/* Recording items */
.recording-item {
padding: 4px;
}
.recording-item-title {
font-size: 110%;
}
.recording-item-footer {
padding-top: 4px;
font-size: 90%;
}
.recording-item-save {
text-decoration: underline;
cursor: pointer;
}
.recording-item-duration,
.recording-item-save {
color: var(--theme-body-color-alt);
}
#recordings-list .selected label {
/* Text inside a selected item should not be custom colored. */
color: inherit !important;
}

View File

@ -145,6 +145,20 @@ text {
-moz-image-region: rect(0px,32px,16px,16px);
}
/**
* Automation Styles
*/
#automation-param-toolbar .automation-param-button[selected] {
color: var(--theme-selection-color);
background-color: var(--theme-selection-background);
}
#automation-graph {
overflow: hidden;
-moz-box-flex: 1;
}
@media (min-resolution: 2dppx) {
#inspector-pane-toggle {
list-style-image: url(debugger-collapse@2x.png);

View File

@ -51,11 +51,10 @@ static RedirEntry kRedirMap[] = {
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
{ "compartments", "chrome://global/content/aboutCompartments.xhtml",
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
{ "memory", "chrome://global/content/aboutMemory.xhtml",
nsIAboutModule::ALLOW_SCRIPT },
{ "compartments", "chrome://global/content/aboutCompartments.xhtml",
nsIAboutModule::ALLOW_SCRIPT },
{ "addons", "chrome://mozapps/content/extensions/extensions.xul",
nsIAboutModule::ALLOW_SCRIPT },
{ "newaddon", "chrome://mozapps/content/extensions/newaddon.xul",

View File

@ -168,6 +168,7 @@ const mozilla::Module::ContractIDEntry kDocShellContracts[] = {
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "neterror", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "compartments", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "memory", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "compartments", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "addons", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "newaddon", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "support", &kNS_ABOUT_REDIRECTOR_MODULE_CID },

View File

@ -167,6 +167,30 @@ JS_GetEmptyString(JSRuntime *rt)
return rt->emptyString;
}
JS_PUBLIC_API(bool)
JS_GetCompartmentStats(JSRuntime *rt, CompartmentStatsVector &stats)
{
if (!stats.resizeUninitialized(rt->numCompartments))
return false;
size_t pos = 0;
for (CompartmentsIter c(rt, WithAtoms); !c.done(); c.next()) {
CompartmentTimeStats *stat = &stats[pos];
stat->time = c.get()->totalTime;
stat->compartment = c.get();
stat->addonId = c.get()->addonId;
if (rt->compartmentNameCallback) {
(*rt->compartmentNameCallback)(rt, stat->compartment,
stat->compartmentName,
MOZ_ARRAY_LENGTH(stat->compartmentName));
} else {
strcpy(stat->compartmentName, "<unknown>");
}
pos++;
}
return true;
}
namespace js {
void

View File

@ -1042,6 +1042,19 @@ JS_GetEmptyStringValue(JSContext *cx);
extern JS_PUBLIC_API(JSString *)
JS_GetEmptyString(JSRuntime *rt);
struct CompartmentTimeStats {
char compartmentName[1024];
JSAddonId *addonId;
JSCompartment *compartment;
uint64_t time; // microseconds
uint64_t cpowTime; // microseconds
};
typedef js::Vector<CompartmentTimeStats, 0, js::SystemAllocPolicy> CompartmentStatsVector;
extern JS_PUBLIC_API(bool)
JS_GetCompartmentStats(JSRuntime *rt, CompartmentStatsVector &stats);
/*
* Format is a string of the following characters (spaces are insignificant),
* specifying the tabulated type conversions:

View File

@ -345,8 +345,16 @@ xpc::TraceXPCGlobal(JSTracer *trc, JSObject *obj)
compartmentPrivate->scope->TraceInside(trc);
}
namespace xpc {
uint64_t
GetCompartmentCPOWMicroseconds(JSCompartment *compartment)
{
xpc::CompartmentPrivate *compartmentPrivate = xpc::CompartmentPrivate::Get(compartment);
return compartmentPrivate ? PR_IntervalToMicroseconds(compartmentPrivate->CPOWTime) : 0;
}
JSObject*
CreateGlobalObject(JSContext *cx, const JSClass *clasp, nsIPrincipal *principal,
JS::CompartmentOptions& aOptions)

View File

@ -139,6 +139,9 @@ XrayAwareCalleeGlobal(JSObject *fun);
void
TraceXPCGlobal(JSTracer *trc, JSObject *obj);
uint64_t
GetCompartmentCPOWMicroseconds(JSCompartment *compartment);
} /* namespace xpc */
namespace JS {

View File

@ -24,54 +24,55 @@ let TabMirror = function(deviceId, window) {
this.RTCIceCandidate = window.mozRTCIceCandidate;
Services.obs.addObserver((aSubject, aTopic, aData) => this._processMessage(aData), "MediaPlayer:Response", false);
this._sendMessage({ start: true });
this._window = window;
this._pc = new window.mozRTCPeerConnection(CONFIG, {});
if (!this._pc) {
throw "Failure creating Webrtc object";
}
this._pc.onicecandidate = this._onIceCandidate.bind(this);
let windowId = window.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
let viewport = window.BrowserApp.selectedTab.getViewport();
let maxWidth = Math.max(viewport.cssWidth, viewport.width);
let maxHeight = Math.max(viewport.cssHeight, viewport.height);
let minRatio = Math.sqrt((maxWidth * maxHeight) / (640 * 480));
let screenWidth = 640;
let screenHeight = 480;
let videoWidth = 0;
let videoHeight = 0;
if (screenWidth/screenHeight > maxWidth / maxHeight) {
videoWidth = screenWidth;
videoHeight = Math.ceil(videoWidth * maxHeight / maxWidth);
} else {
videoHeight = screenHeight;
videoWidth = Math.ceil(videoHeight * maxWidth / maxHeight);
}
let constraints = {
video: {
mediaSource: "browser",
browserWindow: windowId,
scrollWithPage: true,
advanced: [
{ width: { min: videoWidth, max: videoWidth },
height: { min: videoHeight, max: videoHeight }
},
{ aspectRatio: maxWidth / maxHeight }
]
}
};
window.navigator.mozGetUserMedia(constraints, this._onGumSuccess.bind(this), this._onGumFailure.bind(this));
};
TabMirror.prototype = {
_window: null,
_screenSize: { width: 1280, height: 720 },
_pc: null,
_start: function() {
this._pc.onicecandidate = this._onIceCandidate.bind(this);
let windowId = this._window.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
let viewport = this._window.BrowserApp.selectedTab.getViewport();
let maxWidth = Math.max(viewport.cssWidth, viewport.width);
let maxHeight = Math.max(viewport.cssHeight, viewport.height);
let videoWidth = 0;
let videoHeight = 0;
if (this._screenSize.width/this._screenSize.height < maxWidth / maxHeight) {
videoWidth = this._screenSize.width;
videoHeight = Math.ceil(videoWidth * maxHeight / maxWidth);
} else {
videoHeight = this._screenSize.height;
videoWidth = Math.ceil(videoHeight * maxWidth / maxHeight);
}
let constraints = {
video: {
mediaSource: "browser",
browserWindow: windowId,
scrollWithPage: true,
advanced: [
{ width: { min: videoWidth, max: videoWidth },
height: { min: videoHeight, max: videoHeight }
},
{ aspectRatio: maxWidth / maxHeight }
]
}
};
this._window.navigator.mozGetUserMedia(constraints, this._onGumSuccess.bind(this), this._onGumFailure.bind(this));
},
_processMessage: function(data) {
if (!data) {
return;
}
@ -82,21 +83,27 @@ TabMirror.prototype = {
return;
}
if (msg.sdp) {
if (msg.type === "answer") {
this._processAnswer(msg);
} else {
log("Unandled sdp message type: " + msg.type);
if (msg.sdp && msg.type === "answer") {
this._processAnswer(msg);
} else if (msg.type == "size") {
if (msg.height) {
this._screenSize.height = msg.height;
}
} else {
if (msg.width) {
this._screenSize.width = msg.width;
}
this._start();
} else if (msg.candidate) {
this._processIceCandidate(msg);
} else {
log("dropping unrecognized message: " + JSON.stringify(msg));
}
},
// Signaling methods
_processAnswer: function(msg) {
this._pc.setRemoteDescription(new this.RTCSessionDescription(msg),
this._setRemoteAnswerSuccess.bind(this), failure);
this._setRemoteAnswerSuccess.bind(this), failure);
},
_processIceCandidate: function(msg) {

View File

@ -0,0 +1,75 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
function go() {
let compartmentInfo = Cc["@mozilla.org/compartment-info;1"]
.getService(Ci.nsICompartmentInfo);
let compartments = compartmentInfo.getCompartments();
let count = compartments.length;
let addons = {};
for (let i = 0; i < count; i++) {
let compartment = compartments.queryElementAt(i, Ci.nsICompartment);
if (addons[compartment.addonId]) {
addons[compartment.addonId].time += compartment.time;
addons[compartment.addonId].CPOWTime += compartment.CPOWTime;
addons[compartment.addonId].compartments.push(compartment);
} else {
addons[compartment.addonId] = {
time: compartment.time,
CPOWTime: compartment.CPOWTime,
compartments: [compartment]
};
}
}
let dataDiv = document.getElementById("data");
for (let addon in addons) {
let el = document.createElement("tr");
let name = document.createElement("td");
let time = document.createElement("td");
let cpow = document.createElement("td");
name.className = "addon";
time.className = "time";
cpow.className = "cpow";
name.textContent = addon;
AddonManager.getAddonByID(addon, function(a) {
if (a) {
name.textContent = a.name;
}
});
time.textContent = addons[addon].time +"μs";
cpow.textContent = addons[addon].CPOWTime +"μs";
el.appendChild(time);
el.appendChild(cpow);
el.appendChild(name);
let div = document.createElement("tr");
for (let comp of addons[addon].compartments) {
let c = document.createElement("tr");
let name = document.createElement("td");
let time = document.createElement("td");
let cpow = document.createElement("td");
name.className = "addon";
time.className = "time";
cpow.className = "cpow";
name.textContent = comp.label;
time.textContent = comp.time +"μs";
cpow.textContent = comp.CPOWTime +"μs";
c.appendChild(time);
c.appendChild(cpow);
c.appendChild(name);
div.appendChild(c);
div.className = "details";
}
el.addEventListener("click", function() { div.style.display = (div.style.display != "block" ? "block" : "none"); });
el.appendChild(div);
dataDiv.appendChild(el);
}
}

View File

@ -0,0 +1,43 @@
<?xml version="1.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/. -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>about:compartments</title>
<script type="text/javascript;version=1.8" src="chrome://global/content/aboutCompartments.js"></script>
<style>
td.addon {
display: inline-block;
width: 400px;
}
td.time {
display: inline-block;
width: 100px;
}
td.cpow {
display: inline-block;
width: 100px;
}
.header {
font-weight: bold;
}
tr.details {
font-weight: lighter;
color: gray;
display: none;
}
</style>
</head>
<body onload="go()">
<table id="data">
<tr class="header">
<td class="time">time</td>
<td class="cpow">time in CPOWs</td>
<td class="addon">name</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,7 @@
# 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/.
toolkit.jar:
+ content/global/aboutCompartments.xhtml (content/aboutCompartments.xhtml)
+ content/global/aboutCompartments.js (content/aboutCompartments.js)

View File

@ -0,0 +1,23 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
JAR_MANIFESTS += ['jar.mn']
XPIDL_MODULE = 'compartments'
XPIDL_SOURCES += [
'nsICompartmentInfo.idl',
]
UNIFIED_SOURCES += [
'nsCompartmentInfo.cpp'
]
EXPORTS += [
'nsCompartmentInfo.h'
]
FINAL_LIBRARY = 'xul'

View File

@ -0,0 +1,94 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#include "nsCompartmentInfo.h"
#include "nsMemory.h"
#include "nsLiteralString.h"
#include "nsCRTGlue.h"
#include "nsIJSRuntimeService.h"
#include "nsServiceManagerUtils.h"
#include "nsIMutableArray.h"
#include "nsJSUtils.h"
#include "xpcpublic.h"
class nsCompartment : public nsICompartment {
public:
nsCompartment(nsAString& aCompartmentName, nsAString& aAddonId,
uint64_t aTime, uint64_t aCPOWTime)
: mCompartmentName(aCompartmentName), mAddonId(aAddonId), mTime(aTime), mCPOWTime(aCPOWTime) {}
NS_DECL_ISUPPORTS
/* readonly attribute wstring compartmentName; */
NS_IMETHOD GetCompartmentName(nsAString& aCompartmentName){
aCompartmentName.Assign(mCompartmentName);
return NS_OK;
};
/* readonly attribute unsigned long time; */
NS_IMETHOD GetTime(uint64_t* aTime) {
*aTime = mTime;
return NS_OK;
}
/* readonly attribute wstring addon id; */
NS_IMETHOD GetAddonId(nsAString& aAddonId){
aAddonId.Assign(mAddonId);
return NS_OK;
};
/* readonly attribute unsigned long CPOW time; */
NS_IMETHOD GetCPOWTime(uint64_t* aCPOWTime) {
*aCPOWTime = mCPOWTime;
return NS_OK;
}
private:
nsString mCompartmentName;
nsString mAddonId;
uint64_t mTime;
uint64_t mCPOWTime;
virtual ~nsCompartment() {}
};
NS_IMPL_ISUPPORTS(nsCompartment, nsICompartment)
NS_IMPL_ISUPPORTS(nsCompartmentInfo, nsICompartmentInfo)
nsCompartmentInfo::nsCompartmentInfo()
{
}
nsCompartmentInfo::~nsCompartmentInfo()
{
}
NS_IMETHODIMP
nsCompartmentInfo::GetCompartments(nsIArray** aCompartments)
{
JSRuntime* rt;
nsCOMPtr<nsIJSRuntimeService> svc(do_GetService("@mozilla.org/js/xpc/RuntimeService;1"));
NS_ENSURE_TRUE(svc, NS_ERROR_FAILURE);
svc->GetRuntime(&rt);
nsCOMPtr<nsIMutableArray> compartments = do_CreateInstance(NS_ARRAY_CONTRACTID);
CompartmentStatsVector stats;
if (!JS_GetCompartmentStats(rt, stats))
return NS_ERROR_OUT_OF_MEMORY;
size_t num = stats.length();
for (size_t pos = 0; pos < num; pos++) {
nsString addonId;
if (stats[pos].addonId) {
AssignJSFlatString(addonId, (JSFlatString*)stats[pos].addonId);
} else {
addonId.AssignLiteral("<non-addon>");
}
uint32_t cpowTime = xpc::GetCompartmentCPOWMicroseconds(stats[pos].compartment);
nsCString compartmentName(stats[pos].compartmentName);
NS_ConvertUTF8toUTF16 name(compartmentName);
compartments->AppendElement(new nsCompartment(name, addonId, stats[pos].time, cpowTime), false);
}
compartments.forget(aCompartments);
return NS_OK;
}

View File

@ -0,0 +1,25 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#ifndef nsCompartmentInfo_h
#define nsCompartmentInfo_h
#include "nsICompartmentInfo.h"
class nsCompartmentInfo : public nsICompartmentInfo
{
public:
NS_DECL_ISUPPORTS
NS_DECL_NSICOMPARTMENTINFO
nsCompartmentInfo();
private:
virtual ~nsCompartmentInfo();
protected:
};
#endif

View File

@ -0,0 +1,31 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#include "nsISupports.idl"
#include "nsIArray.idl"
[scriptable, uuid(13dd4c09-ff11-4943-8dc2-d96eb69c963b)]
interface nsICompartment : nsISupports {
/* name of compartment */
readonly attribute AString compartmentName;
/* time spent executing code in this compartment in microseconds */
readonly attribute unsigned long long time;
/* the id of the addon associated with this compartment, or null */
readonly attribute AString addonId;
/* time spent processing CPOWs in microseconds */
readonly attribute unsigned long long CPOWTime;
};
[scriptable, builtinclass, uuid(5795113a-39a1-4087-ba09-98b7d07d025a)]
interface nsICompartmentInfo : nsISupports {
nsIArray getCompartments();
};
%{C++
#define NS_COMPARTMENT_INFO_CID \
{ 0x2d3c2f2d, 0x698d, 0x471d, \
{ 0xba, 0x3e, 0x14, 0x44, 0xdd, 0x52, 0x1e, 0x29 } }
%}

View File

@ -3,7 +3,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
toolkit.jar:
+ content/global/aboutCompartments.xhtml (content/aboutCompartments.xhtml)
+ content/global/aboutMemory.js (content/aboutMemory.js)
+ content/global/aboutMemory.xhtml (content/aboutMemory.xhtml)
+ content/global/aboutMemory.css (content/aboutMemory.css)

View File

@ -496,11 +496,21 @@ let RemoteAddonsChild = {
_ready: false,
makeReady: function() {
Prefetcher.init();
NotificationTracker.init();
ContentPolicyChild.init();
AboutProtocolChild.init();
ObserverChild.init();
let shims = [
Prefetcher,
NotificationTracker,
ContentPolicyChild,
AboutProtocolChild,
ObserverChild,
];
for (let shim of shims) {
try {
shim.init();
} catch(e) {
Cu.reportError(e);
}
}
},
init: function(global) {
@ -520,7 +530,11 @@ let RemoteAddonsChild = {
uninit: function(perTabShims) {
for (let shim of perTabShims) {
shim.uninit();
try {
shim.uninit();
} catch(e) {
Cu.reportError(e);
}
}
},
};

View File

@ -10,6 +10,7 @@ if CONFIG['MOZ_ENABLE_XREMOTE']:
DIRS += [
'aboutcache',
'aboutcompartments',
'aboutmemory',
'addoncompat',
'alerts',

View File

@ -140,7 +140,7 @@ exports.items = [
let dirName = prefBranch.getComplexValue(PREF_DIR,
Ci.nsISupportsString).data.trim();
return gcli.lookupFormat("cmdStatus2", [ dirName ]);
return gcli.lookupFormat("cmdStatus3", [ dirName ]);
}
},
{

View File

@ -45,18 +45,32 @@ const NODE_ROUTING_METHODS = [
const NODE_PROPERTIES = {
"OscillatorNode": {
"type": {},
"frequency": {},
"detune": {}
"frequency": {
"param": true
},
"detune": {
"param": true
}
},
"GainNode": {
"gain": {}
"gain": {
"param": true
}
},
"DelayNode": {
"delayTime": {}
"delayTime": {
"param": true
}
},
// TODO deal with figuring out adding `detune` AudioParam
// for AudioBufferSourceNode, which is in the spec
// but not yet added in implementation
// bug 1116852
"AudioBufferSourceNode": {
"buffer": { "Buffer": true },
"playbackRate": {},
"playbackRate": {
"param": true,
},
"loop": {},
"loopStart": {},
"loopEnd": {}
@ -79,19 +93,37 @@ const NODE_PROPERTIES = {
"normalize": {},
},
"DynamicsCompressorNode": {
"threshold": {},
"knee": {},
"ratio": {},
"threshold": {
"param": true
},
"knee": {
"param": true
},
"ratio": {
"param": true
},
"reduction": {},
"attack": {},
"release": {}
"attack": {
"param": true
},
"release": {
"param": true
}
},
"BiquadFilterNode": {
"type": {},
"frequency": {},
"Q": {},
"detune": {},
"gain": {}
"frequency": {
"param": true
},
"Q": {
"param": true
},
"detune": {
"param": true
},
"gain": {
"param": true
}
},
"WaveShaperNode": {
"curve": { "Float32Array": true },
@ -401,14 +433,14 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
getAutomationData: method(function (paramName) {
let timeline = this.automation[paramName];
let events = timeline.events;
let values = [];
let i = 0;
if (!timeline) {
return null;
}
let events = timeline.events;
let values = [];
let i = 0;
if (!timeline.events.length) {
return { events, values };
}
@ -421,9 +453,9 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
let scale = timeDelta / AUTOMATION_GRANULARITY;
for (; i < AUTOMATION_GRANULARITY; i++) {
let t = firstEvent.time + (i * scale);
let value = timeline.getValueAtTime(t);
values.push({ t, value });
let delta = firstEvent.time + (i * scale);
let value = timeline.getValueAtTime(delta);
values.push({ delta, value });
}
// If the last event is setTargetAtTime, the automation
@ -432,9 +464,9 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
// until we're "close enough" to the target.
if (lastEvent.type === "setTargetAtTime") {
for (; i < AUTOMATION_GRANULARITY_MAX; i++) {
let t = firstEvent.time + (++i * scale);
let value = timeline.getValueAtTime(t);
values.push({ t, value });
let delta = firstEvent.time + (++i * scale);
let value = timeline.getValueAtTime(delta);
values.push({ delta, value });
}
}

View File

@ -4,6 +4,8 @@
"use strict";
let { Cu } = require("chrome");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let Services = require("Services");
let promise = require("devtools/toolkit/deprecated-sync-thenables");
let {Class} = require("sdk/core/heritage");
@ -1148,7 +1150,7 @@ let Front = Class({
this.actor().then(actorID => {
packet.to = actorID;
this.conn._transport.send(packet);
});
}).then(null, e => DevToolsUtils.reportException("Front.prototype.send", e));
}
},

View File

@ -18,12 +18,18 @@ function test() {
let principal = document.getElementById("content").contentDocument.defaultView.document.nodePrincipal;
is(DOMApplicationRegistry.getAppLocalIdByManifestURL(app.manifestURL), principal.appId, "Principal app ID correct");
let alwaysAllowed = ["indexedDB"]
// Check if all the permissions of the app are unknown.
for (let permName of AllPossiblePermissions) {
// Get the value for the permission.
let permValue = Services.perms.testExactPermissionFromPrincipal(principal, permName);
is(permValue, Ci.nsIPermissionManager.UNKNOWN_ACTION, "Permission " + permName + " unknown.");
if (alwaysAllowed.includes(permName)) {
is(permValue, Ci.nsIPermissionManager.ALLOW_ACTION, "Permission " + permName + " allowed.");
} else {
is(permValue, Ci.nsIPermissionManager.UNKNOWN_ACTION, "Permission " + permName + " unknown.");
}
}
finish();

View File

@ -50,6 +50,8 @@
#include "nsThreadManager.h"
#include "nsThreadPool.h"
#include "nsCompartmentInfo.h"
#include "xptinfo.h"
#include "nsIInterfaceInfoManager.h"
#include "xptiprivate.h"
@ -234,6 +236,8 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsMemoryInfoDumper)
NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsStatusReporterManager, Init)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsCompartmentInfo)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsIOUtil)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsSecurityConsoleMessage)

View File

@ -82,3 +82,4 @@
COMPONENT(CYCLE_COLLECTOR_LOGGER, nsCycleCollectorLoggerConstructor)
COMPONENT(MESSAGE_LOOP, nsMessageLoopConstructor)
COMPONENT(STATUS_REPORTER_MANAGER, nsStatusReporterManagerConstructor)
COMPONENT(COMPARTMENT_INFO, nsCompartmentInfoConstructor)

View File

@ -90,6 +90,8 @@
*/
#define NS_MESSAGE_LOOP_CONTRACTID "@mozilla.org/message-loop;1"
#define NS_COMPARTMENT_INFO_CONTRACTID "@mozilla.org/compartment-info;1"
/**
* The following are the CIDs and Contract IDs of the nsISupports wrappers for
* primative types.