mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-02 07:05:24 +00:00
370 lines
12 KiB
JavaScript
370 lines
12 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
/**
|
|
* Functions handling the recordings UI.
|
|
*/
|
|
let RecordingsListView = 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._onClearButtonClick = this._onClearButtonClick.bind(this);
|
|
this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
|
|
this._onImportButtonClick = this._onImportButtonClick.bind(this);
|
|
this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
|
|
|
|
this.emptyText = L10N.getStr("noRecordingsText");
|
|
this.widget.addEventListener("select", this._onSelect, false);
|
|
},
|
|
|
|
/**
|
|
* Destruction function, called when the tool is closed.
|
|
*/
|
|
destroy: function() {
|
|
this.widget.removeEventListener("select", this._onSelect, false);
|
|
},
|
|
|
|
/**
|
|
* Adds an empty recording to this container.
|
|
*
|
|
* @param string profileLabel [optional]
|
|
* A custom label for the newly created recording item.
|
|
*/
|
|
addEmptyRecording: function(profileLabel) {
|
|
let titleNode = document.createElement("label");
|
|
titleNode.className = "plain recording-item-title";
|
|
titleNode.setAttribute("value", profileLabel ||
|
|
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], {
|
|
attachment: {
|
|
// The profiler and refresh driver ticks data will be available
|
|
// as soon as recording finishes.
|
|
profilerData: { profileLabel },
|
|
ticksData: null
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Signals that a recording session has started.
|
|
*
|
|
* @param string profileLabel
|
|
* The provided string argument if available, undefined otherwise.
|
|
*/
|
|
handleRecordingStarted: function(profileLabel) {
|
|
// 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 (profileLabel) {
|
|
recordingItem = this.getItemForAttachment(e =>
|
|
e.profilerData.profileLabel == profileLabel);
|
|
}
|
|
// Otherwise, create a new empty recording item.
|
|
if (!recordingItem) {
|
|
recordingItem = this.addEmptyRecording(profileLabel);
|
|
}
|
|
|
|
// Mark the corresponding item as being a "record in progress".
|
|
recordingItem.isRecording = true;
|
|
|
|
// If this is the first item, immediately select it.
|
|
if (this.itemCount == 1) {
|
|
this.selectedItem = recordingItem;
|
|
}
|
|
|
|
window.emit(EVENTS.RECORDING_STARTED, profileLabel);
|
|
},
|
|
|
|
/**
|
|
* Signals that a recording session has ended.
|
|
*
|
|
* @param object recordingData
|
|
* The profiler and refresh driver ticks data received from the front.
|
|
*/
|
|
handleRecordingEnded: function(recordingData) {
|
|
let profileLabel = recordingData.profilerData.profileLabel;
|
|
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;
|
|
|
|
// Store the recording data, customize and select this recording item.
|
|
this.customizeRecording(recordingItem, recordingData);
|
|
this.forceSelect(recordingItem);
|
|
|
|
window.emit(EVENTS.RECORDING_ENDED, recordingData);
|
|
},
|
|
|
|
/**
|
|
* Signals that a recording session has ended abruptly and the accumulated
|
|
* data should be discarded.
|
|
*/
|
|
handleRecordingCancelled: Task.async(function*() {
|
|
if ($("#record-button").hasAttribute("checked")) {
|
|
$("#record-button").removeAttribute("checked");
|
|
yield gFront.cancelRecording();
|
|
}
|
|
ProfileView.showEmptyNotice();
|
|
|
|
window.emit(EVENTS.RECORDING_LOST);
|
|
}),
|
|
|
|
/**
|
|
* Adds recording data to a recording item in this container.
|
|
*
|
|
* @param Item recordingItem
|
|
* An item inserted via `RecordingsListView.addEmptyRecording`.
|
|
* @param object recordingData
|
|
* The profiler and refresh driver ticks data received from the front.
|
|
*/
|
|
customizeRecording: function(recordingItem, recordingData) {
|
|
recordingItem.attachment = recordingData;
|
|
|
|
let saveNode = $(".recording-item-save", recordingItem.target);
|
|
saveNode.setAttribute("value",
|
|
L10N.getStr("recordingsList.saveLabel"));
|
|
|
|
let durationMillis = recordingData.recordingDuration;
|
|
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 }) {
|
|
if (!recordingItem) {
|
|
ProfileView.showEmptyNotice();
|
|
return;
|
|
}
|
|
if (recordingItem.isRecording) {
|
|
ProfileView.showRecordingNotice();
|
|
return;
|
|
}
|
|
|
|
ProfileView.showLoadingNotice();
|
|
ProfileView.removeAllTabs();
|
|
|
|
let recordingData = recordingItem.attachment;
|
|
let durationMillis = recordingData.recordingDuration;
|
|
yield ProfileView.addTabAndPopulate(recordingData, 0, durationMillis);
|
|
ProfileView.showTabbedBrowser();
|
|
|
|
// Only clear the checked state if there's nothing recording.
|
|
if (!this.getItemForPredicate(e => e.isRecording)) {
|
|
$("#record-button").removeAttribute("checked");
|
|
}
|
|
|
|
// But don't leave it locked in any case.
|
|
$("#record-button").removeAttribute("locked");
|
|
|
|
window.emit(EVENTS.RECORDING_DISPLAYED);
|
|
}),
|
|
|
|
/**
|
|
* The click listener for the "clear" button in this container.
|
|
*/
|
|
_onClearButtonClick: Task.async(function*() {
|
|
this.empty();
|
|
yield this.handleRecordingCancelled();
|
|
}),
|
|
|
|
/**
|
|
* The click listener for the "record" button in this container.
|
|
*/
|
|
_onRecordButtonClick: Task.async(function*() {
|
|
if (!$("#record-button").hasAttribute("checked")) {
|
|
$("#record-button").setAttribute("checked", "true");
|
|
yield gFront.startRecording();
|
|
this.handleRecordingStarted();
|
|
} else {
|
|
$("#record-button").setAttribute("locked", "");
|
|
let recordingData = yield gFront.stopRecording();
|
|
this.handleRecordingEnded(recordingData);
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* The click listener for the "import" button in this container.
|
|
*/
|
|
_onImportButtonClick: Task.async(function*() {
|
|
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
|
fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
|
|
fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
|
|
fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
|
|
|
|
if (fp.show() == Ci.nsIFilePicker.returnOK) {
|
|
loadRecordingFromFile(fp.file);
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* 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);
|
|
saveRecordingToFile(recordingItem, fp.file);
|
|
}});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
|
|
* @return object
|
|
*/
|
|
function getUnicodeConverter() {
|
|
let className = "@mozilla.org/intl/scriptableunicodeconverter";
|
|
let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
|
|
converter.charset = "UTF-8";
|
|
return converter;
|
|
}
|
|
|
|
/**
|
|
* Saves a recording as JSON to a file. The provided data is assumed to be
|
|
* acyclical, so that it can be properly serialized.
|
|
*
|
|
* @param Item recordingItem
|
|
* The recording item containing the data to stream as JSON.
|
|
* @param nsILocalFile file
|
|
* The file to stream the data into.
|
|
* @return object
|
|
* A promise that is resolved once streaming finishes, or rejected
|
|
* if there was an error.
|
|
*/
|
|
function saveRecordingToFile(recordingItem, file) {
|
|
let deferred = promise.defer();
|
|
|
|
let recordingData = recordingItem.attachment;
|
|
recordingData.fileType = PROFILE_SERIALIZER_IDENTIFIER;
|
|
recordingData.version = PROFILE_SERIALIZER_VERSION;
|
|
|
|
let string = JSON.stringify(recordingData);
|
|
let inputStream = getUnicodeConverter().convertToInputStream(string);
|
|
let outputStream = FileUtils.openSafeFileOutputStream(file);
|
|
|
|
NetUtil.asyncCopy(inputStream, outputStream, status => {
|
|
if (!Components.isSuccessCode(status)) {
|
|
deferred.reject(new Error("Could not save recording data file."));
|
|
}
|
|
deferred.resolve();
|
|
});
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* Loads a recording stored as JSON from a file.
|
|
*
|
|
* @param nsILocalFile file
|
|
* The file to import the data from.
|
|
* @return object
|
|
* A promise that is resolved once importing finishes, or rejected
|
|
* if there was an error.
|
|
*/
|
|
function loadRecordingFromFile(file) {
|
|
let deferred = promise.defer();
|
|
|
|
let channel = NetUtil.newChannel2(file,
|
|
null,
|
|
null,
|
|
window.document,
|
|
null, // aLoadingPrincipal
|
|
null, // aTriggeringPrincipal
|
|
Ci.nsILoadInfo.SEC_NORMAL,
|
|
Ci.nsIContentPolicy.TYPE_OTHER);
|
|
channel.contentType = "text/plain";
|
|
|
|
NetUtil.asyncFetch2(channel, (inputStream, status) => {
|
|
if (!Components.isSuccessCode(status)) {
|
|
deferred.reject(new Error("Could not import recording data file."));
|
|
return;
|
|
}
|
|
try {
|
|
let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
|
|
var recordingData = JSON.parse(string);
|
|
} catch (e) {
|
|
deferred.reject(new Error("Could not read recording data file."));
|
|
return;
|
|
}
|
|
if (recordingData.fileType != PROFILE_SERIALIZER_IDENTIFIER) {
|
|
deferred.reject(new Error("Unrecognized recording data file."));
|
|
return;
|
|
}
|
|
|
|
let profileLabel = recordingData.profilerData.profileLabel;
|
|
let recordingItem = RecordingsListView.addEmptyRecording(profileLabel);
|
|
RecordingsListView.customizeRecording(recordingItem, recordingData);
|
|
|
|
// If this is the first item, immediately select it.
|
|
if (RecordingsListView.itemCount == 1) {
|
|
RecordingsListView.selectedItem = recordingItem;
|
|
}
|
|
|
|
deferred.resolve();
|
|
});
|
|
|
|
return deferred.promise;
|
|
}
|