Merge m-c to inbound.

This commit is contained in:
Ryan VanderMeulen 2013-12-18 21:28:22 -05:00
commit 9b0cd72e73
138 changed files with 4174 additions and 1029 deletions

View File

@ -1,4 +1,4 @@
{
"revision": "7412c36923b59f6e4d7076de5be7e6ded6ddc586",
"revision": "c6aeaa41977d9410b9baac23ccea6909f796fd1f",
"repo_path": "/integration/gaia-central"
}

View File

@ -1121,6 +1121,7 @@ pref("devtools.debugger.pause-on-exceptions", false);
pref("devtools.debugger.ignore-caught-exceptions", true);
pref("devtools.debugger.source-maps-enabled", true);
pref("devtools.debugger.pretty-print-enabled", true);
pref("devtools.debugger.tracer", false);
// The default Debugger UI settings
pref("devtools.debugger.ui.panes-sources-width", 200);
@ -1304,8 +1305,6 @@ pref("social.manifest.facebook", "{\"origin\":\"https://www.facebook.com\",\"nam
pref("social.sidebar.open", true);
pref("social.sidebar.unload_timeout_ms", 10000);
pref("social.allowMultipleWorkers", true);
pref("dom.identity.enabled", false);
// Turn on the CSP 1.0 parser for Content Security Policy headers

View File

@ -1031,14 +1031,10 @@ let SocialStatusWidgetListener = {
SocialStatus = {
populateToolbarPalette: function() {
if (!Social.allowMultipleWorkers)
return;
this._toolbarHelper.populatePalette();
},
removeProvider: function(origin) {
if (!Social.allowMultipleWorkers)
return;
this._removeFrame(origin);
this._toolbarHelper.removeProviderButton(origin);
},
@ -1124,8 +1120,6 @@ SocialStatus = {
},
updateButton: function(origin) {
if (!Social.allowMultipleWorkers)
return;
let id = this._toolbarHelper.idFromOrigin(origin);
let widget = CustomizableUI.getWidget(id);
if (!widget)
@ -1165,8 +1159,6 @@ SocialStatus = {
},
showPopup: function(aToolbarButton) {
if (!Social.allowMultipleWorkers)
return;
// attach our notification panel if necessary
let origin = aToolbarButton.getAttribute("origin");
let provider = Social._getProviderFromOrigin(origin);

View File

@ -747,18 +747,6 @@ toolbarpaletteitem[place="palette"] > toolbarbutton[type="badged"] > .toolbarbut
max-height: 32px;
}
@media (min-resolution: 2dppx) {
toolbarbutton[type="badged"] > .toolbarbutton-badge-container > .toolbarbutton-icon,
toolbarbutton[type="socialmark"] > .toolbarbutton-icon {
max-width: 32px;
max-height: 32px;
}
toolbarpaletteitem[place="palette"] > toolbarbutton[type="badged"] > .toolbarbutton-badge-container > .toolbarbutton-icon {
max-width: 64px;
max-height: 64px;
}
}
panelview > .social-panel-frame {
width: auto;
height: auto;

View File

@ -2474,8 +2474,15 @@ function _checkDefaultAndSwitchToMetro() {
getService(Components.interfaces.nsIAppStartup);
Services.prefs.setBoolPref('browser.sessionstore.resume_session_once', true);
appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit |
Components.interfaces.nsIAppStartup.eRestartTouchEnvironment);
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
.createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
if (!cancelQuit.data) {
appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit |
Components.interfaces.nsIAppStartup.eRestartTouchEnvironment);
}
return true;
}
return false;

View File

@ -63,15 +63,12 @@ function test() {
ok(chats.children.length == 0, "no chatty children left behind");
cb();
};
// always run chat tests with multiple workers.
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
runSocialTestWithProvider(manifests, function (finishcb) {
ok(Social.enabled, "Social is enabled");
ok(Social.providers[0].getWorkerPort(), "provider 0 has port");
ok(Social.providers[1].getWorkerPort(), "provider 1 has port");
ok(Social.providers[2].getWorkerPort(), "provider 2 has port");
runSocialTests(tests, undefined, postSubTest, function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
window.moveTo(oldleft, window.screenY)
window.resizeTo(oldwidth, window.outerHeight);
finishcb();

View File

@ -55,14 +55,12 @@ function openWindowAndWaitForInit(callback) {
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
let toolbar = document.getElementById("nav-bar");
let currentsetAtStart = toolbar.currentSet;
runSocialTestWithProvider(manifest, function () {
runSocialTests(tests, undefined, undefined, function () {
Services.prefs.clearUserPref("social.remote-install.enabled");
// just in case the tests failed, clear these here as well
Services.prefs.clearUserPref("social.allowMultipleWorkers");
Services.prefs.clearUserPref("social.whitelist");
ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish");
CustomizableUI.reset();

View File

@ -5,11 +5,9 @@
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
runSocialTestWithProvider(gProviders, function (finishcb) {
Social.enabled = true;
runSocialTests(tests, undefined, undefined, function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
finishcb();
});
});
@ -58,6 +56,7 @@ var tests = {
waitForCondition(function() messageReceived == Social.providers.length,
next, "received messages from all workers");
},
testWorkerDisabling: function(next) {
Social.enabled = false;
is(Social.providers.length, gProviders.length, "providers still available");
@ -66,37 +65,5 @@ var tests = {
ok(!p.getWorkerPort(), "worker disabled");
}
next();
},
testSingleWorkerEnabling: function(next) {
// test that only one worker is enabled when we limit workers
Services.prefs.setBoolPref("social.allowMultipleWorkers", false);
Social.enabled = true;
for (let p of Social.providers) {
if (p == Social.provider) {
ok(p.enabled, "primary provider enabled");
let port = p.getWorkerPort();
ok(port, "primary worker enabled");
port.close();
} else {
ok(!p.enabled, "secondary provider is not enabled");
ok(!p.getWorkerPort(), "secondary worker disabled");
}
}
next();
},
testMultipleWorkerEnabling: function(next) {
// test that all workers are enabled when we allow multiple workers
Social.enabled = false;
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
Social.enabled = true;
for (let p of Social.providers) {
ok(p.enabled, "provider enabled");
let port = p.getWorkerPort();
ok(port, "worker enabled");
port.close();
}
next();
}
}

View File

@ -40,12 +40,10 @@ function openWindowAndWaitForInit(callback) {
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
runSocialTestWithProvider(manifest, function (finishcb) {
runSocialTests(tests, undefined, undefined, function () {
Services.prefs.clearUserPref("social.remote-install.enabled");
// just in case the tests failed, clear these here as well
Services.prefs.clearUserPref("social.allowMultipleWorkers");
Services.prefs.clearUserPref("social.whitelist");
ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish");
CustomizableUI.reset();

View File

@ -13,7 +13,6 @@ let {getFrameWorkerHandle} = Cu.import("resource://gre/modules/FrameWorker.jsm",
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
// We need to ensure all our workers are in the same content process.
Services.prefs.setIntPref("dom.ipc.processCount", 1);
@ -22,7 +21,6 @@ function test() {
runSocialTests(tests, undefined, undefined, function() {
Services.prefs.clearUserPref("dom.ipc.processCount");
Services.prefs.clearUserPref("social.sidebar.open");
Services.prefs.clearUserPref("social.allowMultipleWorkers");
finishcb();
});
});

View File

@ -883,7 +883,7 @@ let CustomizableUIInternal = {
}
let nextNode = null;
if (aNextNodeId) {
nextNode = aAreaNode.customizationTarget.querySelector(idToSelector(aNextNodeId));
nextNode = aAreaNode.customizationTarget.getElementsByAttribute("id", aNextNodeId)[0];
}
return [aAreaNode.customizationTarget, nextNode];
},
@ -1004,7 +1004,7 @@ let CustomizableUIInternal = {
if (toolbox.palette) {
// Attempt to locate a node with a matching ID within
// the palette.
let node = toolbox.palette.querySelector(idToSelector(aId));
let node = toolbox.palette.getElementsByAttribute("id", aId)[0];
if (node) {
// Normalize the removable attribute. For backwards compat, this
// is optional if the widget is located in the toolbox palette,
@ -1881,7 +1881,7 @@ let CustomizableUIInternal = {
windowCache.delete(aWidgetId);
}
let widgetNode = window.document.getElementById(aWidgetId) ||
window.gNavToolbox.palette.querySelector(idToSelector(aWidgetId));
window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
if (widgetNode) {
widgetNode.remove();
}
@ -2014,7 +2014,7 @@ let CustomizableUIInternal = {
if (!container.length) {
return false;
}
let existingNode = container[0].querySelector(idToSelector(aWidgetId));
let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
if (existingNode) {
return true;
}
@ -2058,7 +2058,7 @@ let CustomizableUIInternal = {
// Clone the array so we don't modify the actual placements...
currentPlacements = [...currentPlacements];
currentPlacements = currentPlacements.filter((item) => {
let itemNode = container.querySelector(idToSelector(item));
let itemNode = container.getElementsByAttribute("id", item)[0];
return itemNode && removableOrDefault(itemNode || item);
});
}
@ -2972,7 +2972,7 @@ function XULWidgetGroupWrapper(aWidgetId) {
if (!instance) {
// Toolbar palettes aren't part of the document, so elements in there
// won't be found via document.getElementById().
instance = aWindow.gNavToolbox.palette.querySelector(idToSelector(aWidgetId));
instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
}
let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance);
@ -3039,6 +3039,12 @@ function OverflowableToolbar(aToolbarNode) {
this._enabled = true;
this._toolbar.setAttribute("overflowable", "true");
let doc = this._toolbar.ownerDocument;
this._target = this._toolbar.customizationTarget;
this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget"));
this._list.toolbox = this._toolbar.toolbox;
this._list.customizationTarget = this._list;
Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
}
@ -3055,12 +3061,7 @@ OverflowableToolbar.prototype = {
},
init: function() {
this._target = this._toolbar.customizationTarget;
let doc = this._toolbar.ownerDocument;
this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget"));
this._list.toolbox = this._toolbar.toolbox;
this._list.customizationTarget = this._list;
let window = doc.defaultView;
window.addEventListener("resize", this);
window.gNavToolbox.addEventListener("customizationstarting", this);
@ -3221,7 +3222,7 @@ OverflowableToolbar.prototype = {
}
let inserted = false;
for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
let beforeNode = this._target.querySelector(idToSelector(placements[beforeNodeIndex]));
let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0];
if (beforeNode) {
this._target.insertBefore(child, beforeNode);
inserted = true;
@ -3354,7 +3355,7 @@ OverflowableToolbar.prototype = {
return [this._list, null];
}
let nextNode = this._list.querySelector(idToSelector(aNextNodeId));
let nextNode = this._list.getElementsByAttribute("id", aNextNodeId)[0];
// If this is the first item, we can actually just append the node
// to the end of the toolbar. If it results in an overflow event, we'll move
// the new node to the overflow target.
@ -3372,9 +3373,4 @@ OverflowableToolbar.prototype = {
},
};
// When IDs contain special characters, we need to escape them for use with querySelector:
function idToSelector(aId) {
return "#" + aId.replace(/[ !"'#$%&\(\)*+\-,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
}
CustomizableUIInternal.initialize();

View File

@ -16,12 +16,12 @@ let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource:///modules/sessionstore/Utils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
"resource:///modules/sessionstore/PageStyle.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource:///modules/sessionstore/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
"resource:///modules/sessionstore/SessionHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
@ -29,6 +29,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
"resource:///modules/sessionstore/TextAndScrollData.jsm");
Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
let gFrameTree = new FrameTree(this);
/**
* Returns a lazy function that will evaluate the given
* function |fn| only once and cache its return value.
@ -78,7 +81,7 @@ let EventListener = {
handleEvent: function (event) {
switch (event.type) {
case "pageshow":
if (event.persisted)
if (event.persisted && event.target == content.document)
sendAsyncMessage("SessionStore:pageshow");
break;
case "input":
@ -198,6 +201,43 @@ let ProgressListener = {
Ci.nsISupportsWeakReference])
};
/**
* Listens for scroll position changes. Whenever the user scrolls the top-most
* frame we update the scroll position and will restore it when requested.
*
* Causes a SessionStore:update message to be sent that contains the current
* scroll positions as a tree of strings. If no frame of the whole frame tree
* is scrolled this will return null so that we don't tack a property onto
* the tabData object in the parent process.
*
* Example:
* {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
*/
let ScrollPositionListener = {
init: function () {
addEventListener("scroll", this);
gFrameTree.addObserver(this);
},
handleEvent: function (event) {
let frame = event.target && event.target.defaultView;
// Don't collect scroll data for frames created at or after the load event
// as SessionStore can't restore scroll data for those.
if (frame && gFrameTree.contains(frame)) {
MessageQueue.push("scroll", () => this.collect());
}
},
onFrameTreeReset: function () {
MessageQueue.push("scroll", () => null);
},
collect: function () {
return gFrameTree.map(ScrollPosition.collect);
}
};
/**
* Listens for changes to the page style. Whenever a different page style is
* selected or author styles are enabled/disabled we send a message with the
@ -308,6 +348,30 @@ let SessionStorageListener = {
Ci.nsISupportsWeakReference])
};
/**
* Listen for changes to the privacy status of the tab.
* By definition, tabs start in non-private mode.
*
* Causes a SessionStore:update message to be sent for
* field "isPrivate". This message contains
* |true| if the tab is now private
* |null| if the tab is now public - the field is therefore
* not saved.
*/
let PrivacyListener = {
init: function() {
docShell.addWeakPrivacyTransitionObserver(this);
},
// Ci.nsIPrivacyTransitionObserver
privateModeChanged: function(enabled) {
MessageQueue.push("isPrivate", () => enabled || null);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver,
Ci.nsISupportsWeakReference])
};
/**
* A message queue that takes collected data and will take care of sending it
* to the chrome process. It allows flushing using synchronous messages and
@ -399,6 +463,8 @@ let MessageQueue = {
// request.
let sendMessage = sync ? sendRpcMessage : sendAsyncMessage;
let durationMs = Date.now();
let data = {};
for (let [key, id] of this._lastUpdated) {
// There is no data for the given key anymore because
@ -418,8 +484,17 @@ let MessageQueue = {
data[key] = this._data.get(key)();
}
durationMs = Date.now() - durationMs;
let telemetry = {
FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs
}
// Send all data to the parent process.
sendMessage("SessionStore:update", {id: this._id, data: data});
sendMessage("SessionStore:update", {
id: this._id,
data: data,
telemetry: telemetry
});
// Increase our unique message ID.
this._id++;
@ -465,4 +540,6 @@ SyncHandler.init();
ProgressListener.init();
PageStyleListener.init();
SessionStorageListener.init();
ScrollPositionListener.init();
DocShellCapabilitiesListener.init();
PrivacyListener.init();

View File

@ -0,0 +1,217 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FrameTree"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
const EXPORTED_METHODS = ["addObserver", "contains", "map"];
/**
* A FrameTree represents all frames that were reachable when the document
* was loaded. We use this information to ignore frames when collecting
* sessionstore data as we can't currently restore anything for frames that
* have been created dynamically after or at the load event.
*
* @constructor
*/
function FrameTree(chromeGlobal) {
let internal = new FrameTreeInternal(chromeGlobal);
let external = {};
for (let method of EXPORTED_METHODS) {
external[method] = internal[method].bind(internal);
}
return Object.freeze(external);
}
/**
* The internal frame tree API that the public one points to.
*
* @constructor
*/
function FrameTreeInternal(chromeGlobal) {
// A WeakMap that uses frames (DOMWindows) as keys and their initial indices
// in their parents' child lists as values. Suppose we have a root frame with
// three subframes i.e. a page with three iframes. The WeakMap would have
// four entries and look as follows:
//
// root -> 0
// subframe1 -> 0
// subframe2 -> 1
// subframe3 -> 2
//
// Should one of the subframes disappear we will stop collecting data for it
// as |this._frames.has(frame) == false|. All other subframes will maintain
// their initial indices to ensure we can restore frame data appropriately.
this._frames = new WeakMap();
// The Set of observers that will be notified when the frame changes.
this._observers = new Set();
// The chrome global we use to retrieve the current DOMWindow.
this._chromeGlobal = chromeGlobal;
// Register a web progress listener to be notified about new page loads.
let docShell = chromeGlobal.docShell;
let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
}
FrameTreeInternal.prototype = {
// Returns the docShell's current global.
get content() {
return this._chromeGlobal.content;
},
/**
* Adds a given observer |obs| to the set of observers that will be notified
* when the frame tree is reset (when a new document starts loading) or
* recollected (when a document finishes loading).
*
* @param obs (object)
*/
addObserver: function (obs) {
this._observers.add(obs);
},
/**
* Notifies all observers that implement the given |method|.
*
* @param method (string)
*/
notifyObservers: function (method) {
for (let obs of this._observers) {
if (obs.hasOwnProperty(method)) {
obs[method]();
}
}
},
/**
* Checks whether a given |frame| is contained in the collected frame tree.
* If it is not, this indicates that we should not collect data for it.
*
* @param frame (nsIDOMWindow)
* @return bool
*/
contains: function (frame) {
return this._frames.has(frame);
},
/**
* Recursively applies the given function |cb| to the stored frame tree. Use
* this method to collect sessionstore data for all reachable frames stored
* in the frame tree.
*
* If a given function |cb| returns a value, it must be an object. It may
* however return "null" to indicate that there is no data to be stored for
* the given frame.
*
* The object returned by |cb| cannot have any property named "children" as
* that is used to store information about subframes in the tree returned
* by |map()| and might be overridden.
*
* @param cb (function)
* @return object
*/
map: function (cb) {
let frames = this._frames;
function walk(frame) {
let obj = cb(frame) || {};
if (frames.has(frame)) {
let children = [];
Array.forEach(frame.frames, subframe => {
// Don't collect any data if the frame is not contained in the
// initial frame tree. It's a dynamic frame added later.
if (!frames.has(subframe)) {
return;
}
// Retrieve the frame's original position in its parent's child list.
let index = frames.get(subframe);
// Recursively collect data for the current subframe.
let result = walk(subframe, cb);
if (result && Object.keys(result).length) {
children[index] = result;
}
});
if (children.length) {
obj.children = children;
}
}
return Object.keys(obj).length ? obj : null;
}
return walk(this.content);
},
/**
* Stores a given |frame| and its children in the frame tree.
*
* @param frame (nsIDOMWindow)
* @param index (int)
* The index in the given frame's parent's child list.
*/
collect: function (frame, index = 0) {
// Mark the given frame as contained in the frame tree.
this._frames.set(frame, index);
// Mark the given frame's subframes as contained in the tree.
Array.forEach(frame.frames, this.collect, this);
},
/**
* @see nsIWebProgressListener.onStateChange
*
* We want to be notified about:
* - new documents that start loading to clear the current frame tree;
* - completed document loads to recollect reachable frames.
*/
onStateChange: function (webProgress, request, stateFlags, status) {
// Ignore state changes for subframes because we're only interested in the
// top-document starting or stopping its load. We thus only care about any
// changes to the root of the frame tree, not to any of its nodes/leafs.
if (!webProgress.isTopLevel || webProgress.DOMWindow != this.content) {
return;
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
// Clear the list of frames until we can recollect it.
this._frames.clear();
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeReset");
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
// The document and its resources have finished loading.
this.collect(webProgress.DOMWindow);
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeCollected");
}
},
// Unused nsIWebProgressListener methods.
onLocationChange: function () {},
onProgressChange: function () {},
onSecurityChange: function () {},
onStatusChange: function () {},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};

View File

@ -0,0 +1,90 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["ScrollPosition"];
const Ci = Components.interfaces;
/**
* It provides methods to collect and restore scroll positions for single
* frames and frame trees.
*
* This is a child process module.
*/
this.ScrollPosition = Object.freeze({
/**
* Collects scroll position data for any given |frame| in the frame hierarchy.
*
* @param frame (DOMWindow)
*
* @return {scroll: "x,y"} e.g. {scroll: "100,200"}
* Returns null when there is no scroll data we want to store for the
* given |frame|.
*/
collect: function (frame) {
let ifreq = frame.QueryInterface(Ci.nsIInterfaceRequestor);
let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {};
utils.getScrollXY(false /* no layout flush */, scrollX, scrollY);
if (scrollX.value || scrollY.value) {
return {scroll: scrollX.value + "," + scrollY.value};
}
return null;
},
/**
* Restores scroll position data for any given |frame| in the frame hierarchy.
*
* @param frame (DOMWindow)
* @param value (object, see collect())
*/
restore: function (frame, value) {
let match;
if (value && (match = /(\d+),(\d+)/.exec(value))) {
frame.scrollTo(match[1], match[2]);
}
},
/**
* Restores scroll position data for the current frame hierarchy starting at
* |root| using the given scroll position |data|.
*
* If the given |root| frame's hierarchy doesn't match that of the given
* |data| object we will silently discard data for unreachable frames. We
* may as well assign scroll positions to the wrong frames if some were
* reordered or removed.
*
* @param root (DOMWindow)
* @param data (object)
* {
* scroll: "100,200",
* children: [
* {scroll: "100,200"},
* null,
* {scroll: "200,300", children: [ ... ]}
* ]
* }
*/
restoreTree: function (root, data) {
if (data.hasOwnProperty("scroll")) {
this.restore(root, data.scroll);
}
if (!data.hasOwnProperty("children")) {
return;
}
let frames = root.frames;
data.children.forEach((child, index) => {
if (child && index < frames.length) {
this.restoreTree(frames[index], child);
}
});
}
});

View File

@ -72,7 +72,7 @@ this.SessionSaver = Object.freeze({
* Immediately saves the current session to disk.
*/
run: function () {
SessionSaverInternal.run();
return SessionSaverInternal.run();
},
/**
@ -129,7 +129,7 @@ let SessionSaverInternal = {
* Immediately saves the current session to disk.
*/
run: function () {
this._saveState(true /* force-update all windows */);
return this._saveState(true /* force-update all windows */);
},
/**
@ -192,12 +192,24 @@ let SessionSaverInternal = {
stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
let state = SessionStore.getCurrentState(forceUpdateAllWindows);
// Forget about private windows.
// Forget about private windows and tabs.
for (let i = state.windows.length - 1; i >= 0; i--) {
if (state.windows[i].isPrivate) {
state.windows.splice(i, 1);
if (state.selectedWindow >= i) {
state.selectedWindow--;
let win = state.windows[i];
if (win.isPrivate || false) { // The whole window is private, remove it
state.windows.splice(i, 1);
if (state.selectedWindow >= i) {
state.selectedWindow--;
}
continue;
}
// The window is not private, but its tabs still might
for (let j = win.tabs.length - 1; j >= 0 ; --j) {
let tab = win.tabs[j];
if (tab.isPrivate || false) {
win.tabs.splice(j, 1);
if (win.selected >= j) {
win.selected--;
}
}
}
}
@ -209,6 +221,10 @@ let SessionSaverInternal = {
}
}
// Note that closed private tabs are never stored (see
// SessionStoreInternal.onTabClose), so we do not need to remove
// them.
// Make sure that we keep the previous session if we started with a single
// private window and no non-private windows have been opened, yet.
if (state.deferredInitialState) {
@ -235,7 +251,7 @@ let SessionSaverInternal = {
#endif
stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
this._writeState(state);
return this._writeState(state);
},
/**
@ -278,7 +294,7 @@ let SessionSaverInternal = {
// Don't touch the file if an observer has deleted all state data.
if (!data) {
return;
return Promise.resolve();
}
// We update the time stamp before writing so that we don't write again
@ -290,7 +306,7 @@ let SessionSaverInternal = {
// Write (atomically) to a session file, using a tmp file. Once the session
// file is successfully updated, save the time stamp of the last save and
// notify the observers.
SessionFile.write(data).then(() => {
return SessionFile.write(data).then(() => {
this.updateLastSaveTime();
notify(null, "sessionstore-state-write-complete");
}, Cu.reportError);

View File

@ -99,6 +99,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
"@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
"@mozilla.org/base/telemetry;1", "nsITelemetry");
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
@ -110,6 +112,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
"resource:///modules/devtools/scratchpad-manager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource:///modules/sessionstore/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
"resource:///modules/sessionstore/SessionSaver.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
@ -606,6 +610,7 @@ let SessionStoreInternal = {
TabState.setSyncHandler(browser, aMessage.objects.handler);
break;
case "SessionStore:update":
this.recordTelemetry(aMessage.data.telemetry);
TabState.update(browser, aMessage.data);
this.saveStateDelayed(win);
break;
@ -617,6 +622,18 @@ let SessionStoreInternal = {
this._clearRestoringWindows();
},
/**
* Record telemetry measurements stored in an object.
* @param telemetry
* {histogramID: value, ...} An object mapping histogramIDs to the
* value to be recorded for that ID,
*/
recordTelemetry: function (telemetry) {
for (let histogramId in telemetry){
Telemetry.getHistogramById(histogramId).add(telemetry[histogramId]);
}
},
/* ........ Window Event Handlers .............. */
/**
@ -630,14 +647,17 @@ let SessionStoreInternal = {
let browser;
switch (aEvent.type) {
case "load":
// If __SS_restore_data is set, then we need to restore the document
// (form data, scrolling, etc.). This will only happen when a tab is
// first restored.
browser = aEvent.currentTarget;
TabStateCache.delete(browser);
if (browser.__SS_restore_data)
this.restoreDocument(win, browser, aEvent);
this.onTabLoad(win, browser);
// Ignore load events from subframes.
if (aEvent.target == browser.contentDocument) {
// If __SS_restore_data is set, then we need to restore the document
// (form data, scrolling, etc.). This will only happen when a tab is
// first restored.
TabStateCache.delete(browser);
if (browser.__SS_restore_data)
this.restoreDocument(win, browser, aEvent);
this.onTabLoad(win, browser);
}
break;
case "SwapDocShells":
browser = aEvent.currentTarget;
@ -1311,6 +1331,11 @@ let SessionStoreInternal = {
// Get the latest data for this tab (generally, from the cache)
let tabState = TabState.collectSync(aTab);
// Don't save private tabs
if (tabState.isPrivate || false) {
return;
}
// store closed-tab data for undo
if (this._shouldSaveTabState(tabState)) {
let tabTitle = aTab.label;
@ -2649,6 +2674,7 @@ let SessionStoreInternal = {
// Update the persistent tab state cache with |tabData| information.
TabStateCache.updatePersistent(browser, {
scroll: tabData.scroll || null,
storage: tabData.storage || null,
disallow: tabData.disallow || null,
pageStyle: tabData.pageStyle || null
@ -2816,8 +2842,15 @@ let SessionStoreInternal = {
// restore those aspects of the currently active documents which are not
// preserved in the plain history entries (mainly scroll state and text data)
browser.__SS_restore_data = tabData.entries[activeIndex] || {};
browser.__SS_restore_pageStyle = tabData.pageStyle || "";
browser.__SS_restore_tab = aTab;
if (tabData.pageStyle) {
RestoreData.set(browser, "pageStyle", tabData.pageStyle);
}
if (tabData.scroll) {
RestoreData.set(browser, "scroll", tabData.scroll);
}
didStartLoad = true;
try {
// In order to work around certain issues in session history, we need to
@ -2832,7 +2865,6 @@ let SessionStoreInternal = {
}
} else {
browser.__SS_restore_data = {};
browser.__SS_restore_pageStyle = "";
browser.__SS_restore_tab = aTab;
browser.loadURIWithFlags("about:blank",
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
@ -2944,7 +2976,11 @@ let SessionStoreInternal = {
}
let frameList = this.getFramesToRestore(aBrowser);
PageStyle.restore(aBrowser.docShell, frameList, aBrowser.__SS_restore_pageStyle);
let pageStyle = RestoreData.get(aBrowser, "pageStyle") || "";
let scrollPositions = RestoreData.get(aBrowser, "scroll") || {};
PageStyle.restore(aBrowser.docShell, frameList, pageStyle);
ScrollPosition.restoreTree(aBrowser.contentWindow, scrollPositions);
TextAndScrollData.restore(frameList);
let tab = aBrowser.__SS_restore_tab;
@ -2953,8 +2989,8 @@ let SessionStoreInternal = {
// done with that now.
delete aBrowser.__SS_data;
delete aBrowser.__SS_restore_data;
delete aBrowser.__SS_restore_pageStyle;
delete aBrowser.__SS_restore_tab;
RestoreData.clear(aBrowser);
// Notify the tabbrowser that this document has been completely
// restored. Do so after restoration is completely finished and
@ -4122,4 +4158,32 @@ let GlobalState = {
setFromState: function (aState) {
this.state = (aState && aState.global) || {};
}
}
};
/**
* Keeps track of data that needs to be restored after the tab's document
* has been loaded. This includes scroll positions, form data, and page style.
*/
let RestoreData = {
_data: new WeakMap(),
get: function (browser, key) {
if (!this._data.has(browser)) {
return null;
}
return this._data.get(browser).get(key);
},
set: function (browser, key, value) {
if (!this._data.has(browser)) {
this._data.set(browser, new Map());
}
this._data.get(browser).set(key, value);
},
clear: function (browser) {
this._data.delete(browser);
}
};

View File

@ -16,6 +16,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
"resource:///modules/sessionstore/DocumentUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource:///modules/sessionstore/PrivacyLevel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource:///modules/sessionstore/ScrollPosition.jsm");
/**
* The external API exported by this module.
@ -78,14 +80,6 @@ let TextAndScrollDataInternal = {
entry.innerHTML = content.document.body.innerHTML;
}
}
// get scroll position from nsIDOMWindowUtils, since it allows avoiding a
// flush of layout
let domWindowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {};
domWindowUtils.getScrollXY(false, scrollX, scrollY);
entry.scroll = scrollX.value + "," + scrollY.value;
},
isAboutSessionRestore: function (url) {
@ -146,9 +140,6 @@ let TextAndScrollDataInternal = {
}, 0);
}
let match;
if (data.scroll && (match = /(\d+),(\d+)/.exec(data.scroll)) != null) {
content.scrollTo(match[1], match[2]);
}
ScrollPosition.restore(content, data.scroll || "");
},
};

View File

@ -15,10 +15,12 @@ JS_MODULES_PATH = 'modules/sessionstore'
EXTRA_JS_MODULES = [
'DocShellCapabilities.jsm',
'DocumentUtils.jsm',
'FrameTree.jsm',
'Messenger.jsm',
'PageStyle.jsm',
'PrivacyLevel.jsm',
'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
'ScrollPosition.jsm',
'SessionCookies.jsm',
'SessionFile.jsm',
'SessionHistory.jsm',

View File

@ -11,11 +11,15 @@
support-files =
head.js
content.js
browser_frametree_sample.html
browser_frametree_sample_frameset.html
browser_form_restore_events_sample.html
browser_formdata_format_sample.html
browser_input_sample.html
browser_pageStyle_sample.html
browser_pageStyle_sample_nested.html
browser_scrollPositions_sample.html
browser_scrollPositions_sample_frameset.html
browser_248970_b_sample.html
browser_339445_sample.html
browser_346337_sample.html
@ -54,11 +58,14 @@ support-files =
[browser_dying_cache.js]
[browser_form_restore_events.js]
[browser_formdata_format.js]
[browser_frametree.js]
[browser_global_store.js]
[browser_input.js]
[browser_merge_closed_tabs.js]
[browser_pageshow.js]
[browser_pageStyle.js]
[browser_privatetabs.js]
[browser_scrollPositions.js]
[browser_sessionStorage.js]
[browser_swapDocShells.js]
[browser_tabStateCache.js]
@ -95,7 +102,6 @@ skip-if = true
[browser_477657.js]
[browser_480148.js]
[browser_480893.js]
[browser_483330.js]
[browser_485482.js]
[browser_485563.js]
[browser_490040.js]

View File

@ -34,9 +34,7 @@ function test() {
EventUtils.sendMouseEvent({type: "click"}, chk);
let browser = newWin.gBrowser.selectedBrowser;
promiseContentMessage(browser, "SessionStore:input").then(result => {
ok(result, "received message for input changes");
promiseContentMessage(browser, "SessionStore:input").then(() => {
newWin.close();
// Now give it time to close

View File

@ -1,40 +0,0 @@
function test() {
/** Test for Bug 483330 **/
waitForExplicitFinish();
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
let browser = tab.linkedBrowser;
browser.addEventListener("load", function loadListener(e) {
browser.removeEventListener("load", arguments.callee, true);
// Scroll the content document
browser.contentWindow.scrollTo(1100, 1200);
is(browser.contentWindow.scrollX, 1100, "scrolled horizontally");
is(browser.contentWindow.scrollY, 1200, "scrolled vertically");
gBrowser.removeTab(tab);
let newTab = ss.undoCloseTab(window, 0);
newTab.addEventListener("SSTabRestored", function tabRestored(e) {
newTab.removeEventListener("SSTabRestored", arguments.callee, true);
let newBrowser = newTab.linkedBrowser;
// check that the scroll position was restored
is(newBrowser.contentWindow.scrollX, 1100, "still scrolled horizontally");
is(newBrowser.contentWindow.scrollY, 1200, "still scrolled vertically");
gBrowser.removeTab(newTab);
// Call stopPropagation on the event so we won't fire the
// tabbrowser's SSTabRestored listeners.
e.stopPropagation();
finish();
}, true);
}, true);
browser.loadURI("data:text/html;charset=utf-8,<body style='width: 100000px; height: 100000px;'><p>top</p></body>");
}

View File

@ -56,8 +56,7 @@ function test() {
// Start a load and interrupt it by closing the tab
tab.linkedBrowser.loadURI(URI_TO_LOAD);
let loaded = yield waitForLoadStarted(tab);
ok(loaded, "Load started");
yield waitForLoadStarted(tab);
let tabClosing = waitForTabClosed();
gBrowser.removeTab(tab);

View File

@ -0,0 +1,130 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const ROOT = getRootDirectory(gTestPath);
const URL = ROOT + "browser_frametree_sample.html";
const URL_FRAMESET = ROOT + "browser_frametree_sample_frameset.html";
/**
* This ensures that loading a page normally, aborting a page load, reloading
* a page, navigating using the bfcache, and ignoring frames that were
* created dynamically work as expect. We expect the frame tree to be reset
* when a page starts loading and we also expect a valid frame tree to exist
* when it has stopped loading.
*/
add_task(function test_frametree() {
const FRAME_TREE_SINGLE = { href: URL };
const FRAME_TREE_FRAMESET = {
href: URL_FRAMESET,
children: [{href: URL}, {href: URL}, {href: URL}]
};
// Create a tab with a single frame.
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseNewFrameTree(browser);
yield checkFrameTree(browser, FRAME_TREE_SINGLE,
"loading a page resets and creates the frame tree correctly");
// Load the frameset and create two frames dynamically, the first on
// DOMContentLoaded and the second on load.
yield sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
browser.loadURI(URL_FRAMESET);
yield promiseNewFrameTree(browser);
yield checkFrameTree(browser, FRAME_TREE_FRAMESET,
"dynamic frames created on or after the load event are ignored");
// Go back to the previous single-frame page. There will be no load event as
// the page is still in the bfcache. We thus make sure this type of navigation
// resets the frame tree.
browser.goBack();
yield promiseNewFrameTree(browser);
yield checkFrameTree(browser, FRAME_TREE_SINGLE,
"loading from bfache resets and creates the frame tree correctly");
// Load the frameset again but abort the load early.
// The frame tree should still be reset and created.
browser.loadURI(URL_FRAMESET);
executeSoon(() => browser.stop());
yield promiseNewFrameTree(browser);
// Load the frameset and check the tree again.
yield sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
browser.loadURI(URL_FRAMESET);
yield promiseNewFrameTree(browser);
yield checkFrameTree(browser, FRAME_TREE_FRAMESET,
"reloading a page resets and creates the frame tree correctly");
// Cleanup.
gBrowser.removeTab(tab);
});
/**
* This test ensures that we ignore frames that were created dynamically at or
* after the load event. SessionStore can't handle these and will not restore
* or collect any data for them.
*/
add_task(function test_frametree_dynamic() {
// The frame tree as expected. The first two frames are static
// and the third one was created on DOMContentLoaded.
const FRAME_TREE = {
href: URL_FRAMESET,
children: [{href: URL}, {href: URL}, {href: URL}]
};
const FRAME_TREE_REMOVED = {
href: URL_FRAMESET,
children: [{href: URL}, {href: URL}]
};
// Add an empty tab for a start.
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Create dynamic frames on "DOMContentLoaded" and on "load".
yield sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
browser.loadURI(URL_FRAMESET);
yield promiseNewFrameTree(browser);
// Check that the frame tree does not contain the frame created on "load".
// The two static frames and the one created on DOMContentLoaded must be in
// the tree.
yield checkFrameTree(browser, FRAME_TREE,
"frame tree contains first four frames");
// Remove the last frame in the frameset.
yield sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
// Check that the frame tree didn't change.
yield checkFrameTree(browser, FRAME_TREE,
"frame tree contains first four frames");
// Remove the last frame in the frameset.
yield sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
// Check that the frame tree excludes the removed frame.
yield checkFrameTree(browser, FRAME_TREE_REMOVED,
"frame tree contains first three frames");
// Cleanup.
gBrowser.removeTab(tab);
});
/**
* Checks whether the current frame hierarchy of a given |browser| matches the
* |expected| frame hierarchy.
*/
function checkFrameTree(browser, expected, msg) {
return sendMessage(browser, "ss-test:mapFrameTree").then(tree => {
is(JSON.stringify(tree), JSON.stringify(expected), msg);
});
}
/**
* Returns a promise that will be resolved when the given |browser| has loaded
* and we received messages saying that its frame tree has been reset and
* recollected.
*/
function promiseNewFrameTree(browser) {
let reset = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
let collect = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
return Promise.all([reset, collect]);
}

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>browser_frametree_sample.html</title>
</head>
<body style='width: 100000px; height: 100000px;'>top</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<html lang="en">
<head>
<meta charset="utf-8">
<title>browser_frametree_sample_frameset.html</title>
</head>
<frameset id="frames" rows="50%, 50%">
<frame src="browser_frametree_sample.html">
<frame src="browser_frametree_sample.html">
</frameset>
</html>

View File

@ -65,7 +65,7 @@ add_task(function nested_page_style() {
});
function getStyleSheets(browser) {
return sendMessage(browser, "ss-test:getStyleSheets").then(({data}) => data);
return sendMessage(browser, "ss-test:getStyleSheets");
}
function enableStyleSheetsForSet(browser, name) {
@ -79,8 +79,7 @@ function enableSubDocumentStyleSheetsForSet(browser, name) {
}
function getAuthorStyleDisabled(browser) {
return sendMessage(browser, "ss-test:getAuthorStyleDisabled")
.then(({data}) => data);
return sendMessage(browser, "ss-test:getAuthorStyleDisabled");
}
function setAuthorStyleDisabled(browser, val) {

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
let Imports = {};
Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", Imports);
let {SessionSaver} = Imports;
add_task(function cleanup() {
info("Forgetting closed tabs");
while (ss.getClosedTabCount(window)) {
ss.forgetClosedTab(window, 0);
}
});
add_task(function() {
let URL_PUBLIC = "http://example.com/public/" + Math.random();
let URL_PRIVATE = "http://example.com/private/" + Math.random();
let tab1, tab2;
try {
// Setup a public tab and a private tab
info("Setting up public tab");
tab1 = gBrowser.addTab(URL_PUBLIC);
yield promiseBrowserLoaded(tab1.linkedBrowser);
info("Setting up private tab");
tab2 = gBrowser.addTab();
yield promiseBrowserLoaded(tab2.linkedBrowser);
yield setUsePrivateBrowsing(tab2.linkedBrowser, true);
tab2.linkedBrowser.loadURI(URL_PRIVATE);
yield promiseBrowserLoaded(tab2.linkedBrowser);
info("Flush to make sure chrome received all data.");
SyncHandlers.get(tab2.linkedBrowser).flush();
info("Checking out state");
yield SessionSaver.run();
let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
let data = yield OS.File.read(path);
let state = new TextDecoder().decode(data);
info("State: " + state);
// Ensure that sessionstore.js only knows about the public tab
ok(state.indexOf(URL_PUBLIC) != -1, "State contains public tab");
ok(state.indexOf(URL_PRIVATE) == -1, "State does not contain private tab");
// Ensure that we can close and undo close the public tab but not the private tab
gBrowser.removeTab(tab2);
tab2 = null;
gBrowser.removeTab(tab1);
tab1 = null;
tab1 = ss.undoCloseTab(window, 0);
ok(true, "Public tab supports undo close");
is(ss.getClosedTabCount(window), 0, "Private tab does not support undo close");
} finally {
if (tab1) {
gBrowser.removeTab(tab1);
}
if (tab2) {
gBrowser.removeTab(tab2);
}
}
});
function setUsePrivateBrowsing(browser, val) {
return sendMessage(browser, "ss-test:setUsePrivateBrowsing", val);
}

View File

@ -0,0 +1,137 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ROOT = getRootDirectory(gTestPath);
const URL = ROOT + "browser_scrollPositions_sample.html";
const URL_FRAMESET = ROOT + "browser_scrollPositions_sample_frameset.html";
// Randomized set of scroll positions we will use in this test.
const SCROLL_X = Math.round(100 * (1 + Math.random()));
const SCROLL_Y = Math.round(200 * (1 + Math.random()));
const SCROLL_STR = SCROLL_X + "," + SCROLL_Y;
const SCROLL2_X = Math.round(300 * (1 + Math.random()));
const SCROLL2_Y = Math.round(400 * (1 + Math.random()));
const SCROLL2_STR = SCROLL2_X + "," + SCROLL2_Y;
/**
* This test ensures that we properly serialize and restore scroll positions
* for an average page without any frames.
*/
add_task(function test_scroll() {
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Scroll down a little.
yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL_X, y: SCROLL_Y});
checkScroll(tab, {scroll: SCROLL_STR}, "scroll is fine");
// Duplicate and check that the scroll position is restored.
let tab2 = ss.duplicateTab(window, tab);
let browser2 = tab2.linkedBrowser;
yield promiseTabRestored(tab2);
let scroll = yield sendMessage(browser2, "ss-test:getScrollPosition");
is(JSON.stringify(scroll), JSON.stringify({x: SCROLL_X, y: SCROLL_Y}),
"scroll position has been duplicated correctly");
// Check that reloading retains the scroll positions.
browser2.reload();
yield promiseBrowserLoaded(browser2);
checkScroll(tab2, {scroll: SCROLL_STR}, "reloading retains scroll positions");
// Check that a force-reload resets scroll positions.
browser2.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
yield promiseBrowserLoaded(browser2);
checkScroll(tab2, null, "force-reload resets scroll positions");
// Scroll back to the top and check that the position has been reset. We
// expect the scroll position to be "null" here because there is no data to
// be stored if the frame is in its default scroll position.
yield sendMessage(browser, "ss-test:setScrollPosition", {x: 0, y: 0});
checkScroll(tab, null, "no scroll stored");
// Cleanup.
gBrowser.removeTab(tab);
gBrowser.removeTab(tab2);
});
/**
* This tests ensures that we properly serialize and restore scroll positions
* for multiple frames of pages with framesets.
*/
add_task(function test_scroll_nested() {
let tab = gBrowser.addTab(URL_FRAMESET);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Scroll the first child frame down a little.
yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL_X, y: SCROLL_Y, frame: 0});
checkScroll(tab, {children: [{scroll: SCROLL_STR}]}, "scroll is fine");
// Scroll the second child frame down a little.
yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL2_X, y: SCROLL2_Y, frame: 1});
checkScroll(tab, {children: [{scroll: SCROLL_STR}, {scroll: SCROLL2_STR}]}, "scroll is fine");
// Duplicate and check that the scroll position is restored.
let tab2 = ss.duplicateTab(window, tab);
let browser2 = tab2.linkedBrowser;
yield promiseTabRestored(tab2);
let scroll = yield sendMessage(browser2, "ss-test:getScrollPosition", {frame: 0});
is(JSON.stringify(scroll), JSON.stringify({x: SCROLL_X, y: SCROLL_Y}),
"scroll position #1 has been duplicated correctly");
scroll = yield sendMessage(browser2, "ss-test:getScrollPosition", {frame: 1});
is(JSON.stringify(scroll), JSON.stringify({x: SCROLL2_X, y: SCROLL2_Y}),
"scroll position #2 has been duplicated correctly");
// Check that resetting one frame's scroll position removes it from the
// serialized value.
yield sendMessage(browser, "ss-test:setScrollPosition", {x: 0, y: 0, frame: 0});
checkScroll(tab, {children: [null, {scroll: SCROLL2_STR}]}, "scroll is fine");
// Check the resetting all frames' scroll positions nulls the stored value.
yield sendMessage(browser, "ss-test:setScrollPosition", {x: 0, y: 0, frame: 1});
checkScroll(tab, null, "no scroll stored");
// Cleanup.
gBrowser.removeTab(tab);
gBrowser.removeTab(tab2);
});
/**
* This test ensures that by moving scroll positions out of tabData.entries[]
* we still support the old scroll data format stored per shistory entry.
*/
add_task(function test_scroll_old_format() {
const TAB_STATE = { entries: [{url: URL, scroll: SCROLL_STR}] };
// Add a blank tab.
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Apply the tab state with the old format.
ss.setTabState(tab, JSON.stringify(TAB_STATE));
yield promiseTabRestored(tab);
// Check that the scroll positions has been applied.
let scroll = yield sendMessage(browser, "ss-test:getScrollPosition");
is(JSON.stringify(scroll), JSON.stringify({x: SCROLL_X, y: SCROLL_Y}),
"scroll position has been restored correctly");
// Cleanup.
gBrowser.removeTab(tab);
});
function checkScroll(tab, expected, msg) {
let browser = tab.linkedBrowser;
SyncHandlers.get(browser).flush();
let scroll = JSON.parse(ss.getTabState(tab)).scroll || null;
is(JSON.stringify(scroll), JSON.stringify(expected), msg);
}

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>browser_scrollPositions_sample.html</title>
</head>
<body style='width: 100000px; height: 100000px;'>top</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<html lang="en">
<head>
<meta charset="utf-8">
<title>browser_scrollPositions_sample_frameset.html</title>
</head>
<frameset id="frames" rows="50%, 50%">
<frame src="browser_scrollPositions_sample.html">
<frame src="browser_scrollPositions_sample.html">
</frameset>
</html>

View File

@ -2,8 +2,22 @@
* 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/. */
let Cu = Components.utils;
let Ci = Components.interfaces;
Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
let gFrameTree = new FrameTree(this);
gFrameTree.addObserver({
onFrameTreeReset: function () {
sendAsyncMessage("ss-test:onFrameTreeReset");
},
onFrameTreeCollected: function () {
sendAsyncMessage("ss-test:onFrameTreeCollected");
}
});
/**
* This frame script is only loaded for sessionstore mochitests. It enables us
* to modify and query docShell data when running with multiple processes.
@ -52,3 +66,77 @@ addMessageListener("ss-test:setAuthorStyleDisabled", function (msg) {
markupDocumentViewer.authorStyleDisabled = msg.data;
sendSyncMessage("ss-test:setAuthorStyleDisabled");
});
addMessageListener("ss-test:setUsePrivateBrowsing", function (msg) {
let loadContext =
docShell.QueryInterface(Ci.nsILoadContext);
loadContext.usePrivateBrowsing = msg.data;
sendAsyncMessage("ss-test:setUsePrivateBrowsing");
});
addMessageListener("ss-test:getScrollPosition", function (msg) {
let frame = content;
if (msg.data.hasOwnProperty("frame")) {
frame = content.frames[msg.data.frame];
}
let {scrollX: x, scrollY: y} = frame;
sendAsyncMessage("ss-test:getScrollPosition", {x: x, y: y});
});
addMessageListener("ss-test:setScrollPosition", function (msg) {
let frame = content;
let {x, y} = msg.data;
if (msg.data.hasOwnProperty("frame")) {
frame = content.frames[msg.data.frame];
}
frame.scrollTo(x, y);
frame.addEventListener("scroll", function onScroll(event) {
if (frame.document == event.target) {
frame.removeEventListener("scroll", onScroll);
sendAsyncMessage("ss-test:setScrollPosition");
}
});
});
addMessageListener("ss-test:createDynamicFrames", function ({data}) {
function createIFrame(rows) {
let frames = content.document.getElementById(data.id);
frames.setAttribute("rows", rows);
let frame = content.document.createElement("frame");
frame.setAttribute("src", data.url);
frames.appendChild(frame);
}
addEventListener("DOMContentLoaded", function onContentLoaded(event) {
if (content.document == event.target) {
removeEventListener("DOMContentLoaded", onContentLoaded, true);
// DOMContentLoaded is fired right after we finished parsing the document.
createIFrame("33%, 33%, 33%");
}
}, true);
addEventListener("load", function onLoad(event) {
if (content.document == event.target) {
removeEventListener("load", onLoad, true);
// Creating this frame on the same tick as the load event
// means that it must not be included in the frame tree.
createIFrame("25%, 25%, 25%, 25%");
}
}, true);
sendAsyncMessage("ss-test:createDynamicFrames");
});
addMessageListener("ss-test:removeLastFrame", function ({data}) {
let frames = content.document.getElementById(data.id);
frames.lastElementChild.remove();
sendAsyncMessage("ss-test:removeLastFrame");
});
addMessageListener("ss-test:mapFrameTree", function (msg) {
let result = gFrameTree.map(frame => ({href: frame.location.href}));
sendAsyncMessage("ss-test:mapFrameTree", result);
});

View File

@ -193,7 +193,7 @@ function promiseContentMessage(browser, name) {
function listener(msg) {
removeListener();
deferred.resolve(msg);
deferred.resolve(msg.data);
}
mm.addMessageListener(name, listener);
@ -280,9 +280,11 @@ function forceSaveState() {
}
function whenBrowserLoaded(aBrowser, aCallback = next) {
aBrowser.addEventListener("load", function onLoad() {
aBrowser.removeEventListener("load", onLoad, true);
executeSoon(aCallback);
aBrowser.addEventListener("load", function onLoad(event) {
if (event.target == aBrowser.contentDocument) {
aBrowser.removeEventListener("load", onLoad, true);
executeSoon(aCallback);
}
}, true);
}
function promiseBrowserLoaded(aBrowser) {

View File

@ -99,6 +99,7 @@ const promise = require("sdk/core/promise");
const Editor = require("devtools/sourceeditor/editor");
const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
const FastListWidget = require("devtools/shared/widgets/FastListWidget");
XPCOMUtils.defineLazyModuleGetter(this, "Parser",
"resource:///modules/devtools/Parser.jsm");
@ -192,6 +193,7 @@ let DebuggerController = {
this.SourceScripts.disconnect();
this.StackFrames.disconnect();
this.ThreadState.disconnect();
this.Tracer.disconnect();
this.disconnect();
// Chrome debugging needs to close its parent process on shutdown.
@ -218,39 +220,44 @@ let DebuggerController = {
return this._connection;
}
let deferred = promise.defer();
this._connection = deferred.promise;
let startedDebugging = promise.defer();
this._connection = startedDebugging.promise;
if (!window._isChromeDebugger) {
let target = this._target;
let { client, form: { chromeDebugger }, threadActor } = target;
let { client, form: { chromeDebugger, traceActor }, threadActor } = target;
target.on("close", this._onTabDetached);
target.on("navigate", this._onTabNavigated);
target.on("will-navigate", this._onTabNavigated);
this.client = client;
if (target.chrome) {
this._startChromeDebugging(client, chromeDebugger, deferred.resolve);
this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
} else {
this._startDebuggingTab(client, threadActor, deferred.resolve);
this._startDebuggingTab(threadActor, startedDebugging.resolve);
const startedTracing = promise.defer();
this._startTracingTab(traceActor, startedTracing.resolve);
return promise.all([startedDebugging.promise, startedTracing.promise]);
}
return deferred.promise;
return startedDebugging.promise;
}
// Chrome debugging needs to make its own connection to the debuggee.
let transport = debuggerSocketConnect(
Prefs.chromeDebuggingHost, Prefs.chromeDebuggingPort);
let client = new DebuggerClient(transport);
let client = this.client = new DebuggerClient(transport);
client.addListener("tabNavigated", this._onTabNavigated);
client.addListener("tabDetached", this._onTabDetached);
client.connect(() => {
client.listTabs(aResponse => {
this._startChromeDebugging(client, aResponse.chromeDebugger, deferred.resolve);
this._startChromeDebugging(aResponse.chromeDebugger, startedDebugging.resolve);
});
});
return deferred.promise;
return startedDebugging.promise;
},
/**
@ -331,21 +338,13 @@ let DebuggerController = {
/**
* Sets up a debugging session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param string aThreadActor
* The remote protocol grip of the tab.
* @param function aCallback
* A function to invoke once the client attached to the active thread.
* A function to invoke once the client attaches to the active thread.
*/
_startDebuggingTab: function(aClient, aThreadActor, aCallback) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachThread(aThreadActor, (aResponse, aThreadClient) => {
_startDebuggingTab: function(aThreadActor, aCallback) {
this.client.attachThread(aThreadActor, (aResponse, aThreadClient) => {
if (!aThreadClient) {
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
return;
@ -366,21 +365,13 @@ let DebuggerController = {
/**
* Sets up a chrome debugging session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param object aChromeDebugger
* The remote protocol grip of the chrome debugger.
* @param function aCallback
* A function to invoke once the client attached to the active thread.
* A function to invoke once the client attaches to the active thread.
*/
_startChromeDebugging: function(aClient, aChromeDebugger, aCallback) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
_startChromeDebugging: function(aChromeDebugger, aCallback) {
this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
if (!aThreadClient) {
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
return;
@ -398,6 +389,30 @@ let DebuggerController = {
}, { useSourceMaps: Prefs.sourceMapsEnabled });
},
/**
* Sets up an execution tracing session.
*
* @param object aTraceActor
* The remote protocol grip of the trace actor.
* @param function aCallback
* A function to invoke once the client attaches to the tracer.
*/
_startTracingTab: function(aTraceActor, aCallback) {
this.client.attachTracer(aTraceActor, (response, traceClient) => {
if (!traceClient) {
DevToolsUtils.reportError(new Error("Failed to attach to tracing actor."));
return;
}
this.traceClient = traceClient;
this.Tracer.connect();
if (aCallback) {
aCallback();
}
});
},
/**
* Detach and reattach to the thread actor with useSourceMaps true, blow
* away old sources and get them again.
@ -1411,6 +1426,218 @@ SourceScripts.prototype = {
}
};
/**
* Tracer update the UI according to the messages exchanged with the tracer
* actor.
*/
function Tracer() {
this._trace = null;
this._idCounter = 0;
this.onTraces = this.onTraces.bind(this);
}
Tracer.prototype = {
get client() {
return DebuggerController.client;
},
get traceClient() {
return DebuggerController.traceClient;
},
get tracing() {
return !!this._trace;
},
/**
* Hooks up the debugger controller with the tracer client.
*/
connect: function() {
this._stack = [];
this.client.addListener("traces", this.onTraces);
},
/**
* Disconnects the debugger controller from the tracer client. Any further
* communcation with the tracer actor will not have any effect on the UI.
*/
disconnect: function() {
this._stack = null;
this.client.removeListener("traces", this.onTraces);
},
/**
* Instructs the tracer actor to start tracing.
*/
startTracing: function(aCallback = () => {}) {
DebuggerView.Tracer.selectTab();
if (this.tracing) {
return;
}
this._trace = "dbg.trace" + Math.random();
this.traceClient.startTrace([
"name",
"location",
"parameterNames",
"depth",
"arguments",
"return",
"throw",
"yield"
], this._trace, (aResponse) => {
const { error } = aResponse;
if (error) {
DevToolsUtils.reportException(error);
this._trace = null;
}
aCallback(aResponse);
});
},
/**
* Instructs the tracer actor to stop tracing.
*/
stopTracing: function(aCallback = () => {}) {
if (!this.tracing) {
return;
}
this.traceClient.stopTrace(this._trace, aResponse => {
const { error } = aResponse;
if (error) {
DevToolsUtils.reportException(error);
}
this._trace = null;
aCallback(aResponse);
});
},
onTraces: function (aEvent, { traces }) {
const tracesLength = traces.length;
let tracesToShow;
if (tracesLength > TracerView.MAX_TRACES) {
tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
tracesLength);
DebuggerView.Tracer.empty();
this._stack.splice(0, this._stack.length);
} else {
tracesToShow = traces;
}
for (let t of tracesToShow) {
if (t.type == "enteredFrame") {
this._onCall(t);
} else {
this._onReturn(t);
}
}
DebuggerView.Tracer.commit();
},
/**
* Callback for handling a new call frame.
*/
_onCall: function({ name, location, parameterNames, depth, arguments: args }) {
const item = {
name: name,
location: location,
id: this._idCounter++
};
this._stack.push(item);
DebuggerView.Tracer.addTrace({
type: "call",
name: name,
location: location,
depth: depth,
parameterNames: parameterNames,
arguments: args,
frameId: item.id
});
},
/**
* Callback for handling an exited frame.
*/
_onReturn: function(aPacket) {
if (!this._stack.length) {
return;
}
const { name, id, location } = this._stack.pop();
DebuggerView.Tracer.addTrace({
type: aPacket.why,
name: name,
location: location,
depth: aPacket.depth,
frameId: id,
returnVal: aPacket.return || aPacket.throw || aPacket.yield
});
},
/**
* Create an object which has the same interface as a normal object client,
* but since we already have all the information for an object that we will
* ever get (the server doesn't create actors when tracing, just firehoses
* data and forgets about it) just return the data immdiately.
*
* @param Object aObject
* The tracer object "grip" (more like a limited snapshot).
* @returns Object
* The synchronous client object.
*/
syncGripClient: function(aObject) {
return {
get isFrozen() { return aObject.frozen; },
get isSealed() { return aObject.sealed; },
get isExtensible() { return aObject.extensible; },
get ownProperties() { return aObject.ownProperties; },
get prototype() { return null; },
getParameterNames: callback => callback(aObject),
getPrototypeAndProperties: callback => callback(aObject),
getPrototype: callback => callback(aObject),
getOwnPropertyNames: (callback) => {
callback({
ownPropertyNames: aObject.ownProperties
? Object.keys(aObject.ownProperties)
: []
});
},
getProperty: (property, callback) => {
callback({
descriptor: aObject.ownProperties
? aObject.ownProperties[property]
: null
});
},
getDisplayString: callback => callback("[object " + aObject.class + "]"),
getScope: callback => callback({
error: "scopeNotAvailable",
message: "Cannot get scopes for traced objects"
})
};
},
/**
* Wraps object snapshots received from the tracer server so that we can
* differentiate them from long living object grips from the debugger server
* in the variables view.
*
* @param Object aObject
* The object snapshot from the tracer actor.
*/
WrappedObject: function(aObject) {
this.object = aObject;
}
};
/**
* Handles breaking on event listeners in the currently debugged target.
*/
@ -1955,6 +2182,7 @@ let Prefs = new ViewHelpers.Prefs("devtools", {
ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
tracerEnabled: ["Bool", "debugger.tracer"],
editorTabSize: ["Int", "editor.tabsize"]
});
@ -1982,6 +2210,7 @@ DebuggerController.StackFrames = new StackFrames();
DebuggerController.SourceScripts = new SourceScripts();
DebuggerController.Breakpoints = new Breakpoints();
DebuggerController.Breakpoints.DOM = new EventListeners();
DebuggerController.Tracer = new Tracer();
/**
* Export some properties to the global scope for easier access.

View File

@ -1056,6 +1056,376 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
_conditionalPopupVisible: false
});
/**
* Functions handling the traces UI.
*/
function TracerView() {
this._selectedItem = null;
this._matchingItems = null;
this.widget = null;
this._highlightItem = this._highlightItem.bind(this);
this._isNotSelectedItem = this._isNotSelectedItem.bind(this);
this._unhighlightMatchingItems =
DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this));
this._onToggleTracing =
DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this));
this._onStartTracing =
DevToolsUtils.makeInfallible(this._onStartTracing.bind(this));
this._onClear = DevToolsUtils.makeInfallible(this._onClear.bind(this));
this._onSelect = DevToolsUtils.makeInfallible(this._onSelect.bind(this));
this._onMouseOver =
DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this));
}
TracerView.MAX_TRACES = 200;
TracerView.prototype = Heritage.extend(WidgetMethods, {
/**
* Initialization function, called when the debugger is started.
*/
initialize: function() {
dumpn("Initializing the TracerView");
this._traceButton = document.getElementById("trace");
this._tracerTab = document.getElementById("tracer-tab");
// Remove tracer related elements from the dom and tear everything down if
// the tracer isn't enabled.
if (!Prefs.tracerEnabled) {
this._traceButton.remove();
this._traceButton = null;
this._tracerTab.remove();
this._tracerTab = null;
document.getElementById("tracer-tabpanel").remove();
this.widget = null;
return;
}
this.widget = new FastListWidget(document.getElementById("tracer-traces"));
this._traceButton.removeAttribute("hidden");
this._tracerTab.removeAttribute("hidden");
this._tracerDeck = document.getElementById("tracer-deck");
this._search = document.getElementById("tracer-search");
this._template = document.getElementsByClassName("trace-item-template")[0];
this._templateItem = this._template.getElementsByClassName("trace-item")[0];
this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0];
this._templateNameNode = this._template.getElementsByClassName("trace-name")[0];
this.widget.addEventListener("select", this._onSelect, false);
this.widget.addEventListener("mouseover", this._onMouseOver, false);
this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false);
this._search.addEventListener("input", this._onSearch, false);
this._startTooltip = L10N.getStr("startTracingTooltip");
this._stopTooltip = L10N.getStr("stopTracingTooltip");
this._traceButton.setAttribute("tooltiptext", this._startTooltip);
},
/**
* Destruction function, called when the debugger is closed.
*/
destroy: function() {
dumpn("Destroying the TracerView");
if (!this.widget) {
return;
}
this.widget.removeEventListener("select", this._onSelect, false);
this.widget.removeEventListener("mouseover", this._onMouseOver, false);
this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false);
this._search.removeEventListener("input", this._onSearch, false);
},
/**
* Function invoked by the "toggleTracing" command to switch the tracer state.
*/
_onToggleTracing: function() {
if (DebuggerController.Tracer.tracing) {
this._onStopTracing();
} else {
this._onStartTracing();
}
},
/**
* Function invoked either by the "startTracing" command or by
* _onToggleTracing to start execution tracing in the backend.
*/
_onStartTracing: function() {
this._tracerDeck.selectedIndex = 0;
this._traceButton.setAttribute("checked", true);
this._traceButton.setAttribute("tooltiptext", this._stopTooltip);
this.empty();
DebuggerController.Tracer.startTracing();
},
/**
* Function invoked by _onToggleTracing to stop execution tracing in the
* backend.
*/
_onStopTracing: function() {
this._traceButton.removeAttribute("checked");
this._traceButton.setAttribute("tooltiptext", this._startTooltip);
DebuggerController.Tracer.stopTracing();
},
/**
* Function invoked by the "clearTraces" command to empty the traces pane.
*/
_onClear: function() {
this.empty();
},
/**
* Populate the given parent scope with the variable with the provided name
* and value.
*
* @param String aName
* The name of the variable.
* @param Object aParent
* The parent scope.
* @param Object aValue
* The value of the variable.
*/
_populateVariable: function(aName, aParent, aValue) {
let item = aParent.addItem(aName, { value: aValue });
if (aValue) {
DebuggerView.Variables.controller.populate(
item, new DebuggerController.Tracer.WrappedObject(aValue));
item.expand();
item.twisty = false;
}
},
/**
* Handler for the widget's "select" event. Displays parameters, exception, or
* return value depending on whether the selected trace is a call, throw, or
* return respectively.
*
* @param Object traceItem
* The selected trace item.
*/
_onSelect: function _onSelect({ detail: traceItem }) {
if (!traceItem) {
return;
}
const data = traceItem.attachment.trace;
const { location: { url, line } } = data;
DebuggerView.setEditorLocation(url, line, { noDebug: true });
DebuggerView.Variables.empty();
const scope = DebuggerView.Variables.addScope();
if (data.type == "call") {
const params = DevToolsUtils.zip(data.parameterNames, data.arguments);
for (let [name, val] of params) {
if (val === undefined) {
scope.addItem(name, { value: "<value not available>" });
} else {
this._populateVariable(name, scope, val);
}
}
} else {
const varName = "<" +
(data.type == "throw" ? "exception" : data.type) +
">";
this._populateVariable(varName, scope, data.returnVal);
}
scope.expand();
DebuggerView.showInstrumentsPane();
},
/**
* Add the hover frame enter/exit highlighting to a given item.
*/
_highlightItem: function(aItem) {
aItem.target.querySelector(".trace-item")
.classList.add("selected-matching");
},
/**
* Remove the hover frame enter/exit highlighting to a given item.
*/
_unhighlightItem: function(aItem) {
if (!aItem || !aItem.target) {
return;
}
const match = aItem.target.querySelector(".selected-matching");
if (match) {
match.classList.remove("selected-matching");
}
},
/**
* Remove the frame enter/exit pair highlighting we do when hovering.
*/
_unhighlightMatchingItems: function() {
if (this._matchingItems) {
this._matchingItems.forEach(this._unhighlightItem);
this._matchingItems = null;
}
},
/**
* Returns true if the given item is not the selected item.
*/
_isNotSelectedItem: function(aItem) {
return aItem !== this.selectedItem;
},
/**
* Highlight the frame enter/exit pair of items for the given item.
*/
_highlightMatchingItems: function(aItem) {
this._unhighlightMatchingItems();
this._matchingItems = this.items.filter(t => t.value == aItem.value);
this._matchingItems
.filter(this._isNotSelectedItem)
.forEach(this._highlightItem);
},
/**
* Listener for the mouseover event.
*/
_onMouseOver: function({ target }) {
const traceItem = this.getItemForElement(target);
if (traceItem) {
this._highlightMatchingItems(traceItem);
}
},
/**
* Listener for typing in the search box.
*/
_onSearch: function() {
const query = this._search.value.trim().toLowerCase();
this.filterContents(item =>
item.attachment.trace.name.toLowerCase().contains(query));
},
/**
* Select the traces tab in the sidebar.
*/
selectTab: function() {
const tabs = this._tracerTab.parentElement;
tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab);
this._tracerDeck.selectedIndex = 0;
},
/**
* Commit all staged items to the widget. Overridden so that we can call
* |FastListWidget.prototype.flush|.
*/
commit: function() {
WidgetMethods.commit.call(this);
// TODO: Accessing non-standard widget properties. Figure out what's the
// best way to expose such things. Bug 895514.
this.widget.flush();
},
/**
* Adds the trace record provided as an argument to the view.
*
* @param object aTrace
* The trace record coming from the tracer actor.
*/
addTrace: function(aTrace) {
const { type, frameId } = aTrace;
// Create the element node for the trace item.
let view = this._createView(aTrace);
// Append a source item to this container.
this.push([view, aTrace.frameId, ""], {
staged: true,
attachment: {
trace: aTrace
}
});
},
/**
* Customization function for creating an item's UI.
*
* @return nsIDOMNode
* The network request view.
*/
_createView: function({ type, name, frameId, parameterNames, returnVal,
location, depth, arguments: args }) {
let fragment = document.createDocumentFragment();
this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
this._templateItem.style.MozPaddingStart = depth + "em";
const TYPES = ["call", "yield", "return", "throw"];
for (let t of TYPES) {
this._templateTypeIcon.classList.toggle("trace-" + t, t == type);
}
this._templateTypeIcon.setAttribute("value", {
call: "\u2192",
yield: "Y",
return: "\u2190",
throw: "E",
terminated: "TERMINATED"
}[type]);
this._templateNameNode.setAttribute("value", name);
// All extra syntax and parameter nodes added.
const addedNodes = [];
if (parameterNames) {
const syntax = (p) => {
const el = document.createElement("label");
el.setAttribute("value", p);
el.classList.add("trace-syntax");
el.classList.add("plain");
addedNodes.push(el);
return el;
};
this._templateItem.appendChild(syntax("("));
for (let i = 0, n = parameterNames.length; i < n; i++) {
let param = document.createElement("label");
param.setAttribute("value", parameterNames[i]);
param.classList.add("trace-param");
param.classList.add("plain");
addedNodes.push(param);
this._templateItem.appendChild(param);
if (i + 1 !== n) {
this._templateItem.appendChild(syntax(", "));
}
}
this._templateItem.appendChild(syntax(")"));
}
// Flatten the DOM by removing one redundant box (the template container).
for (let node of this._template.childNodes) {
fragment.appendChild(node.cloneNode(true));
}
// Remove any added nodes from the template.
for (let node of addedNodes) {
this._templateItem.removeChild(node);
}
return fragment;
}
});
/**
* Utility functions for handling sources.
*/
@ -2789,6 +3159,7 @@ LineResults.size = function() {
*/
DebuggerView.Sources = new SourcesView();
DebuggerView.VariableBubble = new VariableBubbleView();
DebuggerView.Tracer = new TracerView();
DebuggerView.WatchExpressions = new WatchExpressionsView();
DebuggerView.EventListeners = new EventListenersView();
DebuggerView.GlobalSearch = new GlobalSearchView();

View File

@ -60,6 +60,7 @@ let DebuggerView = {
this.StackFramesClassicList.initialize();
this.Sources.initialize();
this.VariableBubble.initialize();
this.Tracer.initialize();
this.WatchExpressions.initialize();
this.EventListeners.initialize();
this.GlobalSearch.initialize();
@ -95,6 +96,7 @@ let DebuggerView = {
this.StackFramesClassicList.destroy();
this.Sources.destroy();
this.VariableBubble.destroy();
this.Tracer.destroy();
this.WatchExpressions.destroy();
this.EventListeners.destroy();
this.GlobalSearch.destroy();
@ -169,7 +171,11 @@ let DebuggerView = {
// Attach a controller that handles interfacing with the debugger protocol.
VariablesViewController.attach(this.Variables, {
getEnvironmentClient: aObject => gThreadClient.environment(aObject),
getObjectClient: aObject => gThreadClient.pauseGrip(aObject)
getObjectClient: aObject => {
return aObject instanceof DebuggerController.Tracer.WrappedObject
? DebuggerController.Tracer.syncGripClient(aObject.object)
: gThreadClient.pauseGrip(aObject)
}
});
// Relay events from the VariablesView.
@ -637,6 +643,7 @@ let DebuggerView = {
ChromeGlobals: null,
StackFrames: null,
Sources: null,
Tracer: null,
Variables: null,
VariableBubble: null,
WatchExpressions: null,

View File

@ -86,6 +86,12 @@
oncommand="DebuggerView.Options._toggleShowVariablesFilterBox()"/>
<command id="toggleShowOriginalSource"
oncommand="DebuggerView.Options._toggleShowOriginalSource()"/>
<command id="toggleTracing"
oncommand="DebuggerView.Tracer._onToggleTracing()"/>
<command id="startTracing"
oncommand="DebuggerView.Tracer._onStartTracing()"/>
<command id="clearTraces"
oncommand="DebuggerView.Tracer._onClear()"/>
</commandset>
<popupset id="debuggerPopupset">
@ -304,6 +310,13 @@
class="devtools-toolbarbutton"
tabindex="0"/>
</hbox>
<hbox>
<toolbarbutton id="trace"
class="devtools-toolbarbutton"
command="toggleTracing"
tabindex="0"
hidden="true"/>
</hbox>
<menulist id="chrome-globals"
class="devtools-menulist"
sizetopopup="none" hidden="true"/>
@ -328,6 +341,7 @@
<tabs>
<tab id="sources-tab" label="&debuggerUI.tabs.sources;"/>
<tab id="callstack-tab" label="&debuggerUI.tabs.callstack;"/>
<tab id="tracer-tab" label="&debuggerUI.tabs.traces;" hidden="true"/>
</tabs>
<tabpanels flex="1">
<tabpanel id="sources-tabpanel">
@ -354,6 +368,41 @@
<tabpanel id="callstack-tabpanel">
<vbox id="callstack-list" flex="1"/>
</tabpanel>
<tabpanel id="tracer-tabpanel" flex="1">
<deck id="tracer-deck" selectedIndex="1" flex="1">
<vbox flex="1">
<vbox id="tracer-traces" flex="1">
<hbox class="trace-item-template" hidden="true">
<hbox class="trace-item" align="center" flex="1" crop="end">
<label class="trace-type plain"/>
<label class="trace-name plain" crop="end"/>
</hbox>
</hbox>
</vbox>
<toolbar id="tracer-toolbar" class="devtools-toolbar">
<toolbarbutton id="clear-tracer"
label="&debuggerUI.clearButton;"
tooltiptext="&debuggerUI.clearButton.tooltip;"
command="clearTraces"
class="devtools-toolbarbutton"/>
<textbox id="tracer-search"
class="devtools-searchinput"
flex="1"
type="search"/>
</toolbar>
</vbox>
<vbox id="tracer-message"
flex="1"
align="center"
pack="center">
<description value="&debuggerUI.tracingNotStarted.label;" />
<button id="start-tracing"
class="devtools-toolbarbutton"
command="startTracing"
label="&debuggerUI.startTracing;"/>
</vbox>
</deck>
</tabpanel>
</tabpanels>
</tabbox>
<splitter id="sources-and-editor-splitter"

View File

@ -9,6 +9,8 @@ const { Cc, Ci, Cu, Cr } = require("chrome");
const promise = require("sdk/core/promise");
const EventEmitter = require("devtools/shared/event-emitter");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function DebuggerPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
this._toolbox = toolbox;
@ -57,8 +59,7 @@ DebuggerPanel.prototype = {
return this;
})
.then(null, function onError(aReason) {
Cu.reportError("DebuggerPanel open failed. " +
aReason.error + ": " + aReason.message);
DevToolsUtils.reportException("DebuggerPane.prototype.open", aReason);
});
},

View File

@ -20,6 +20,7 @@ support-files =
code_script-switching-01.js
code_script-switching-02.js
code_test-editor-mode
code_tracing-01.js
code_ugly.js
code_ugly-2.js
code_ugly-3.js
@ -56,6 +57,7 @@ support-files =
doc_script-switching-01.html
doc_script-switching-02.html
doc_step-out.html
doc_tracing-01.html
doc_watch-expressions.html
doc_with-frame.html
head.js
@ -188,6 +190,10 @@ support-files =
[browser_dbg_step-out.js]
[browser_dbg_tabactor-01.js]
[browser_dbg_tabactor-02.js]
[browser_dbg_tracing-01.js]
[browser_dbg_tracing-02.js]
[browser_dbg_tracing-03.js]
[browser_dbg_tracing-04.js]
[browser_dbg_variables-view-01.js]
[browser_dbg_variables-view-02.js]
[browser_dbg_variables-view-03.js]

View File

@ -0,0 +1,109 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that we get the expected frame enter/exit logs in the tracer view.
*/
const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
let gTab, gDebuggee, gPanel, gDebugger;
function test() {
SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
gDebuggee = aDebuggee;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
waitForSourceShown(gPanel, "code_tracing-01.js")
.then(() => startTracing(gPanel))
.then(clickButton)
.then(() => waitForClientEvents(aPanel, "traces"))
.then(testTraceLogs)
.then(() => stopTracing(gPanel))
.then(() => {
const deferred = promise.defer();
SpecialPowers.popPrefEnv(deferred.resolve);
return deferred.promise;
})
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
});
});
});
}
function clickButton() {
EventUtils.sendMouseEvent({ type: "click" },
gDebuggee.document.querySelector("button"),
gDebuggee);
}
function testTraceLogs() {
const onclickLogs = filterTraces(gPanel,
t => t.querySelector(".trace-name[value=onclick]"));
is(onclickLogs.length, 2, "Should have two logs from 'onclick'");
ok(onclickLogs[0].querySelector(".trace-call"),
"The first 'onclick' log should be a call.");
ok(onclickLogs[1].querySelector(".trace-return"),
"The second 'onclick' log should be a return.");
for (let t of onclickLogs) {
ok(t.querySelector(".trace-item").getAttribute("tooltiptext")
.contains("doc_tracing-01.html"));
}
const nonOnclickLogs = filterTraces(gPanel,
t => !t.querySelector(".trace-name[value=onclick]"));
for (let t of nonOnclickLogs) {
ok(t.querySelector(".trace-item").getAttribute("tooltiptext")
.contains("code_tracing-01.js"));
}
const mainLogs = filterTraces(gPanel,
t => t.querySelector(".trace-name[value=main]"));
is(mainLogs.length, 2, "Should have an enter and an exit for 'main'");
ok(mainLogs[0].querySelector(".trace-call"),
"The first 'main' log should be a call.");
ok(mainLogs[1].querySelector(".trace-return"),
"The second 'main' log should be a return.");
const factorialLogs = filterTraces(gPanel,
t => t.querySelector(".trace-name[value=factorial]"));
is(factorialLogs.length, 10, "Should have 5 enter, and 5 exit frames for 'factorial'");
ok(factorialLogs.slice(0, 5).every(t => t.querySelector(".trace-call")),
"The first five 'factorial' logs should be calls.");
ok(factorialLogs.slice(5).every(t => t.querySelector(".trace-return")),
"The second five 'factorial' logs should be returns.")
// Test that the depth affects padding so that calls are indented properly.
let lastDepth = -Infinity;
for (let t of factorialLogs.slice(0, 5)) {
let depth = parseInt(t.querySelector(".trace-item").style.MozPaddingStart, 10);
ok(depth > lastDepth, "The depth should be increasing");
lastDepth = depth;
}
lastDepth = Infinity;
for (let t of factorialLogs.slice(5)) {
let depth = parseInt(t.querySelector(".trace-item").style.MozPaddingStart, 10);
ok(depth < lastDepth, "The depth should be decreasing");
lastDepth = depth;
}
const throwerLogs = filterTraces(gPanel,
t => t.querySelector(".trace-name[value=thrower]"));
is(throwerLogs.length, 2, "Should have an enter and an exit for 'thrower'");
ok(throwerLogs[0].querySelector(".trace-call"),
"The first 'thrower' log should be a call.");
ok(throwerLogs[1].querySelector(".trace-throw",
"The second 'thrower' log should be a throw."));
}
registerCleanupFunction(function() {
gTab = null;
gDebuggee = null;
gPanel = null;
gDebugger = null;
});

View File

@ -0,0 +1,78 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that we highlight matching calls and returns on hover.
*/
const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
let gTab, gDebuggee, gPanel, gDebugger;
function test() {
SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
gDebuggee = aDebuggee;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
waitForSourceShown(gPanel, "code_tracing-01.js")
.then(() => startTracing(gPanel))
.then(clickButton)
.then(() => waitForClientEvents(aPanel, "traces"))
.then(highlightCall)
.then(testReturnHighlighted)
.then(unhighlightCall)
.then(testNoneHighlighted)
.then(() => stopTracing(gPanel))
.then(() => {
const deferred = promise.defer();
SpecialPowers.popPrefEnv(deferred.resolve);
return deferred.promise;
})
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
});
});
});
}
function clickButton() {
EventUtils.sendMouseEvent({ type: "click" },
gDebuggee.document.querySelector("button"),
gDebuggee);
}
function highlightCall() {
const callTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0];
EventUtils.sendMouseEvent({ type: "mouseover" },
callTrace,
gDebugger);
}
function testReturnHighlighted() {
const returnTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[1];
ok(Array.indexOf(returnTrace.querySelector(".trace-item").classList, "selected-matching") >= 0,
"The corresponding return log should be highlighted.");
}
function unhighlightCall() {
const callTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0];
EventUtils.sendMouseEvent({ type: "mouseout" },
callTrace,
gDebugger);
}
function testNoneHighlighted() {
const highlightedTraces = filterTraces(gPanel, t => t.querySelector(".selected-matching"));
is(highlightedTraces.length, 0, "Shouldn't have any highlighted traces");
}
registerCleanupFunction(function() {
gTab = null;
gDebuggee = null;
gPanel = null;
gDebugger = null;
});

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that we can jump to function definitions by clicking on logs.
*/
const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
let gTab, gDebuggee, gPanel, gDebugger;
function test() {
SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
gDebuggee = aDebuggee;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
waitForSourceShown(gPanel, "code_tracing-01.js")
.then(() => startTracing(gPanel))
.then(clickButton)
.then(() => waitForClientEvents(aPanel, "traces"))
.then(() => {
// Switch away from the JS file so we can make sure that clicking on a
// log will switch us back to the correct JS file.
aPanel.panelWin.DebuggerView.Sources.selectedValue = TAB_URL;
return ensureSourceIs(aPanel, TAB_URL, true);
})
.then(() => {
const finished = waitForSourceShown(gPanel, "code_tracing-01.js");
clickTraceLog();
return finished;
})
.then(testCorrectLine)
.then(() => stopTracing(gPanel))
.then(() => {
const deferred = promise.defer();
SpecialPowers.popPrefEnv(deferred.resolve);
return deferred.promise;
})
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
});
});
});
}
function clickButton() {
EventUtils.sendMouseEvent({ type: "click" },
gDebuggee.document.querySelector("button"),
gDebuggee);
}
function clickTraceLog() {
filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0].click();
}
function testCorrectLine() {
is(gDebugger.DebuggerView.editor.getCursor().line, 19,
"The editor should have the function definition site's line selected.");
}
registerCleanupFunction(function() {
gTab = null;
gDebuggee = null;
gPanel = null;
gDebugger = null;
});

View File

@ -0,0 +1,86 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that when we click on logs, we get the parameters/return value in the variables view.
*/
const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
let gTab, gDebuggee, gPanel, gDebugger, gVariables;
function test() {
SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => {
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
gDebuggee = aDebuggee;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
gVariables = gDebugger.DebuggerView.Variables;
waitForSourceShown(gPanel, "code_tracing-01.js")
.then(() => startTracing(gPanel))
.then(clickButton)
.then(() => waitForClientEvents(aPanel, "traces"))
.then(clickTraceCall)
.then(testParams)
.then(clickTraceReturn)
.then(testReturn)
.then(() => stopTracing(gPanel))
.then(() => {
const deferred = promise.defer();
SpecialPowers.popPrefEnv(deferred.resolve);
return deferred.promise;
})
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
DevToolsUtils.reportException("browser_dbg_tracing-04.js", aError);
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
});
});
});
}
function clickButton() {
EventUtils.sendMouseEvent({ type: "click" },
gDebuggee.document.querySelector("button"),
gDebuggee);
}
function clickTraceCall() {
filterTraces(gPanel, t => t.querySelector(".trace-name[value=factorial]"))[0]
.click();
}
function testParams() {
const name = gDebugger.document.querySelector(".variables-view-variable .name");
ok(name, "Should have a variable name");
is(name.getAttribute("value"), "n", "The variable name should be n");
const value = gDebugger.document.querySelector(".variables-view-variable .value.token-number");
ok(value, "Should have a variable value");
is(value.getAttribute("value"), "5", "The variable value should be 5");
}
function clickTraceReturn() {
filterTraces(gPanel, t => t.querySelector(".trace-name[value=factorial]"))
.pop().click();
}
function testReturn() {
const name = gDebugger.document.querySelector(".variables-view-variable .name");
ok(name, "Should have a variable name");
is(name.getAttribute("value"), "<return>", "The variable name should be <return>");
const value = gDebugger.document.querySelector(".variables-view-variable .value.token-number");
ok(value, "Should have a variable value");
is(value.getAttribute("value"), "120", "The variable value should be 120");
}
registerCleanupFunction(function() {
gTab = null;
gDebuggee = null;
gPanel = null;
gDebugger = null;
gVariables = null;
});

View File

@ -17,15 +17,35 @@ function test() {
ok(globalScope, "The globalScope hasn't been created correctly.");
ok(localScope, "The localScope hasn't been created correctly.");
is(globalScope.target.querySelector(".separator"), null,
"No separator string should be created for scopes (1).");
is(localScope.target.querySelector(".separator"), null,
"No separator string should be created for scopes (2).");
let windowVar = globalScope.addItem("window");
let documentVar = globalScope.addItem("document");
ok(windowVar, "The windowVar hasn't been created correctly.");
ok(documentVar, "The documentVar hasn't been created correctly.");
ok(windowVar.target.querySelector(".separator").hidden,
"No separator string should be shown for variables without a grip (1).");
ok(documentVar.target.querySelector(".separator").hidden,
"No separator string should be shown for variables without a grip (2).");
windowVar.setGrip({ type: "object", class: "Window" });
documentVar.setGrip({ type: "object", class: "HTMLDocument" });
is(windowVar.target.querySelector(".separator").hidden, false,
"A separator string should now be shown after setting the grip (1).");
is(documentVar.target.querySelector(".separator").hidden, false,
"A separator string should now be shown after setting the grip (2).");
is(windowVar.target.querySelector(".separator").getAttribute("value"), ": ",
"The separator string label is correct (1).");
is(documentVar.target.querySelector(".separator").getAttribute("value"), ": ",
"The separator string label is correct (2).");
let localVar0 = localScope.addItem("localVar0");
let localVar1 = localScope.addItem("localVar1");
let localVar2 = localScope.addItem("localVar2");

View File

@ -61,7 +61,7 @@ function performTest() {
gVariablesView.switch = function() {};
gVariablesView.delete = function() {};
gVariablesView.rawObject = test;
gVariablesView.pageSize = 5;
gVariablesView.scrollPageSize = 5;
return Task.spawn(function() {
yield waitForTick();

View File

@ -9,24 +9,21 @@
const TAB_URL = EXAMPLE_URL + "doc_large-array-buffer.html";
let gTab, gDebuggee, gPanel, gDebugger;
let gVariables;
let gVariables, gEllipsis;
function test() {
// This is a very, very stressful test.
// Thankfully, after bug 830344 none of this will be necessary anymore.
requestLongerTimeout(10);
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
gDebuggee = aDebuggee;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
gVariables = gDebugger.DebuggerView.Variables;
gEllipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
gDebugger.DebuggerView.Variables.lazyAppend = true;
waitForSourceAndCaretAndScopes(gPanel, ".html", 18)
.then(() => performTest())
waitForSourceAndCaretAndScopes(gPanel, ".html", 23)
.then(() => initialChecks())
.then(() => verifyFirstLevel())
.then(() => verifyNextLevels())
.then(() => resumeDebuggerThenCloseAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
@ -38,23 +35,15 @@ function test() {
});
}
function performTest() {
let deferred = promise.defer();
function initialChecks() {
let localScope = gVariables.getScopeAtIndex(0);
is(localScope.expanded, true,
"The local scope should be expanded by default.");
let localEnums = localScope.target.querySelector(".variables-view-element-details.enum").childNodes;
let localNonEnums = localScope.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(localEnums.length, 5,
"The local scope should contain all the created enumerable elements.");
is(localNonEnums.length, 0,
"The local scope should contain all the created non-enumerable elements.");
let bufferVar = localScope.get("buffer");
let zVar = localScope.get("z");
let arrayVar = localScope.get("largeArray");
let objectVar = localScope.get("largeObject");
ok(bufferVar, "There should be a 'buffer' variable present in the scope.");
ok(arrayVar, "There should be a 'largeArray' variable present in the scope.");
ok(objectVar, "There should be a 'largeObject' variable present in the scope.");
is(bufferVar.target.querySelector(".name").getAttribute("value"), "buffer",
"Should have the right property name for 'buffer'.");
@ -63,163 +52,185 @@ function performTest() {
ok(bufferVar.target.querySelector(".value").className.contains("token-other"),
"Should have the right token class for 'buffer'.");
is(zVar.target.querySelector(".name").getAttribute("value"), "z",
"Should have the right property name for 'z'.");
is(zVar.target.querySelector(".value").getAttribute("value"), "Int8Array",
"Should have the right property value for 'z'.");
ok(zVar.target.querySelector(".value").className.contains("token-other"),
"Should have the right token class for 'z'.");
is(arrayVar.target.querySelector(".name").getAttribute("value"), "largeArray",
"Should have the right property name for 'largeArray'.");
is(arrayVar.target.querySelector(".value").getAttribute("value"), "Int8Array",
"Should have the right property value for 'largeArray'.");
ok(arrayVar.target.querySelector(".value").className.contains("token-other"),
"Should have the right token class for 'largeArray'.");
EventUtils.sendMouseEvent({ type: "mousedown" },
bufferVar.target.querySelector(".arrow"),
gDebugger);
is(objectVar.target.querySelector(".name").getAttribute("value"), "largeObject",
"Should have the right property name for 'largeObject'.");
is(objectVar.target.querySelector(".value").getAttribute("value"), "Object",
"Should have the right property value for 'largeObject'.");
ok(objectVar.target.querySelector(".value").className.contains("token-other"),
"Should have the right token class for 'largeObject'.");
EventUtils.sendMouseEvent({ type: "mousedown" },
zVar.target.querySelector(".arrow"),
gDebugger);
is(bufferVar.expanded, false,
"The 'buffer' variable shouldn't be expanded.");
is(arrayVar.expanded, false,
"The 'largeArray' variable shouldn't be expanded.");
is(objectVar.expanded, false,
"The 'largeObject' variable shouldn't be expanded.");
// Need to wait for 0 enumerable and 2 non-enumerable properties in bufferVar,
// and 10000 enumerable and 5 non-enumerable properties in zVar.
let total = 0 + 2 + 10000 + 5;
let loaded = 0;
let paints = 0;
// Make sure the variables view doesn't scroll while adding the properties.
let [oldX, oldY] = getScroll();
info("Initial scroll position: " + oldX + ", " + oldY);
waitForProperties(total, {
onLoading: function(aLoaded) {
ok(aLoaded >= loaded,
"Should have loaded more properties.");
let [newX, newY] = getScroll();
info("Current scroll position: " + newX + " " + newY);
is(oldX, newX, "The variables view hasn't scrolled horizontally.");
is(oldY, newY, "The variables view hasn't scrolled vertically.");
info("Displayed " + aLoaded + " properties, not finished yet.");
loaded = aLoaded;
paints++;
},
onFinished: function(aLoaded) {
ok(aLoaded == total,
"Displayed all the properties.");
isnot(paints, 0,
"Debugger was unresponsive, sad panda.");
let [newX, newY] = getScroll();
info("Current scroll position: " + newX + ", " + newY);
is(oldX, newX, "The variables view hasn't scrolled horizontally.");
is(oldY, newY, "The variables view hasn't scrolled vertically.");
is(bufferVar._enum.childNodes.length, 0,
"The bufferVar should contain all the created enumerable elements.");
is(bufferVar._nonenum.childNodes.length, 2,
"The bufferVar should contain all the created non-enumerable elements.");
let bufferVarByteLengthProp = bufferVar.get("byteLength");
let bufferVarProtoProp = bufferVar.get("__proto__");
is(bufferVarByteLengthProp.target.querySelector(".name").getAttribute("value"), "byteLength",
"Should have the right property name for 'byteLength'.");
is(bufferVarByteLengthProp.target.querySelector(".value").getAttribute("value"), "10000",
"Should have the right property value for 'byteLength'.");
ok(bufferVarByteLengthProp.target.querySelector(".value").className.contains("token-number"),
"Should have the right token class for 'byteLength'.");
is(bufferVarProtoProp.target.querySelector(".name").getAttribute("value"), "__proto__",
"Should have the right property name for '__proto__'.");
is(bufferVarProtoProp.target.querySelector(".value").getAttribute("value"), "ArrayBufferPrototype",
"Should have the right property value for '__proto__'.");
ok(bufferVarProtoProp.target.querySelector(".value").className.contains("token-other"),
"Should have the right token class for '__proto__'.");
is(zVar._enum.childNodes.length, 10000,
"The zVar should contain all the created enumerable elements.");
is(zVar._nonenum.childNodes.length, 5,
"The zVar should contain all the created non-enumerable elements.");
let zVarByteLengthProp = zVar.get("byteLength");
let zVarByteOffsetProp = zVar.get("byteOffset");
let zVarProtoProp = zVar.get("__proto__");
is(zVarByteLengthProp.target.querySelector(".name").getAttribute("value"), "byteLength",
"Should have the right property name for 'byteLength'.");
is(zVarByteLengthProp.target.querySelector(".value").getAttribute("value"), "10000",
"Should have the right property value for 'byteLength'.");
ok(zVarByteLengthProp.target.querySelector(".value").className.contains("token-number"),
"Should have the right token class for 'byteLength'.");
is(zVarByteOffsetProp.target.querySelector(".name").getAttribute("value"), "byteOffset",
"Should have the right property name for 'byteOffset'.");
is(zVarByteOffsetProp.target.querySelector(".value").getAttribute("value"), "0",
"Should have the right property value for 'byteOffset'.");
ok(zVarByteOffsetProp.target.querySelector(".value").className.contains("token-number"),
"Should have the right token class for 'byteOffset'.");
is(zVarProtoProp.target.querySelector(".name").getAttribute("value"), "__proto__",
"Should have the right property name for '__proto__'.");
is(zVarProtoProp.target.querySelector(".value").getAttribute("value"), "Int8ArrayPrototype",
"Should have the right property value for '__proto__'.");
ok(zVarProtoProp.target.querySelector(".value").className.contains("token-other"),
"Should have the right token class for '__proto__'.");
let arrayElements = zVar._enum.childNodes;
for (let i = 0, len = arrayElements.length; i < len; i++) {
let node = arrayElements[i];
let name = node.querySelector(".name").getAttribute("value");
let value = node.querySelector(".value").getAttribute("value");
if (name !== i + "" || value !== "0") {
ok(false, "The array items aren't in the correct order.");
}
}
deferred.resolve();
},
onTimeout: function() {
ok(false, "Timed out while polling for the properties.");
deferred.resolve();
}
});
function getScroll() {
let scrollX = {};
let scrollY = {};
gVariables.boxObject.getPosition(scrollX, scrollY);
return [scrollX.value, scrollY.value];
}
return deferred.promise;
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2);
arrayVar.expand();
objectVar.expand();
return finished;
}
function waitForProperties(aTotal, aCallbacks, aInterval = 10) {
function verifyFirstLevel() {
let localScope = gVariables.getScopeAtIndex(0);
let bufferEnum = localScope.get("buffer")._enum.childNodes;
let bufferNonEnum = localScope.get("buffer")._nonenum.childNodes;
let zEnum = localScope.get("z")._enum.childNodes;
let zNonEnum = localScope.get("z")._nonenum.childNodes;
let arrayVar = localScope.get("largeArray");
let objectVar = localScope.get("largeObject");
// Poll every few milliseconds until the properties are retrieved.
let count = 0;
let intervalId = window.setInterval(() => {
// Make sure we don't wait for too long.
if (++count > 1000) {
window.clearInterval(intervalId);
aCallbacks.onTimeout();
return;
}
// Check if we need to wait for a few more properties to be fetched.
let loaded = bufferEnum.length + bufferNonEnum.length + zEnum.length + zNonEnum.length;
if (loaded < aTotal) {
aCallbacks.onLoading(loaded);
return;
}
// We got all the properties, it's safe to callback.
window.clearInterval(intervalId);
aCallbacks.onFinished(loaded);
}, aInterval);
let arrayEnums = arrayVar.target.querySelector(".variables-view-element-details.enum").childNodes;
let arrayNonEnums = arrayVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(arrayEnums.length, 0,
"The 'largeArray' shouldn't contain any enumerable elements.");
is(arrayNonEnums.length, 9,
"The 'largeArray' should contain all the created non-enumerable elements.");
let objectEnums = objectVar.target.querySelector(".variables-view-element-details.enum").childNodes;
let objectNonEnums = objectVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(objectEnums.length, 0,
"The 'largeObject' shouldn't contain any enumerable elements.");
is(objectNonEnums.length, 5,
"The 'largeObject' should contain all the created non-enumerable elements.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
0 + gEllipsis + 1999, "The first page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
"", "The first page in the 'largeArray' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
2000 + gEllipsis + 3999, "The second page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
"", "The second page in the 'largeArray' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
4000 + gEllipsis + 5999, "The third page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
"", "The third page in the 'largeArray' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
6000 + gEllipsis + 9999, "The fourth page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
"", "The fourth page in the 'largeArray' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
0 + gEllipsis + 1999, "The first page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
"", "The first page in the 'largeObject' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
2000 + gEllipsis + 3999, "The second page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
"", "The second page in the 'largeObject' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
4000 + gEllipsis + 5999, "The thrid page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
"", "The thrid page in the 'largeObject' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
6000 + gEllipsis + 9999, "The fourth page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
"", "The fourth page in the 'largeObject' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
"length", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
"10000", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"),
"buffer", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"),
"ArrayBuffer", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"),
"byteLength", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"),
"10000", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"),
"byteOffset", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"),
"0", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[8].getAttribute("value"),
"__proto__", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[8].getAttribute("value"),
"Int8ArrayPrototype", "The other properties 'largeArray' have the correct value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
"__proto__", "The other properties 'largeObject' are named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
"Object", "The other properties 'largeObject' have the correct value.");
}
function verifyNextLevels() {
let localScope = gVariables.getScopeAtIndex(0);
let objectVar = localScope.get("largeObject");
let lastPage1 = objectVar.get(6000 + gEllipsis + 9999);
ok(lastPage1, "The last page in the first level was retrieved successfully.");
lastPage1.expand();
let pageEnums1 = lastPage1.target.querySelector(".variables-view-element-details.enum").childNodes;
let pageNonEnums1 = lastPage1.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(pageEnums1.length, 0,
"The last page in the first level shouldn't contain any enumerable elements.");
is(pageNonEnums1.length, 4,
"The last page in the first level should contain all the created non-enumerable elements.");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
6000 + gEllipsis + 6999, "The first page in this level named correctly (1).");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
7000 + gEllipsis + 7999, "The second page in this level named correctly (1).");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
8000 + gEllipsis + 8999, "The third page in this level named correctly (1).");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
9000 + gEllipsis + 9999, "The fourth page in this level named correctly (1).");
let lastPage2 = lastPage1.get(9000 + gEllipsis + 9999);
ok(lastPage2, "The last page in the second level was retrieved successfully.");
lastPage2.expand();
let pageEnums2 = lastPage2.target.querySelector(".variables-view-element-details.enum").childNodes;
let pageNonEnums2 = lastPage2.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(pageEnums2.length, 0,
"The last page in the second level shouldn't contain any enumerable elements.");
is(pageNonEnums2.length, 4,
"The last page in the second level should contain all the created non-enumerable elements.");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
9000 + gEllipsis + 9199, "The first page in this level named correctly (2).");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
9200 + gEllipsis + 9399, "The second page in this level named correctly (2).");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
9400 + gEllipsis + 9599, "The third page in this level named correctly (2).");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
9600 + gEllipsis + 9999, "The fourth page in this level named correctly (2).");
let lastPage3 = lastPage2.get(9600 + gEllipsis + 9999);
ok(lastPage3, "The last page in the third level was retrieved successfully.");
lastPage3.expand();
let pageEnums3 = lastPage3.target.querySelector(".variables-view-element-details.enum").childNodes;
let pageNonEnums3 = lastPage3.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(pageEnums3.length, 400,
"The last page in the third level should contain all the created enumerable elements.");
is(pageNonEnums3.length, 0,
"The last page in the third level shouldn't contain any non-enumerable elements.");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
9600, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
9601, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[398].getAttribute("value"),
9998, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[399].getAttribute("value"),
9999, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
399, "The properties in this level have the correct value (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
398, "The properties in this level have the correct value (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[398].getAttribute("value"),
1, "The properties in this level have the correct value (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[399].getAttribute("value"),
0, "The properties in this level have the correct value (3).");
}
registerCleanupFunction(function() {
@ -228,4 +239,5 @@ registerCleanupFunction(function() {
gPanel = null;
gDebugger = null;
gVariables = null;
gEllipsis = null;
});

View File

@ -0,0 +1,29 @@
function factorial(n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
function* yielder(n) {
while (n-- >= 0) {
yield { value: n, squared: n * n };
}
}
function thrower() {
throw new Error("Curse your sudden but inevitable betrayal!");
}
function main() {
factorial(5);
// XXX bug 923729: Can't test yielding yet.
// for (let x of yielder(5)) {}
try {
thrower();
} catch (e) {
}
}

View File

@ -14,7 +14,12 @@
<script type="text/javascript">
function test(aNumber) {
var buffer = new ArrayBuffer(aNumber);
var z = new Int8Array(buffer);
var largeArray = new Int8Array(buffer);
var largeObject = {};
for (var i = 0; i < aNumber; i++) {
largeObject[i] = aNumber - i - 1;
}
debugger;
}
</script>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Debugger Tracer test page</title>
</head>
<body>
<script src="code_tracing-01.js"></script>
<button onclick="main()">Click me!</button>
</body>
</html>

View File

@ -375,6 +375,26 @@ function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) {
return deferred.promise;
}
function waitForClientEvents(aPanel, aEventName, aEventRepeat = 1) {
info("Waiting for client event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
let deferred = promise.defer();
let client = aPanel.panelWin.gClient;
let count = 0;
client.addListener(aEventName, function onEvent(aEventName, ...aArgs) {
info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s).");
if (count == aEventRepeat) {
ok(true, "Enough '" + aEventName + "' thread events have been fired.");
client.removeListener(aEventName, onEvent);
deferred.resolve.apply(deferred, aArgs);
}
});
return deferred.promise;
}
function ensureThreadClientState(aPanel, aState) {
let thread = aPanel.panelWin.gThreadClient;
let state = thread.state;
@ -488,8 +508,6 @@ function prepareDebugger(aDebugger) {
if ("target" in aDebugger) {
let variables = aDebugger.panelWin.DebuggerView.Variables;
variables.lazyEmpty = false;
variables.lazyAppend = false;
variables.lazyExpand = false;
variables.lazySearch = false;
} else {
// Nothing to do here yet.
@ -600,3 +618,38 @@ function hideVarPopupByScrollingEditor(aPanel) {
function reopenVarPopup(...aArgs) {
return hideVarPopup.apply(this, aArgs).then(() => openVarPopup.apply(this, aArgs));
}
// Tracing helpers
function startTracing(aPanel) {
const deferred = promise.defer();
aPanel.panelWin.DebuggerController.Tracer.startTracing(aResponse => {
if (aResponse.error) {
deferred.reject(aResponse);
} else {
deferred.resolve(aResponse);
}
});
return deferred.promise;
}
function stopTracing(aPanel) {
const deferred = promise.defer();
aPanel.panelWin.DebuggerController.Tracer.stopTracing(aResponse => {
if (aResponse.error) {
deferred.reject(aResponse);
} else {
deferred.resolve(aResponse);
}
});
return deferred.promise;
}
function filterTraces(aPanel, f) {
const traces = aPanel.panelWin.document
.getElementById("tracer-traces")
.querySelector("scrollbox")
.children;
return Array.filter(traces, f);
}

View File

@ -515,7 +515,11 @@ InspectorPanel.prototype = {
if (this.walker) {
this.walker.off("new-root", this.onNewRoot);
this._destroyPromise = this.walker.release()
.then(() => this._inspector.destroy())
.then(() => this._inspector.destroy(),
(e) => {
console.error("Walker.release() failed: " + e);
return this._inspector.destroy();
})
.then(() => {
this._inspector = null;
}, console.error);

View File

@ -1750,8 +1750,7 @@ NetworkDetailsView.prototype = {
return promise.all(aResponse.headers.map(header => {
let headerVar = headersScope.addItem(header.name, {}, true);
return gNetwork.getString(header.value)
.then(aString => headerVar.setGrip(aString));
return gNetwork.getString(header.value).then(aString => headerVar.setGrip(aString));
}));
},

View File

@ -61,14 +61,14 @@ function test() {
is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
"There should be 1 json scope displayed in this tabpanel.");
is(tabpanel.querySelectorAll(".variables-view-property").length, 6057,
"There should be 6057 json properties displayed in this tabpanel.");
is(tabpanel.querySelectorAll(".variables-view-property").length, 6143,
"There should be 6143 json properties displayed in this tabpanel.");
is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
"The empty notice should not be displayed in this tabpanel.");
let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
let names = ".variables-view-property .name";
let values = ".variables-view-property .value";
let names = ".variables-view-property > .title > .name";
let values = ".variables-view-property > .title > .value";
is(jsonScope.querySelector(".name").getAttribute("value"),
L10N.getStr("jsonScopeName"),
@ -83,11 +83,6 @@ function test() {
"greeting", "The second json property name was incorrect.");
is(jsonScope.querySelectorAll(values)[1].getAttribute("value"),
"\"Hello long string JSON!\"", "The second json property value was incorrect.");
is(Array.slice(jsonScope.querySelectorAll(names), -1).shift().getAttribute("value"),
"__proto__", "The last json property name was incorrect.");
is(Array.slice(jsonScope.querySelectorAll(values), -1).shift().getAttribute("value"),
"Object", "The last json property value was incorrect.");
}
});

View File

@ -0,0 +1,210 @@
const EventEmitter = require("devtools/shared/event-emitter");
const { Cu, Ci } = require("chrome");
const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
/**
* A list menu widget that attempts to be very fast.
*
* Note: this widget should be used in tandem with the WidgetMethods in
* ViewHelpers.jsm.
*
* Note: this widget also reuses SideMenuWidget CSS class names.
*
* @param nsIDOMNode aNode
* The element associated with the widget.
*/
const FastListWidget = module.exports = function FastListWidget(aNode) {
this.document = aNode.ownerDocument;
this.window = this.document.defaultView;
this._parent = aNode;
this._fragment = this.document.createDocumentFragment();
// This is a prototype element that each item added to the list clones.
this._templateElement = this.document.createElement("hbox");
this._templateElement.className = "side-menu-widget-item side-menu-widget-item-contents";
// Create an internal scrollbox container.
this._list = this.document.createElement("scrollbox");
this._list.className = "side-menu-widget-container";
this._list.setAttribute("flex", "1");
this._list.setAttribute("orient", "vertical");
this._list.setAttribute("theme", "dark");
this._list.setAttribute("tabindex", "0");
this._list.addEventListener("keypress", e => this.emit("keyPress", e), false);
this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
this._parent.appendChild(this._list);
this._orderedMenuElementsArray = [];
this._itemsByElement = new Map();
// This widget emits events that can be handled in a MenuContainer.
EventEmitter.decorate(this);
// Delegate some of the associated node's methods to satisfy the interface
// required by MenuContainer instances.
ViewHelpers.delegateWidgetEventMethods(this, aNode);
}
FastListWidget.prototype = {
/**
* Inserts an item in this container at the specified index, optionally
* grouping by name.
*
* @param number aIndex
* The position in the container intended for this item.
* @param nsIDOMNode aContents
* The node to be displayed in the container.
* @param Object aAttachment [optional]
* Extra data for the user.
* @return nsIDOMNode
* The element associated with the displayed item.
*/
insertItemAt: function(aIndex, aContents, aAttachment={}) {
let element = this._templateElement.cloneNode();
element.appendChild(aContents);
if (aIndex >= 0) {
throw new Error("FastListWidget only supports appending items.");
}
this._fragment.appendChild(element);
this._orderedMenuElementsArray.push(element);
this._itemsByElement.set(element, this);
return element;
},
/**
* This is a non-standard widget implementation method. When appending items,
* they are queued in a document fragment. This method appends the document
* fragment to the dom.
*/
flush: function() {
this._list.appendChild(this._fragment);
},
/**
* Removes all of the child nodes from this container.
*/
removeAllItems: function() {
let parent = this._parent;
let list = this._list;
while (list.hasChildNodes()) {
list.firstChild.remove();
}
this._selectedItem = null;
this._orderedMenuElementsArray.length = 0;
this._itemsByElement.clear();
},
/**
* Remove the given item.
*/
removeChild: function(child) {
throw new Error("Not yet implemented");
},
/**
* Gets the currently selected child node in this container.
* @return nsIDOMNode
*/
get selectedItem() this._selectedItem,
/**
* Sets the currently selected child node in this container.
* @param nsIDOMNode child
*/
set selectedItem(child) {
let menuArray = this._orderedMenuElementsArray;
if (!child) {
this._selectedItem = null;
}
for (let node of menuArray) {
if (node == child) {
node.classList.add("selected");
node.parentNode.classList.add("selected");
this._selectedItem = node;
} else {
node.classList.remove("selected");
node.parentNode.classList.remove("selected");
}
}
this.ensureElementIsVisible(this.selectedItem);
},
/**
* Returns the child node in this container situated at the specified index.
*
* @param number index
* The position in the container intended for this item.
* @return nsIDOMNode
* The element associated with the displayed item.
*/
getItemAtIndex: function(index) {
return this._orderedMenuElementsArray[index];
},
/**
* Returns the value of the named attribute on this container.
*
* @param string name
* The name of the attribute.
* @return string
* The current attribute value.
*/
getAttribute: function(name) {
return this._parent.getAttribute(name);
},
/**
* Adds a new attribute or changes an existing attribute on this container.
*
* @param string name
* The name of the attribute.
* @param string value
* The desired attribute value.
*/
setAttribute: function(name, value) {
this._parent.setAttribute(name, value);
},
/**
* Removes an attribute on this container.
*
* @param string name
* The name of the attribute.
*/
removeAttribute: function(name) {
this._parent.removeAttribute(name);
},
/**
* Ensures the specified element is visible.
*
* @param nsIDOMNode element
* The element to make visible.
*/
ensureElementIsVisible: function(element) {
if (!element) {
return;
}
// Ensure the element is visible but not scrolled horizontally.
let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
boxObject.ensureElementIsVisible(element);
boxObject.scrollBy(-element.clientWidth, 0);
},
window: null,
document: null,
_parent: null,
_list: null,
_selectedItem: null,
_orderedMenuElementsArray: null,
_itemsByElement: null
};

View File

@ -177,6 +177,7 @@ SideMenuWidget.prototype = {
this._orderedMenuElementsArray.splice(
this._orderedMenuElementsArray.indexOf(aChild), 1);
this._itemsByElement.delete(aChild);
if (this._selectedItem == aChild) {

View File

@ -11,8 +11,8 @@ const Cu = Components.utils;
const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const LAZY_EMPTY_DELAY = 150; // ms
const LAZY_EXPAND_DELAY = 50; // ms
const LAZY_APPEND_DELAY = 100; // ms
const LAZY_APPEND_BATCH = 100; // nodes
const SCROLL_PAGE_SIZE_DEFAULT = 0;
const APPEND_PAGE_SIZE_DEFAULT = 500;
const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
const PAGE_SIZE_MAX_JUMPS = 30;
const SEARCH_ACTION_MAX_DELAY = 300; // ms
@ -225,23 +225,24 @@ VariablesView.prototype = {
*/
lazyEmpty: false,
/**
* Specifies if nodes in this view may be added lazily.
* @see Scope.prototype._lazyAppend
*/
lazyAppend: true,
/**
* Specifies if nodes in this view may be expanded lazily.
* @see Scope.prototype.expand
*/
lazyExpand: true,
/**
* Specifies if nodes in this view may be searched lazily.
*/
lazySearch: true,
/**
* The number of elements in this container to jump when Page Up or Page Down
* keys are pressed. If falsy, then the page size will be based on the
* container height.
*/
scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
/**
* The maximum number of elements allowed in a scope, variable or property
* that allows pagination when appending children.
*/
appendPageSize: APPEND_PAGE_SIZE_DEFAULT,
/**
* Function called each time a variable or property's value is changed via
* user interaction. If null, then value changes are disabled.
@ -821,14 +822,14 @@ VariablesView.prototype = {
case e.DOM_VK_PAGE_UP:
// Rewind a certain number of elements based on the container height.
this.focusItemAtDelta(-(this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
this.focusItemAtDelta(-(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
PAGE_SIZE_SCROLL_HEIGHT_RATIO),
PAGE_SIZE_MAX_JUMPS)));
return;
case e.DOM_VK_PAGE_DOWN:
// Advance a certain number of elements based on the container height.
this.focusItemAtDelta(+(this.pageSize || Math.min(Math.floor(this._list.scrollHeight /
this.focusItemAtDelta(+(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight /
PAGE_SIZE_SCROLL_HEIGHT_RATIO),
PAGE_SIZE_MAX_JUMPS)));
return;
@ -882,13 +883,6 @@ VariablesView.prototype = {
}
},
/**
* The number of elements in this container to jump when Page Up or Page Down
* keys are pressed. If falsy, then the page size will be based on the
* container height.
*/
pageSize: 0,
/**
* Sets the text displayed in this container when there are no available items.
* @param string aValue
@ -1201,10 +1195,11 @@ function Scope(aView, aName, aFlags = {}) {
this._onClick = this._onClick.bind(this);
this._openEnum = this._openEnum.bind(this);
this._openNonEnum = this._openNonEnum.bind(this);
this._batchAppend = this._batchAppend.bind(this);
// Inherit properties and flags from the parent view. You can override
// each of these directly onto any scope, variable or property instance.
this.scrollPageSize = aView.scrollPageSize;
this.appendPageSize = aView.appendPageSize;
this.eval = aView.eval;
this.switch = aView.switch;
this.delete = aView.delete;
@ -1227,6 +1222,16 @@ Scope.prototype = {
*/
shouldPrefetch: true,
/**
* Whether this Scope should paginate its contents.
*/
allowPaginate: false,
/**
* The class name applied to this scope's target element.
*/
targetClassName: "variables-view-scope",
/**
* Create a new Variable that is a child of this Scope.
*
@ -1259,8 +1264,9 @@ Scope.prototype = {
* - { value: { type: "object", class: "Object" } }
* - { get: { type: "object", class: "Function" },
* set: { type: "undefined" } }
* @param boolean aRelaxed
* True if name duplicates should be allowed.
* @param boolean aRelaxed [optional]
* Pass true if name duplicates should be allowed.
* You probably shouldn't do it. Use this with caution.
* @return Variable
* The newly created Variable instance, null if it already exists.
*/
@ -1298,14 +1304,84 @@ Scope.prototype = {
* Additional options for adding the properties. Supported options:
* - sorted: true to sort all the properties before adding them
* - callback: function invoked after each item is added
* @param string aKeysType [optional]
* Helper argument in the case of paginated items. Can be either
* "just-strings" or "just-numbers". Humans shouldn't use this argument.
*/
addItems: function(aItems, aOptions = {}) {
addItems: function(aItems, aOptions = {}, aKeysType = "") {
let names = Object.keys(aItems);
// Building the view when inspecting an object with a very large number of
// properties may take a long time. To avoid blocking the UI, group
// the items into several lazily populated pseudo-items.
let exceedsThreshold = names.length >= this.appendPageSize;
let shouldPaginate = exceedsThreshold && aKeysType != "just-strings";
if (shouldPaginate && this.allowPaginate) {
// Group the items to append into two separate arrays, one containing
// number-like keys, the other one containing string keys.
if (aKeysType == "just-numbers") {
var numberKeys = names;
var stringKeys = [];
} else {
var numberKeys = [];
var stringKeys = [];
for (let name of names) {
// Be very careful. Avoid Infinity, NaN and non Natural number keys.
let coerced = +name;
if (Number.isInteger(coerced) && coerced > -1) {
numberKeys.push(name);
} else {
stringKeys.push(name);
}
}
}
// This object contains a very large number of properties, but they're
// almost all strings that can't be coerced to numbers. Don't paginate.
if (numberKeys.length < this.appendPageSize) {
this.addItems(aItems, aOptions, "just-strings");
return;
}
// Slices a section of the { name: descriptor } data properties.
let paginate = (aArray, aBegin = 0, aEnd = aArray.length) => {
let store = {}
for (let i = aBegin; i < aEnd; i++) {
let name = aArray[i];
store[name] = aItems[name];
}
return store;
};
// Creates a pseudo-item that populates itself with the data properties
// from the corresponding page range.
let createRangeExpander = (aArray, aBegin, aEnd, aOptions, aKeyTypes) => {
let rangeVar = this.addItem(aArray[aBegin] + Scope.ellipsis + aArray[aEnd - 1]);
rangeVar.onexpand = () => {
let pageItems = paginate(aArray, aBegin, aEnd);
rangeVar.addItems(pageItems, aOptions, aKeyTypes);
}
rangeVar.showArrow();
rangeVar.target.setAttribute("pseudo-item", "");
};
// Divide the number keys into quarters.
let page = +Math.round(numberKeys.length / 4).toPrecision(1);
createRangeExpander(numberKeys, 0, page, aOptions, "just-numbers");
createRangeExpander(numberKeys, page, page * 2, aOptions, "just-numbers");
createRangeExpander(numberKeys, page * 2, page * 3, aOptions, "just-numbers");
createRangeExpander(numberKeys, page * 3, numberKeys.length, aOptions, "just-numbers");
// Append all the string keys together.
this.addItems(paginate(stringKeys), aOptions, "just-strings");
return;
}
// Sort all of the properties before adding them, if preferred.
if (aOptions.sorted) {
if (aOptions.sorted && aKeysType != "just-numbers") {
names.sort();
}
// Add the properties to the current scope.
for (let name of names) {
let descriptor = aItems[name];
@ -1432,32 +1508,15 @@ Scope.prototype = {
* Expands the scope, showing all the added details.
*/
expand: function() {
if (this._isExpanded || this._locked) {
if (this._isExpanded || this._isLocked) {
return;
}
// If there's a large number of enumerable or non-enumerable items
// contained in this scope, painting them may take several seconds,
// even if they were already displayed before. In this case, show a throbber
// to suggest that this scope is expanding.
if (!this._isExpanding &&
this._variablesView.lazyExpand &&
this._store.size > LAZY_APPEND_BATCH) {
this._isExpanding = true;
// Start spinning a throbber in this scope's title and allow a few
// milliseconds for it to be painted.
this._startThrobber();
this.window.setTimeout(this.expand.bind(this), LAZY_EXPAND_DELAY);
return;
}
if (this._variablesView._enumVisible) {
this._openEnum();
}
if (this._variablesView._nonEnumVisible) {
Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0);
}
this._isExpanding = false;
this._isExpanded = true;
if (this.onexpand) {
@ -1469,7 +1528,7 @@ Scope.prototype = {
* Collapses the scope, hiding all the added details.
*/
collapse: function() {
if (!this._isExpanded || this._locked) {
if (!this._isExpanded || this._isLocked) {
return;
}
this._arrow.removeAttribute("open");
@ -1576,7 +1635,7 @@ Scope.prototype = {
* Gets the expand lock state.
* @return boolean
*/
get locked() this._locked,
get locked() this._isLocked,
/**
* Sets the visibility state.
@ -1606,7 +1665,7 @@ Scope.prototype = {
* Sets the expand lock state.
* @param boolean aFlag
*/
set locked(aFlag) this._locked = aFlag,
set locked(aFlag) this._isLocked = aFlag,
/**
* Specifies if this target node may be focused.
@ -1699,7 +1758,7 @@ Scope.prototype = {
*/
_init: function(aName, aFlags) {
this._idString = generateId(this._nameString = aName);
this._displayScope(aName, "variables-view-scope", "devtools-toolbar");
this._displayScope(aName, this.targetClassName, "devtools-toolbar");
this._addEventListeners();
this.parentNode.appendChild(this._target);
},
@ -1709,17 +1768,17 @@ Scope.prototype = {
*
* @param string aName
* The scope's name.
* @param string aClassName
* A custom class name for this scope.
* @param string aTargetClassName
* A custom class name for this scope's target element.
* @param string aTitleClassName [optional]
* A custom class name for this scope's title.
* A custom class name for this scope's title element.
*/
_displayScope: function(aName, aClassName, aTitleClassName) {
_displayScope: function(aName, aTargetClassName, aTitleClassName = "") {
let document = this.document;
let element = this._target = document.createElement("vbox");
element.id = this._idString;
element.className = aClassName;
element.className = aTargetClassName;
let arrow = this._arrow = document.createElement("hbox");
arrow.className = "arrow";
@ -1729,7 +1788,7 @@ Scope.prototype = {
name.setAttribute("value", aName);
let title = this._title = document.createElement("hbox");
title.className = "title " + (aTitleClassName || "");
title.className = "title " + aTitleClassName;
title.setAttribute("align", "center");
let enumerable = this._enum = document.createElement("vbox");
@ -1766,99 +1825,12 @@ Scope.prototype = {
this.focus();
},
/**
* Lazily appends a node to this scope's enumerable or non-enumerable
* container. Once a certain number of nodes have been batched, they
* will be appended.
*
* @param boolean aImmediateFlag
* Set to false if append calls should be dispatched synchronously
* on the current thread, to allow for a paint flush.
* @param boolean aEnumerableFlag
* Specifies if the node to append is enumerable or non-enumerable.
* @param nsIDOMNode aChild
* The child node to append.
*/
_lazyAppend: function(aImmediateFlag, aEnumerableFlag, aChild) {
// Append immediately, don't stage items and don't allow for a paint flush.
if (aImmediateFlag || !this._variablesView.lazyAppend) {
if (aEnumerableFlag) {
this._enum.appendChild(aChild);
} else {
this._nonenum.appendChild(aChild);
}
return;
}
let window = this.window;
let batchItems = this._batchItems;
window.clearTimeout(this._batchTimeout);
batchItems.push({ enumerableFlag: aEnumerableFlag, child: aChild });
// If a certain number of nodes have been batched, append all the
// staged items now.
if (batchItems.length > LAZY_APPEND_BATCH) {
// Allow for a paint flush.
Services.tm.currentThread.dispatch({ run: this._batchAppend }, 1);
return;
}
// Postpone appending the staged items for later, to allow batching
// more nodes.
this._batchTimeout = window.setTimeout(this._batchAppend, LAZY_APPEND_DELAY);
},
/**
* Appends all the batched nodes to this scope's enumerable and non-enumerable
* containers.
*/
_batchAppend: function() {
let document = this.document;
let batchItems = this._batchItems;
// Create two document fragments, one for enumerable nodes, and one
// for non-enumerable nodes.
let frags = [document.createDocumentFragment(), document.createDocumentFragment()];
for (let item of batchItems) {
frags[~~item.enumerableFlag].appendChild(item.child);
}
batchItems.length = 0;
this._enum.appendChild(frags[1]);
this._nonenum.appendChild(frags[0]);
},
/**
* Starts spinning a throbber in this scope's title.
*/
_startThrobber: function() {
if (this._throbber) {
this._throbber.hidden = false;
return;
}
let throbber = this._throbber = this.document.createElement("hbox");
throbber.className = "variables-view-throbber";
throbber.setAttribute("optional-visibility", "");
this._title.insertBefore(throbber, this._spacer);
},
/**
* Stops spinning the throbber in this scope's title.
*/
_stopThrobber: function() {
if (!this._throbber) {
return;
}
this._throbber.hidden = true;
},
/**
* Opens the enumerable items container.
*/
_openEnum: function() {
this._arrow.setAttribute("open", "");
this._enum.setAttribute("open", "");
this._stopThrobber();
},
/**
@ -1866,7 +1838,6 @@ Scope.prototype = {
*/
_openNonEnum: function() {
this._nonenum.setAttribute("open", "");
this._stopThrobber();
},
/**
@ -2108,10 +2079,7 @@ Scope.prototype = {
_fetched: false,
_retrieved: false,
_committed: false,
_batchItems: null,
_batchTimeout: null,
_locked: false,
_isExpanding: false,
_isLocked: false,
_isExpanded: false,
_isContentVisible: true,
_isHeaderVisible: true,
@ -2125,7 +2093,6 @@ Scope.prototype = {
_title: null,
_enum: null,
_nonenum: null,
_throbber: null
};
// Creating maps and arrays thousands of times for variables or properties
@ -2134,7 +2101,10 @@ Scope.prototype = {
DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_store", Map);
DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array);
DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_nonEnumItems", Array);
DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_batchItems", Array);
// An ellipsis symbol (usually "…") used for localization.
XPCOMUtils.defineLazyGetter(Scope, "ellipsis", () =>
Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
/**
* A Variable is a Scope holding Property instances.
@ -2167,12 +2137,24 @@ function Variable(aScope, aName, aDescriptor) {
Variable.prototype = Heritage.extend(Scope.prototype, {
/**
* Whether this Scope should be prefetched when it is remoted.
* Whether this Variable should be prefetched when it is remoted.
*/
get shouldPrefetch(){
get shouldPrefetch() {
return this.name == "window" || this.name == "this";
},
/**
* Whether this Variable should paginate its contents.
*/
get allowPaginate() {
return this.name != "window" && this.name != "this";
},
/**
* The class name applied to this variable's target element.
*/
targetClassName: "variables-view-variable variable-or-property",
/**
* Create a new Property that is a child of Variable.
*
@ -2371,6 +2353,7 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
this._valueLabel.classList.add(this._valueClassName);
this._valueLabel.setAttribute("value", this._valueString);
this._separatorLabel.hidden = false;
},
/**
@ -2414,33 +2397,21 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
*/
_init: function(aName, aDescriptor) {
this._idString = generateId(this._nameString = aName);
this._displayScope(aName, "variables-view-variable variable-or-property");
this._displayScope(aName, this.targetClassName);
this._displayVariable();
this._customizeVariable();
this._prepareTooltips();
this._setAttributes();
this._addEventListeners();
this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
},
/**
* Called when this variable has finished initializing, and is ready to
* be attached to the owner view.
*
* @param boolean aImmediateFlag
* @see Scope.prototype._lazyAppend
*/
_onInit: function(aImmediateFlag) {
if (this._initialDescriptor.enumerable ||
this._nameString == "this" ||
this._nameString == "<return>" ||
this._nameString == "<exception>") {
this.ownerView._lazyAppend(aImmediateFlag, true, this._target);
this.ownerView._enum.appendChild(this._target);
this.ownerView._enumItems.push(this);
} else {
this.ownerView._lazyAppend(aImmediateFlag, false, this._target);
this.ownerView._nonenum.appendChild(this._target);
this.ownerView._nonEnumItems.push(this);
}
},
@ -2454,7 +2425,7 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
let separatorLabel = this._separatorLabel = document.createElement("label");
separatorLabel.className = "plain separator";
separatorLabel.setAttribute("value", this.ownerView.separatorStr + " ");
separatorLabel.setAttribute("value", this.separatorStr + " ");
let valueLabel = this._valueLabel = document.createElement("label");
valueLabel.className = "plain value";
@ -2867,42 +2838,9 @@ function Property(aVar, aName, aDescriptor) {
Property.prototype = Heritage.extend(Variable.prototype, {
/**
* Initializes this property's id, view and binds event listeners.
*
* @param string aName
* The property's name.
* @param object aDescriptor
* The property's descriptor.
* The class name applied to this property's target element.
*/
_init: function(aName = "", aDescriptor) {
this._idString = generateId(this._nameString = aName);
this._displayScope(aName, "variables-view-property variable-or-property");
this._displayVariable();
this._customizeVariable();
this._prepareTooltips();
this._setAttributes();
this._addEventListeners();
this._onInit(this.ownerView._store.size < LAZY_APPEND_BATCH);
},
/**
* Called when this property has finished initializing, and is ready to
* be attached to the owner view.
*
* @param boolean aImmediateFlag
* @see Scope.prototype._lazyAppend
*/
_onInit: function(aImmediateFlag) {
if (this._initialDescriptor.enumerable) {
this.ownerView._lazyAppend(aImmediateFlag, true, this._target);
this.ownerView._enumItems.push(this);
} else {
this.ownerView._lazyAppend(aImmediateFlag, false, this._target);
this.ownerView._nonEnumItems.push(this);
}
}
targetClassName: "variables-view-property variable-or-property"
});
/**
@ -3272,7 +3210,6 @@ let generateId = (function() {
};
})();
/**
* An Editable encapsulates the UI of an edit box that overlays a label,
* allowing the user to edit the value.
@ -3364,7 +3301,6 @@ Editable.prototype = {
this._variable.collapse();
this._variable.hideArrow();
this._variable.locked = true;
this._variable._stopThrobber();
},
/**
@ -3382,7 +3318,6 @@ Editable.prototype = {
this._variable.locked = false;
this._variable.twisty = this._prevExpandable;
this._variable.expanded = this._prevExpanded;
this._variable._stopThrobber();
},
/**
@ -3498,7 +3433,7 @@ EditableNameAndValue.create = Editable.create;
EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
_reset: function(e) {
// Hide the Varible or Property if the user presses escape.
// Hide the Variable or Property if the user presses escape.
this._variable.remove();
this.deactivate();
},

View File

@ -64,7 +64,14 @@
display: none;
}
.variable-or-property[pseudo-item] > tooltip {
.variable-or-property[pseudo-item] > tooltip,
.variable-or-property[pseudo-item] > .title > .variables-view-edit,
.variable-or-property[pseudo-item] > .title > .variables-view-delete,
.variable-or-property[pseudo-item] > .title > .variables-view-add-property,
.variable-or-property[pseudo-item] > .title > .variable-or-property-frozen-label,
.variable-or-property[pseudo-item] > .title > .variable-or-property-sealed-label,
.variable-or-property[pseudo-item] > .title > .variable-or-property-non-extensible-label,
.variable-or-property[pseudo-item] > .title > .variable-or-property-non-writable-icon {
display: none;
}

View File

@ -51,7 +51,7 @@
.ruleview-header {
vertical-align: middle;
height: 1.5em;
min-height: 1.5em;
line-height: 1.5em;
}

View File

@ -796,7 +796,6 @@ function openDebugger(aOptions = {})
let panelWin = panel.panelWin;
panel._view.Variables.lazyEmpty = false;
panel._view.Variables.lazyAppend = false;
let resolveObject = {
target: target,

View File

@ -3431,7 +3431,6 @@ JSTerm.prototype = {
view.emptyText = l10n.getStr("emptyPropertiesList");
view.searchEnabled = !aOptions.hideFilterInput;
view.lazyEmpty = this._lazyVariablesView;
view.lazyAppend = this._lazyVariablesView;
VariablesViewController.attach(view, {
getEnvironmentClient: aGrip => {

View File

@ -379,6 +379,8 @@
@BINPATH@/components/Downloads.manifest
@BINPATH@/components/DownloadLegacy.js
@BINPATH@/components/BrowserPageThumbs.manifest
@BINPATH@/components/crashmonitor.manifest
@BINPATH@/components/nsCrashMonitor.js
@BINPATH@/components/SiteSpecificUserAgent.js
@BINPATH@/components/SiteSpecificUserAgent.manifest
@BINPATH@/components/toolkitsearch.manifest

View File

@ -23,7 +23,7 @@
<!-- LOCALIZATION NOTE (remoteHelp, remoteDocumentation, remoteHelpSuffix):
these strings will be concatenated in a single label, remoteDocumentation will
be used as text for a link to MDN. -->
<!ENTITY remoteHelp "Firefox Developer Tools can debug remote devices (Firefox for Android and Firefox OS, for example). Make sure that you have turned on the 'Remote debugging' option in the remote device. For more, see the">
<!ENTITY remoteHelp "Firefox Developer Tools can debug remote devices (Firefox for Android and Firefox OS, for example). Make sure that you have turned on the 'Remote debugging' option in the remote device. For more, see the ">
<!ENTITY remoteDocumentation "documentation">
<!ENTITY remoteHelpSuffix ".">

View File

@ -45,6 +45,22 @@
- button that toggles all breakpoints for all sources. -->
<!ENTITY debuggerUI.sources.toggleBreakpoints "Enable/disable all breakpoints">
<!-- LOCALIZATION NOTE (debuggerUI.tracingNotStarted.label): This is the text
- displayed when tracing hasn't started in the debugger UI. -->
<!ENTITY debuggerUI.tracingNotStarted.label "Tracing has not started.">
<!-- LOCALIZATION NOTE (debuggerUI.startTracing): This is the text displayed in
- the button to start execution tracing. -->
<!ENTITY debuggerUI.startTracing "Start Tracing">
<!-- LOCALIZATION NOTE (debuggerUI.clearButton): This is the label for
- the button that clears the collected tracing data in the tracing tab. -->
<!ENTITY debuggerUI.clearButton "Clear">
<!-- LOCALIZATION NOTE (debuggerUI.clearButton.tooltip): This is the tooltip for
- the button that clears the collected tracing data in the tracing tab. -->
<!ENTITY debuggerUI.clearButton.tooltip "Clear the collected traces">
<!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the
- checkbox that toggles pausing on exceptions. -->
<!ENTITY debuggerUI.pauseExceptions "Pause on exceptions">
@ -136,6 +152,7 @@
<!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that
- appears in the debugger's side pane tabs. -->
<!ENTITY debuggerUI.tabs.sources "Sources">
<!ENTITY debuggerUI.tabs.traces "Traces">
<!ENTITY debuggerUI.tabs.callstack "Call Stack">
<!ENTITY debuggerUI.tabs.variables "Variables">
<!ENTITY debuggerUI.tabs.events "Events">

View File

@ -49,6 +49,14 @@ pauseButtonTooltip=Click to pause (%S)
# button when the debugger is in a paused state.
resumeButtonTooltip=Click to resume (%S)
# LOCALIZATION NOTE (startTracingTooltip): The label that is displayed on the trace
# button when execution tracing is stopped.
startTracingTooltip=Click to start tracing
# LOCALIZATION NOTE (stopTracingTooltip): The label that is displayed on the trace
# button when execution tracing is started.
stopTracingTooltip=Click to stop tracing
# LOCALIZATION NOTE (stepOverTooltip): The label that is displayed on the
# button that steps over a function call.
stepOverTooltip=Step Over (%S)

View File

@ -15,6 +15,9 @@ XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
"@mozilla.org/xre/app-info;1", "nsICrashReporter");
#endif
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
"@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
Cu.import("resource://gre/modules/NetUtil.jsm");
return NetUtil;
@ -39,6 +42,8 @@ SessionStore.prototype = {
Ci.nsISupportsWeakReference]),
_windows: {},
_selectedWindow: 1,
_orderedWindows: [],
_lastSaveTime: 0,
_lastSessionTime: 0,
_interval: 10000,
@ -284,8 +289,8 @@ SessionStore.prototype = {
if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING)
return;
// Assign it a unique identifier (timestamp) and create its data object
aWindow.__SSID = "window" + Date.now();
// Assign it a unique identifier and create its data object
aWindow.__SSID = "window" + gUUIDGenerator.generateUUID().toString();
this._windows[aWindow.__SSID] = { tabs: [], selected: 0, _closedTabs: [] };
// Perform additional initialization when the first window is loading
@ -472,9 +477,9 @@ SessionStore.prototype = {
});
let data = { windows: [] };
let index;
for (index in this._windows)
data.windows.push(this._windows[index]);
for (let i = 0; i < this._orderedWindows.length; i++)
data.windows.push(this._windows[this._orderedWindows[i]]);
data.selectedWindow = this._selectedWindow;
return data;
},
@ -724,11 +729,24 @@ SessionStore.prototype = {
let window = Services.wm.getMostRecentWindow("navigator:browser");
let tabs = data.windows[0].tabs;
let selected = data.windows[0].selected;
this._selectedWindow = data.selectedWindow;
let windowIndex = this._selectedWindow - 1;
let tabs = data.windows[windowIndex].tabs;
let selected = data.windows[windowIndex].selected;
if (data.windows[0]._closedTabs)
this._windows[window.__SSID]._closedTabs = data.windows[0]._closedTabs;
// Move all window data from sessionstore.js to this._windows.
for (let i = 0; i < data.windows.length; i++) {
let SSID;
if (i != windowIndex) {
SSID = "window" + gUUIDGenerator.generateUUID().toString();
this._windows[SSID] = data.windows[i];
} else {
SSID = window.__SSID;
this._windows[SSID]._closedTabs =
this._windows[SSID]._closedTabs.concat(data.windows[windowIndex]._closedTabs);
}
this._orderedWindows.push(SSID);
}
if (selected > tabs.length) // Clamp the selected index if it's bogus
selected = 1;

View File

@ -6,7 +6,7 @@
this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"];
const Cu = Components.utils;
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -18,6 +18,88 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyGetter(this, "DEFAULT_TOOLBAR_PLACEMENTS", function() {
let result = {
"PanelUI-contents": [
"edit-controls",
"zoom-controls",
"new-window-button",
"privatebrowsing-button",
"save-page-button",
"print-button",
"history-panelmenu",
"fullscreen-button",
"find-button",
"preferences-button",
"add-ons-button",
],
"nav-bar": [
"urlbar-container",
"search-container",
"webrtc-status-button",
"bookmarks-menu-button",
"downloads-button",
"home-button",
"social-share-button",
],
// It's true that toolbar-menubar is not visible
// on OS X, but the XUL node is definitely present
// in the document.
"toolbar-menubar": [
"menubar-items",
],
"TabsToolbar": [
"tabbrowser-tabs",
"new-tab-button",
"alltabs-button",
"tabs-closebutton",
],
"PersonalToolbar": [
"personal-bookmarks",
],
};
let showCharacterEncoding = Services.prefs.getComplexValue(
"browser.menu.showCharacterEncoding",
Ci.nsIPrefLocalizedString
).data;
if (showCharacterEncoding == "true") {
result["PanelUI-contents"].push("characterencoding-button");
}
if (Services.sysinfo.getProperty("hasWindowsTouchInterface")) {
result["PanelUI-contents"].push("switch-to-metro-button");
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() {
let result = [
"open-file-button",
"developer-button",
"feed-button",
"email-link-button",
"sync-button",
"tabview-button",
];
let panelPlacements = DEFAULT_TOOLBAR_PLACEMENTS["PanelUI-contents"];
if (panelPlacements.indexOf("characterencoding-button") == -1) {
result.push("characterencoding-button");
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() {
let result = [];
for (let [, buttons] of Iterator(DEFAULT_TOOLBAR_PLACEMENTS)) {
result = result.concat(buttons);
}
return result;
});
const ALL_BUILTIN_ITEMS = [
"fullscreen-button",
"switch-to-metro-button",
@ -249,6 +331,48 @@ this.BrowserUITelemetry = {
let bookmarksBar = document.getElementById("PersonalToolbar");
result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed;
// Examine all customizable areas and see what default items
// are present and missing.
let defaultKept = [];
let defaultMoved = [];
let nondefaultAdded = [];
for (let areaID of CustomizableUI.areas) {
let items = CustomizableUI.getWidgetIdsInArea(areaID);
for (let item of items) {
// Is this a default item?
if (DEFAULT_ITEMS.indexOf(item) != -1) {
// Ok, it's a default item - but is it in its default
// toolbar? We use Array.isArray instead of checking for
// toolbarID in DEFAULT_TOOLBAR_PLACEMENTS because an add-on might
// be clever and give itself the id of "toString" or something.
if (Array.isArray(DEFAULT_TOOLBAR_PLACEMENTS[areaID]) &&
DEFAULT_TOOLBAR_PLACEMENTS[areaID].indexOf(item) != -1) {
// The item is in its default toolbar
defaultKept.push(item);
} else {
defaultMoved.push(item);
}
} else if (PALETTE_ITEMS.indexOf(item) != -1) {
// It's a palette item that's been moved into a toolbar
nondefaultAdded.push(item);
}
// else, it's provided by an add-on, and we won't record it.
}
}
// Now go through the items in the palette to see what default
// items are in there.
let paletteItems =
CustomizableUI.getUnusedWidgets(win.gNavToolbox.palette);
let defaultRemoved = [item.id for (item of paletteItems)
if (DEFAULT_ITEMS.indexOf(item.id) != -1)];
result.defaultKept = defaultKept;
result.defaultMoved = defaultMoved;
result.nondefaultAdded = nondefaultAdded;
result.defaultRemoved = defaultRemoved;
result.countableEvents = this._countableEvents;
return result;

View File

@ -26,7 +26,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/commonjs/sdk/core/promise.js");
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "unescapeService",
"@mozilla.org/feed-unescapehtml;1",
@ -98,10 +98,6 @@ this.Social = {
providers: [],
_disabledForSafeMode: false,
get allowMultipleWorkers() {
return Services.prefs.getBoolPref("social.allowMultipleWorkers");
},
get _currentProviderPref() {
try {
return Services.prefs.getComplexValue("social.provider.current",
@ -130,11 +126,6 @@ this.Social = {
if (this._provider == provider)
return;
// Disable the previous provider, if we are not allowing multiple workers,
// since we want only one provider to be enabled at once.
if (this._provider && !Social.allowMultipleWorkers)
this._provider.enabled = false;
this._provider = provider;
if (this._provider) {
@ -208,10 +199,6 @@ this.Social = {
},
_updateWorkerState: function(enable) {
// ensure that our providers are all disabled, and enabled if we allow
// multiple workers
if (enable && !Social.allowMultipleWorkers)
return;
[p.enabled = enable for (p of Social.providers) if (p.enabled != enable)];
},

View File

@ -4,10 +4,6 @@
function run_test() {
// we are testing worker startup specifically
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
do_register_cleanup(function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
});
do_test_pending();
add_test(testStartupEnabled);
add_test(testDisableAfterStartup);

View File

@ -4,10 +4,6 @@
function run_test() {
// we are testing worker startup specifically
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
do_register_cleanup(function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
});
do_test_pending();
add_test(testStartupDisabled);
add_test(testEnableAfterStartup);

View File

@ -93,6 +93,119 @@
padding: .25em;
}
/* Tracer */
#trace {
list-style-image: url(tracer-icon.png);
-moz-image-region: rect(0px,16px,16px,0px);
}
#trace[checked] {
-moz-image-region: rect(0px,32px,16px,16px);
}
#start-tracing {
padding: 4px;
margin: 4px;
}
#clear-tracer {
min-width: 22px !important;
}
#tracer-search {
min-width: 72px !important;
}
#tracer-message {
/* Prevent the container deck from aquiring the height from this message. */
min-height: 1px;
}
.trace-name {
-moz-padding-start: 4px !important;
}
/* Tracer dark theme */
.theme-dark #tracer-message {
color: #f5f7fa; /* Light foreground text */
background: url(background-noise-toolbar.png) #181d20; /* Content background sidebar */
}
.theme-dark #tracer-traces > scrollbox {
background-color: #181d20 !important; /* Content background sidebar */
}
.theme-dark .trace-item {
color: #f5f7fa; /* Light foreground text */
}
.trace-item.selected-matching {
background-color: #1d4f73; /* Select highlight blue */
}
.theme-dark .trace-call {
color: #46afe3; /* highlight blue */
}
.theme-dark .trace-return,
.theme-dark .trace-yield {
color: #70bf53; /* highlight green */
}
.theme-dark .trace-throw {
color: #eb5368; /* highlight red */
}
.theme-dark .trace-param {
color: #8fa1b2; /* Content text grey */
}
.theme-dark .trace-syntax {
color: #5e88b0; /* highlight blue-grey */
}
/* Tracer light theme */
.theme-light #tracer-message {
color: #292e33; /* Dark foreground text */
background: url(background-noise-toolbar.png) #f7f7f7; /* Content background sidebar */
}
.theme-light #tracer-traces > scrollbox {
background-color: #f7f7f7 !important; /* Content background sidebar */
}
.theme-light .trace-item {
color: #292e33; /* Dark foreground text */
}
.trace-item.selected-matching {
background-color: #4c9ed9; /* Select highlight blue */
}
.theme-light .trace-call {
color: #0088cc; /* highlight blue */
}
.theme-light .trace-return,
.theme-light .trace-yield {
color: #2cbb0f; /* highlight green */
}
.theme-light .trace-throw {
color: #ed2655; /* highlight red */
}
.theme-light .trace-param {
color: #8fa1b2; /* Content text grey */
}
.theme-light .trace-syntax {
color: #5f88b0; /* highlight blue-grey */
}
/* ListWidget items */
.list-widget-item {
@ -226,11 +339,6 @@
/* Instruments pane (watch expressions, variables, event listeners...) */
#instruments-pane > tabs > tab {
min-height: 25px !important;
padding: 0 !important;
}
#instruments-pane .side-menu-widget-container,
#instruments-pane .side-menu-widget-empty-notice-container {
box-shadow: none !important;

View File

@ -22,6 +22,7 @@
padding: 1px 4px;
margin-top: 4px;
-moz-user-select: none;
word-wrap: break-word;
}
.ruleview-rule-source:hover {
@ -115,6 +116,7 @@
.ruleview-property {
border-left: 2px solid transparent;
clear: right;
}
.ruleview-property > * {
@ -135,6 +137,10 @@
border-bottom-color: hsl(0,0%,50%);
}
.ruleview-selector {
word-wrap: break-word;
}
.ruleview-selector-separator, .ruleview-selector-unmatched {
color: #888;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -451,18 +451,8 @@
color: #fff;
}
.variables-view-scope > .variables-view-element-details:not(:empty) {
-moz-margin-start: 2px;
-moz-margin-end: 1px;
}
/* Generic variables traits */
.variables-view-variable {
-moz-margin-start: 1px;
-moz-margin-end: 1px;
}
.variables-view-variable:not(:last-child) {
border-bottom: 1px solid rgba(128, 128, 128, .15);
}
@ -481,8 +471,12 @@
-moz-box-flex: 1;
}
.variable-or-property > .title > .arrow {
-moz-margin-start: 3px;
}
.variable-or-property:not([untitled]) > .variables-view-element-details {
-moz-margin-start: 10px;
-moz-margin-start: 7px;
}
/* Traits applied when variables or properties are changed or overridden */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -77,7 +77,6 @@ browser.jar:
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
skin/classic/browser/downloads/download-glow.png (downloads/download-glow.png)
skin/classic/browser/downloads/download-glow-small.png (downloads/download-glow-small.png)
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)
skin/classic/browser/downloads/download-summary.png (downloads/download-summary.png)
@ -222,6 +221,8 @@ browser.jar:
skin/classic/browser/devtools/debugger-blackbox.png (devtools/debugger-blackbox.png)
skin/classic/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
skin/classic/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
skin/classic/browser/devtools/tracer-icon.png (devtools/tracer-icon.png)
skin/classic/browser/devtools/tracer-icon@2x.png (devtools/tracer-icon@2x.png)
skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)

View File

@ -95,6 +95,119 @@
padding: .25em;
}
/* Tracer */
#trace {
list-style-image: url(tracer-icon.png);
-moz-image-region: rect(0px,16px,16px,0px);
}
#trace[checked] {
-moz-image-region: rect(0px,32px,16px,16px);
}
#start-tracing {
padding: 4px;
margin: 4px;
}
#clear-tracer {
min-width: 22px !important;
}
#tracer-search {
min-width: 72px !important;
}
#tracer-message {
/* Prevent the container deck from aquiring the height from this message. */
min-height: 1px;
}
.trace-name {
-moz-padding-start: 4px !important;
}
/* Tracer dark theme */
.theme-dark #tracer-message {
color: #f5f7fa; /* Light foreground text */
background: url(background-noise-toolbar.png) #181d20; /* Content background sidebar */
}
.theme-dark #tracer-traces > scrollbox {
background-color: #181d20 !important; /* Content background sidebar */
}
.theme-dark .trace-item {
color: #f5f7fa; /* Light foreground text */
}
.trace-item.selected-matching {
background-color: #1d4f73; /* Select highlight blue */
}
.theme-dark .trace-call {
color: #46afe3; /* highlight blue */
}
.theme-dark .trace-return,
.theme-dark .trace-yield {
color: #70bf53; /* highlight green */
}
.theme-dark .trace-throw {
color: #eb5368; /* highlight red */
}
.theme-dark .trace-param {
color: #8fa1b2; /* Content text grey */
}
.theme-dark .trace-syntax {
color: #5e88b0; /* highlight blue-grey */
}
/* Tracer light theme */
.theme-light #tracer-message {
color: #292e33; /* Dark foreground text */
background: url(background-noise-toolbar.png) #f7f7f7; /* Content background sidebar */
}
.theme-light #tracer-traces > scrollbox {
background-color: #f7f7f7 !important; /* Content background sidebar */
}
.theme-light .trace-item {
color: #292e33; /* Dark foreground text */
}
.trace-item.selected-matching {
background-color: #4c9ed9; /* Select highlight blue */
}
.theme-light .trace-call {
color: #0088cc; /* highlight blue */
}
.theme-light .trace-return,
.theme-light .trace-yield {
color: #2cbb0f; /* highlight green */
}
.theme-light .trace-throw {
color: #ed2655; /* highlight red */
}
.theme-light .trace-param {
color: #8fa1b2; /* Content text grey */
}
.theme-light .trace-syntax {
color: #5f88b0; /* highlight blue-grey */
}
/* ListWidget items */
.list-widget-item {
@ -228,11 +341,6 @@
/* Instruments pane (watch expressions, variables, event listeners...) */
#instruments-pane > tabs > tab {
min-height: 1em !important;
padding: 0 !important;
}
#instruments-pane .side-menu-widget-container,
#instruments-pane .side-menu-widget-empty-notice-container {
box-shadow: none !important;

View File

@ -21,6 +21,7 @@
border-bottom-style: solid;
padding: 1px 4px;
-moz-user-select: none;
word-wrap: break-word;
}
.ruleview-rule-pseudo-element {
@ -119,6 +120,7 @@
.ruleview-property {
border-left: 2px solid transparent;
clear: right;
}
.ruleview-property > * {
@ -139,7 +141,10 @@
border-bottom-color: hsl(0,0%,50%);
}
.ruleview-selector {
word-wrap: break-word;
}
.ruleview-selector-separator, .ruleview-selector-unmatched {
color: #888;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -445,18 +445,8 @@
color: #fff;
}
.variables-view-scope > .variables-view-element-details:not(:empty) {
-moz-margin-start: 2px;
-moz-margin-end: 1px;
}
/* Generic variables traits */
.variables-view-variable {
-moz-margin-start: 1px;
-moz-margin-end: 1px;
}
.variables-view-variable:not(:last-child) {
border-bottom: 1px solid rgba(128, 128, 128, .15);
}
@ -475,8 +465,12 @@
-moz-box-flex: 1;
}
.variable-or-property > .title > .arrow {
-moz-margin-start: 3px;
}
.variable-or-property:not([untitled]) > .variables-view-element-details {
-moz-margin-start: 10px;
-moz-margin-start: 7px;
}
/* Traits applied when variables or properties are changed or overridden */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 B

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -324,6 +324,8 @@ browser.jar:
skin/classic/browser/devtools/debugger-blackbox.png (devtools/debugger-blackbox.png)
skin/classic/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
skin/classic/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
skin/classic/browser/devtools/tracer-icon.png (devtools/tracer-icon.png)
skin/classic/browser/devtools/tracer-icon@2x.png (devtools/tracer-icon@2x.png)
skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css)
skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)

View File

@ -850,7 +850,7 @@ menuitem.bookmark-item {
background-color: rgba(255,255,255,.9);
}
#urlbar:-moz-lwtheme[focused],
#urlbar:-moz-lwtheme[focused]:not([readonly]),
.searchbar-textbox:-moz-lwtheme[focused] {
background-color: white;
}

View File

@ -93,6 +93,119 @@
padding: .25em;
}
/* Tracer */
#trace {
list-style-image: url(tracer-icon.png);
-moz-image-region: rect(0px,16px,16px,0px);
}
#trace[checked] {
-moz-image-region: rect(0px,32px,16px,16px);
}
#start-tracing {
padding: 4px;
margin: 4px;
}
#clear-tracer {
min-width: 22px !important;
}
#tracer-search {
min-width: 72px !important;
}
#tracer-message {
/* Prevent the container deck from aquiring the height from this message. */
min-height: 1px;
}
.trace-name {
-moz-padding-start: 4px !important;
}
/* Tracer dark theme */
.theme-dark #tracer-message {
color: #f5f7fa; /* Light foreground text */
background: url(background-noise-toolbar.png) #181d20; /* Content background sidebar */
}
.theme-dark #tracer-traces > scrollbox {
background-color: #181d20 !important; /* Content background sidebar */
}
.theme-dark .trace-item {
color: #f5f7fa; /* Light foreground text */
}
.trace-item.selected-matching {
background-color: #1d4f73; /* Select highlight blue */
}
.theme-dark .trace-call {
color: #46afe3; /* highlight blue */
}
.theme-dark .trace-return,
.theme-dark .trace-yield {
color: #70bf53; /* highlight green */
}
.theme-dark .trace-throw {
color: #eb5368; /* highlight red */
}
.theme-dark .trace-param {
color: #8fa1b2; /* Content text grey */
}
.theme-dark .trace-syntax {
color: #5e88b0; /* highlight blue-grey */
}
/* Tracer light theme */
.theme-light #tracer-message {
color: #292e33; /* Dark foreground text */
background: url(background-noise-toolbar.png) #f7f7f7; /* Content background sidebar */
}
.theme-light #tracer-traces > scrollbox {
background-color: #f7f7f7 !important; /* Content background sidebar */
}
.theme-light .trace-item {
color: #292e33; /* Dark foreground text */
}
.trace-item.selected-matching {
background-color: #4c9ed9; /* Select highlight blue */
}
.theme-light .trace-call {
color: #0088cc; /* highlight blue */
}
.theme-light .trace-return,
.theme-light .trace-yield {
color: #2cbb0f; /* highlight green */
}
.theme-light .trace-throw {
color: #ed2655; /* highlight red */
}
.theme-light .trace-param {
color: #8fa1b2; /* Content text grey */
}
.theme-light .trace-syntax {
color: #5f88b0; /* highlight blue-grey */
}
/* ListWidget items */
.list-widget-item {
@ -226,11 +339,6 @@
/* Instruments pane (watch expressions, variables, event listeners...) */
#instruments-pane > tabs > tab {
min-height: 25px !important;
padding: 0 !important;
}
#instruments-pane .side-menu-widget-container,
#instruments-pane .side-menu-widget-empty-notice-container {
box-shadow: none !important;

View File

@ -22,6 +22,7 @@
padding: 1px 4px;
margin-top: 4px;
-moz-user-select: none;
word-wrap: break-word;
}
.ruleview-rule-source:hover {
@ -115,6 +116,7 @@
.ruleview-property {
border-left: 2px solid transparent;
clear: right;
}
.ruleview-property > * {
@ -135,6 +137,10 @@
border-bottom-color: hsl(0,0%,50%);
}
.ruleview-selector {
word-wrap: break-word;
}
.ruleview-selector-separator, .ruleview-selector-unmatched {
color: #888;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -448,18 +448,8 @@
color: #fff;
}
.variables-view-scope > .variables-view-element-details:not(:empty) {
-moz-margin-start: 2px;
-moz-margin-end: 1px;
}
/* Generic variables traits */
.variables-view-variable {
-moz-margin-start: 1px;
-moz-margin-end: 1px;
}
.variables-view-variable:not(:last-child) {
border-bottom: 1px solid rgba(128, 128, 128, .15);
}
@ -478,8 +468,12 @@
-moz-box-flex: 1;
}
.variable-or-property > .title > .arrow {
-moz-margin-start: 3px;
}
.variable-or-property:not([untitled]) > .variables-view-element-details {
-moz-margin-start: 10px;
-moz-margin-start: 7px;
}
/* Traits applied when variables or properties are changed or overridden */

View File

@ -249,6 +249,8 @@ browser.jar:
skin/classic/browser/devtools/debugger-blackbox.png (devtools/debugger-blackbox.png)
skin/classic/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
skin/classic/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
skin/classic/browser/devtools/tracer-icon.png (devtools/tracer-icon.png)
skin/classic/browser/devtools/tracer-icon@2x.png (devtools/tracer-icon@2x.png)
skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
@ -557,6 +559,8 @@ browser.jar:
skin/classic/aero/browser/devtools/debugger-blackbox.png (devtools/debugger-blackbox.png)
skin/classic/aero/browser/devtools/debugger-blackboxMessageEye.png (devtools/debugger-blackboxMessageEye.png)
skin/classic/aero/browser/devtools/debugger-toggleBreakpoints.png (devtools/debugger-toggleBreakpoints.png)
skin/classic/aero/devtools/tracer-icon.png (devtools/tracer-icon.png)
skin/classic/aero/devtools/tracer-icon@2x.png (devtools/tracer-icon@2x.png)
skin/classic/aero/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
skin/classic/aero/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
skin/classic/aero/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)

View File

@ -324,6 +324,9 @@ DOMMediaStream::OnTracksAvailable(OnTracksAvailableCallback* aRunnable)
void
DOMMediaStream::CheckTracksAvailable()
{
if (mTrackTypesAvailable == 0) {
return;
}
nsTArray<nsAutoPtr<OnTracksAvailableCallback> > callbacks;
callbacks.SwapElements(mRunOnTracksAvailable);

View File

@ -164,6 +164,7 @@ public:
// We only care about track additions, we'll fire the notification even if
// some of the tracks have been removed.
// Takes ownership of aCallback.
// If GetExpectedTracks() returns 0, the callback will be fired as soon as there are any tracks.
void OnTracksAvailable(OnTracksAvailableCallback* aCallback);
/**
@ -182,6 +183,7 @@ protected:
void InitSourceStream(nsIDOMWindow* aWindow, TrackTypeHints aHintContents);
void InitTrackUnionStream(nsIDOMWindow* aWindow, TrackTypeHints aHintContents);
void InitStreamCommon(MediaStream* aStream);
void CheckTracksAvailable();
class StreamListener;

View File

@ -17,12 +17,16 @@
#include "nsIDOMFile.h"
#include "mozilla/dom/BlobEvent.h"
#include "mozilla/dom/AudioStreamTrack.h"
#include "mozilla/dom/VideoStreamTrack.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_INHERITED_1(MediaRecorder, nsDOMEventTargetHelper,
mStream)
NS_IMPL_CYCLE_COLLECTION_INHERITED_2(MediaRecorder, nsDOMEventTargetHelper,
mStream, mSession)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder)
NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper)
@ -45,7 +49,7 @@ NS_IMPL_RELEASE_INHERITED(MediaRecorder, nsDOMEventTargetHelper)
*
* Life cycle of a Session object.
* 1) Initialization Stage (in main thread)
* Setup media streams in MSG, and bind MediaEncoder with Source Stream.
* Setup media streams in MSG, and bind MediaEncoder with Source Stream when mStream is available.
* Resource allocation, such as encoded data cache buffer and MediaEncoder.
* Create read thread.
* Automatically switch to Extract stage in the end of this stage.
@ -90,7 +94,7 @@ class MediaRecorder::Session: public nsIObserver
}
private:
Session *mSession;
nsRefPtr<Session> mSession;
};
// Record thread task.
@ -110,7 +114,36 @@ class MediaRecorder::Session: public nsIObserver
}
private:
Session *mSession;
nsRefPtr<Session> mSession;
};
// For Ensure recorder has tracks to record.
class TracksAvailableCallback : public DOMMediaStream::OnTracksAvailableCallback
{
public:
TracksAvailableCallback(Session *aSession)
: mSession(aSession) {}
virtual void NotifyTracksAvailable(DOMMediaStream* aStream)
{
uint8_t trackType = aStream->GetHintContents();
// ToDo: GetHintContents return 0 when recording media tags.
if (trackType == 0) {
nsTArray<nsRefPtr<mozilla::dom::AudioStreamTrack> > audioTracks;
aStream->GetAudioTracks(audioTracks);
nsTArray<nsRefPtr<mozilla::dom::VideoStreamTrack> > videoTracks;
aStream->GetVideoTracks(videoTracks);
// What is inside the track
if (videoTracks.Length() > 0) {
trackType |= DOMMediaStream::HINT_CONTENTS_VIDEO;
}
if (audioTracks.Length() > 0) {
trackType |= DOMMediaStream::HINT_CONTENTS_AUDIO;
}
}
mSession->AfterTracksAdded(trackType);
}
private:
nsRefPtr<Session> mSession;
};
// Main thread task.
@ -154,6 +187,7 @@ class MediaRecorder::Session: public nsIObserver
friend class PushBlobRunnable;
friend class ExtractRunnable;
friend class DestroyRunnable;
friend class TracksAvailableCallback;
public:
Session(MediaRecorder* aRecorder, int32_t aTimeSlice)
@ -169,8 +203,6 @@ public:
// Only DestroyRunnable is allowed to delete Session object.
virtual ~Session()
{
MOZ_ASSERT(NS_IsMainThread());
CleanupStreams();
}
@ -179,22 +211,6 @@ public:
MOZ_ASSERT(NS_IsMainThread());
SetupStreams();
// Create a thread to read encode media data from MediaEncoder.
if (!mReadThread) {
nsresult rv = NS_NewNamedThread("Media Encoder", getter_AddRefs(mReadThread));
if (NS_FAILED(rv)) {
CleanupStreams();
mRecorder->NotifyError(rv);
return;
}
}
// In case source media stream does not notify track end, recieve
// shutdown notification and stop Read Thread.
nsContentUtils::RegisterShutdownObserver(this);
mReadThread->Dispatch(new ExtractRunnable(this), NS_DISPATCH_NORMAL);
}
void Stop()
@ -290,14 +306,57 @@ private:
mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT);
// Allocate encoder and bind with the Track Union Stream.
mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""));
MOZ_ASSERT(mEncoder, "CreateEncoder failed");
if (mEncoder) {
mTrackUnionStream->AddListener(mEncoder);
}
TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(mRecorder->mSession);
mRecorder->mStream->OnTracksAvailable(tracksAvailableCallback);
}
void AfterTracksAdded(uint8_t aTrackTypes)
{
MOZ_ASSERT(NS_IsMainThread());
// Allocate encoder and bind with union stream.
// At this stage, the API doesn't allow UA to choose the output mimeType format.
mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""), aTrackTypes);
if (!mEncoder) {
DoSessionEndTask(NS_ERROR_ABORT);
return;
}
// media stream is ready but has been issued stop command
if (mRecorder->mState == RecordingState::Inactive) {
DoSessionEndTask(NS_OK);
return;
}
mTrackUnionStream->AddListener(mEncoder);
// Create a thread to read encode media data from MediaEncoder.
if (!mReadThread) {
nsresult rv = NS_NewNamedThread("Media Encoder", getter_AddRefs(mReadThread));
if (NS_FAILED(rv)) {
DoSessionEndTask(rv);
return;
}
}
// In case source media stream does not notify track end, recieve
// shutdown notification and stop Read Thread.
nsContentUtils::RegisterShutdownObserver(this);
mReadThread->Dispatch(new ExtractRunnable(this), NS_DISPATCH_NORMAL);
}
// application should get blob and onstop event
void DoSessionEndTask(nsresult rv)
{
MOZ_ASSERT(NS_IsMainThread());
if (NS_FAILED(rv)) {
mRecorder->NotifyError(rv);
}
CleanupStreams();
// Destroy this session object in main thread.
NS_DispatchToMainThread(new PushBlobRunnable(this));
NS_DispatchToMainThread(new DestroyRunnable(already_AddRefed<Session>(this)));
}
void CleanupStreams()
{
if (mInputPort.get()) {
@ -430,11 +489,10 @@ MediaRecorder::Stop(ErrorResult& aResult)
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
mState = RecordingState::Inactive;
mSession->Stop();
mSession = nullptr;
mState = RecordingState::Inactive;
}
void

View File

@ -104,7 +104,7 @@ protected:
// The current state of the MediaRecorder object.
RecordingState mState;
// Current recording session.
Session *mSession;
nsRefPtr<Session> mSession;
// Thread safe for mMimeType.
Mutex mMutex;
// It specifies the container format as well as the audio and video capture formats.

View File

@ -19,9 +19,14 @@ class ContainerWriter {
public:
ContainerWriter()
: mInitialized(false)
, mIsWritingComplete(false)
{}
virtual ~ContainerWriter() {}
// Mapping to DOMLocalMediaStream::TrackTypeHints
enum {
HAS_AUDIO = 1 << 0,
HAS_VIDEO = 1 << 1,
};
enum {
END_OF_STREAM = 1 << 0
};
@ -44,6 +49,11 @@ public:
*/
virtual nsresult SetMetadata(TrackMetadataBase* aMetadata) = 0;
/**
* Indicate if the writer has finished to output data
*/
virtual bool IsWritingComplete() { return mIsWritingComplete; }
enum {
FLUSH_NEEDED = 1 << 0,
GET_HEADER = 1 << 1
@ -59,9 +69,9 @@ public:
*/
virtual nsresult GetContainerData(nsTArray<nsTArray<uint8_t> >* aOutputBufs,
uint32_t aFlags = 0) = 0;
protected:
bool mInitialized;
bool mIsWritingComplete;
};
}
#endif

View File

@ -52,6 +52,8 @@ public:
P_FRAME, // predicted frame
B_FRAME, // bidirectionally predicted frame
AUDIO_FRAME, // audio frame
AAC_CSD, // AAC codec specific data
AVC_CSD, // AVC codec specific data
UNKNOW // FrameType not set
};
const nsTArray<uint8_t>& GetFrameData() const

View File

@ -5,6 +5,8 @@
#include "MediaEncoder.h"
#include "MediaDecoder.h"
#include "nsIPrincipal.h"
#include "nsMimeTypes.h"
#include "prlog.h"
#ifdef MOZ_OGG
#include "OggWriter.h"
@ -12,50 +14,30 @@
#ifdef MOZ_OPUS
#include "OpusTrackEncoder.h"
#endif
#ifdef MOZ_WEBM_ENCODER
#include "VorbisTrackEncoder.h"
#include "VP8TrackEncoder.h"
#include "WebMWriter.h"
#endif
#ifdef MOZ_OMX_ENCODER
#include "OmxTrackEncoder.h"
#include "ISOMediaWriter.h"
#endif
#ifdef MOZ_WIDGET_GONK
#include <android/log.h>
#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "MediaEncoder", ## args);
#ifdef LOG
#undef LOG
#endif
#ifdef PR_LOGGING
PRLogModuleInfo* gMediaEncoderLog;
#define LOG(type, msg) PR_LOG(gMediaEncoderLog, type, msg)
#else
#define LOG(args,...)
#define LOG(type, msg)
#endif
namespace mozilla {
#define TRACK_BUFFER_LEN 8192
namespace {
template <class String>
static bool
TypeListContains(char const *const * aTypes, const String& aType)
{
for (int32_t i = 0; aTypes[i]; ++i) {
if (aType.EqualsASCII(aTypes[i]))
return true;
}
return false;
}
#ifdef MOZ_OGG
// The recommended mime-type for Ogg Opus files is audio/ogg.
// See http://wiki.xiph.org/OggOpus for more details.
static const char* const gOggTypes[2] = {
"audio/ogg",
nullptr
};
static bool
IsOggType(const nsAString& aType)
{
if (!MediaDecoder::IsOggEnabled()) {
return false;
}
return TypeListContains(gOggTypes, aType);
}
#endif
} //anonymous namespace
static nsIThread* sEncoderThread = nullptr;
void
MediaEncoder::NotifyQueuedTrackChanges(MediaStreamGraph* aGraph,
@ -67,13 +49,15 @@ MediaEncoder::NotifyQueuedTrackChanges(MediaStreamGraph* aGraph,
{
// Process the incoming raw track data from MediaStreamGraph, called on the
// thread of MediaStreamGraph.
if (aQueuedMedia.GetType() == MediaSegment::AUDIO) {
if (mAudioEncoder && aQueuedMedia.GetType() == MediaSegment::AUDIO) {
mAudioEncoder->NotifyQueuedTrackChanges(aGraph, aID, aTrackRate,
aTrackOffset, aTrackEvents,
aQueuedMedia);
} else {
// Type video is not supported for now.
} else if (mVideoEncoder && aQueuedMedia.GetType() == MediaSegment::VIDEO) {
mVideoEncoder->NotifyQueuedTrackChanges(aGraph, aID, aTrackRate,
aTrackOffset, aTrackEvents,
aQueuedMedia);
}
}
@ -81,49 +65,88 @@ void
MediaEncoder::NotifyRemoved(MediaStreamGraph* aGraph)
{
// In case that MediaEncoder does not receive a TRACK_EVENT_ENDED event.
LOG("NotifyRemoved in [MediaEncoder].");
mAudioEncoder->NotifyRemoved(aGraph);
LOG(PR_LOG_DEBUG, ("NotifyRemoved in [MediaEncoder]."));
if (mAudioEncoder) {
mAudioEncoder->NotifyRemoved(aGraph);
}
if (mVideoEncoder) {
mVideoEncoder->NotifyRemoved(aGraph);
}
}
bool
MediaEncoder::OnEncoderThread()
{
return NS_GetCurrentThread() == sEncoderThread;
}
/* static */
already_AddRefed<MediaEncoder>
MediaEncoder::CreateEncoder(const nsAString& aMIMEType)
MediaEncoder::CreateEncoder(const nsAString& aMIMEType, uint8_t aTrackTypes)
{
#ifdef PR_LOGGING
if (!gMediaEncoderLog) {
gMediaEncoderLog = PR_NewLogModule("MediaEncoder");
}
#endif
nsAutoPtr<ContainerWriter> writer;
nsAutoPtr<AudioTrackEncoder> audioEncoder;
nsAutoPtr<VideoTrackEncoder> videoEncoder;
nsRefPtr<MediaEncoder> encoder;
if (aMIMEType.IsEmpty()) {
// TODO: Should pick out a default container+codec base on the track
// coming from MediaStreamGraph. For now, just default to Ogg+Opus.
const_cast<nsAString&>(aMIMEType) = NS_LITERAL_STRING("audio/ogg");
nsString mimeType;
if (!aTrackTypes) {
LOG(PR_LOG_ERROR, ("NO TrackTypes!!!"));
return nullptr;
}
bool isAudioOnly = FindInReadable(NS_LITERAL_STRING("audio/"), aMIMEType);
#ifdef MOZ_OGG
if (IsOggType(aMIMEType)) {
writer = new OggWriter();
if (!isAudioOnly) {
// Initialize the videoEncoder.
#ifdef MOZ_WEBM_ENCODER
else if (MediaDecoder::IsWebMEnabled() &&
(aMIMEType.EqualsLiteral(VIDEO_WEBM) ||
(aTrackTypes & ContainerWriter::HAS_VIDEO))) {
if (aTrackTypes & ContainerWriter::HAS_AUDIO) {
audioEncoder = new VorbisTrackEncoder();
NS_ENSURE_TRUE(audioEncoder, nullptr);
}
#ifdef MOZ_OPUS
audioEncoder = new OpusTrackEncoder();
#endif
}
#endif
// If the given mime-type is video but fail to create the video encoder.
if (!isAudioOnly) {
videoEncoder = new VP8TrackEncoder();
writer = new WebMWriter(aTrackTypes);
NS_ENSURE_TRUE(writer, nullptr);
NS_ENSURE_TRUE(videoEncoder, nullptr);
mimeType = NS_LITERAL_STRING(VIDEO_WEBM);
}
// Return null if we fail to create the audio encoder.
NS_ENSURE_TRUE(audioEncoder, nullptr);
#endif //MOZ_WEBM_ENCODER
#ifdef MOZ_OMX_ENCODER
else if (aMIMEType.EqualsLiteral(VIDEO_MP4) ||
(aTrackTypes & ContainerWriter::HAS_VIDEO)) {
if (aTrackTypes & ContainerWriter::HAS_AUDIO) {
audioEncoder = new OmxAudioTrackEncoder();
NS_ENSURE_TRUE(audioEncoder, nullptr);
}
videoEncoder = new OmxVideoTrackEncoder();
writer = new ISOMediaWriter(aTrackTypes);
NS_ENSURE_TRUE(writer, nullptr);
NS_ENSURE_TRUE(videoEncoder, nullptr);
mimeType = NS_LITERAL_STRING(VIDEO_MP4);
}
#endif // MOZ_OMX_ENCODER
#ifdef MOZ_OGG
else if (MediaDecoder::IsOggEnabled() && MediaDecoder::IsOpusEnabled() &&
(aMIMEType.EqualsLiteral(AUDIO_OGG) ||
(aTrackTypes & ContainerWriter::HAS_AUDIO))) {
writer = new OggWriter();
audioEncoder = new OpusTrackEncoder();
NS_ENSURE_TRUE(writer, nullptr);
NS_ENSURE_TRUE(audioEncoder, nullptr);
mimeType = NS_LITERAL_STRING(AUDIO_OGG);
}
#endif // MOZ_OGG
else {
LOG(PR_LOG_ERROR, ("Can not find any encoder to record this media stream"));
return nullptr;
}
LOG(PR_LOG_DEBUG, ("Create encoder result:a[%d] v[%d] w[%d] mimeType = %s.",
audioEncoder != nullptr, videoEncoder != nullptr,
writer != nullptr, mimeType.get()));
encoder = new MediaEncoder(writer.forget(), audioEncoder.forget(),
videoEncoder.forget(), aMIMEType);
videoEncoder.forget(), mimeType);
return encoder.forget();
}
@ -156,75 +179,75 @@ MediaEncoder::GetEncodedData(nsTArray<nsTArray<uint8_t> >* aOutputBufs,
nsAString& aMIMEType)
{
MOZ_ASSERT(!NS_IsMainThread());
if (!sEncoderThread) {
sEncoderThread = NS_GetCurrentThread();
}
aMIMEType = mMIMEType;
bool reloop = true;
while (reloop) {
switch (mState) {
case ENCODE_METADDATA: {
nsRefPtr<TrackMetadataBase> meta = mAudioEncoder->GetMetadata();
if (meta == nullptr) {
LOG("ERROR! AudioEncoder get null Metadata!");
mState = ENCODE_ERROR;
LOG(PR_LOG_DEBUG, ("ENCODE_METADDATA TimeStamp = %f", GetEncodeTimeStamp()));
nsresult rv = CopyMetadataToMuxer(mAudioEncoder.get());
if (NS_FAILED(rv)) {
LOG(PR_LOG_ERROR, ("Error! Fail to Set Audio Metadata"));
break;
}
nsresult rv = mWriter->SetMetadata(meta);
rv = CopyMetadataToMuxer(mVideoEncoder.get());
if (NS_FAILED(rv)) {
LOG("ERROR! writer can't accept audio metadata!");
mState = ENCODE_ERROR;
break;
LOG(PR_LOG_ERROR, ("Error! Fail to Set Video Metadata"));
break;
}
rv = mWriter->GetContainerData(aOutputBufs,
ContainerWriter::GET_HEADER);
if (NS_FAILED(rv)) {
LOG("ERROR! writer fail to generate header!");
LOG(PR_LOG_ERROR,("Error! writer fail to generate header!"));
mState = ENCODE_ERROR;
break;
}
LOG(PR_LOG_DEBUG, ("Finish ENCODE_METADDATA TimeStamp = %f", GetEncodeTimeStamp()));
mState = ENCODE_TRACK;
break;
}
case ENCODE_TRACK: {
LOG(PR_LOG_DEBUG, ("ENCODE_TRACK TimeStamp = %f", GetEncodeTimeStamp()));
EncodedFrameContainer encodedData;
nsresult rv = mAudioEncoder->GetEncodedTrack(encodedData);
nsresult rv = NS_OK;
rv = WriteEncodedDataToMuxer(mAudioEncoder.get());
if (NS_FAILED(rv)) {
// Encoding might be canceled.
LOG("ERROR! Fail to get encoded data from encoder.");
mState = ENCODE_ERROR;
LOG(PR_LOG_ERROR, ("Error! Fail to write audio encoder data to muxer"));
break;
}
rv = mWriter->WriteEncodedTrack(encodedData,
mAudioEncoder->IsEncodingComplete() ?
ContainerWriter::END_OF_STREAM : 0);
LOG(PR_LOG_DEBUG, ("Audio encoded TimeStamp = %f", GetEncodeTimeStamp()));
rv = WriteEncodedDataToMuxer(mVideoEncoder.get());
if (NS_FAILED(rv)) {
LOG("ERROR! Fail to write encoded track to the media container.");
mState = ENCODE_ERROR;
LOG(PR_LOG_ERROR, ("Fail to write video encoder data to muxer"));
break;
}
LOG(PR_LOG_DEBUG, ("Video encoded TimeStamp = %f", GetEncodeTimeStamp()));
// In audio only or video only case, let unavailable track's flag to be true.
bool isAudioCompleted = (mAudioEncoder && mAudioEncoder->IsEncodingComplete()) || !mAudioEncoder;
bool isVideoCompleted = (mVideoEncoder && mVideoEncoder->IsEncodingComplete()) || !mVideoEncoder;
rv = mWriter->GetContainerData(aOutputBufs,
mAudioEncoder->IsEncodingComplete() ?
isAudioCompleted && isVideoCompleted ?
ContainerWriter::FLUSH_NEEDED : 0);
if (NS_SUCCEEDED(rv)) {
// Successfully get the copy of final container data from writer.
reloop = false;
}
mState = (mAudioEncoder->IsEncodingComplete()) ? ENCODE_DONE : ENCODE_TRACK;
mState = (mWriter->IsWritingComplete()) ? ENCODE_DONE : ENCODE_TRACK;
LOG(PR_LOG_DEBUG, ("END ENCODE_TRACK TimeStamp = %f "
"mState = %d aComplete %d vComplete %d",
GetEncodeTimeStamp(), mState, isAudioCompleted, isVideoCompleted));
break;
}
case ENCODE_DONE:
LOG("MediaEncoder has been shutdown.");
mShutdown = true;
reloop = false;
break;
case ENCODE_ERROR:
LOG("ERROR! MediaEncoder got error!");
LOG(PR_LOG_DEBUG, ("MediaEncoder has been shutdown."));
mShutdown = true;
reloop = false;
break;
@ -234,4 +257,52 @@ MediaEncoder::GetEncodedData(nsTArray<nsTArray<uint8_t> >* aOutputBufs,
}
}
nsresult
MediaEncoder::WriteEncodedDataToMuxer(TrackEncoder *aTrackEncoder)
{
if (aTrackEncoder == nullptr) {
return NS_OK;
}
if (aTrackEncoder->IsEncodingComplete()) {
return NS_OK;
}
EncodedFrameContainer encodedVideoData;
nsresult rv = aTrackEncoder->GetEncodedTrack(encodedVideoData);
if (NS_FAILED(rv)) {
// Encoding might be canceled.
LOG(PR_LOG_ERROR, ("Error! Fail to get encoded data from video encoder."));
mState = ENCODE_ERROR;
return rv;
}
rv = mWriter->WriteEncodedTrack(encodedVideoData,
aTrackEncoder->IsEncodingComplete() ?
ContainerWriter::END_OF_STREAM : 0);
if (NS_FAILED(rv)) {
LOG(PR_LOG_ERROR, ("Error! Fail to write encoded video track to the media container."));
mState = ENCODE_ERROR;
}
return rv;
}
nsresult
MediaEncoder::CopyMetadataToMuxer(TrackEncoder *aTrackEncoder)
{
if (aTrackEncoder == nullptr) {
return NS_OK;
}
nsRefPtr<TrackMetadataBase> meta = aTrackEncoder->GetMetadata();
if (meta == nullptr) {
LOG(PR_LOG_ERROR, ("Error! metadata = null"));
mState = ENCODE_ERROR;
return NS_ERROR_ABORT;
}
nsresult rv = mWriter->SetMetadata(meta);
if (NS_FAILED(rv)) {
LOG(PR_LOG_ERROR, ("Error! SetMetadata fail"));
mState = ENCODE_ERROR;
}
return rv;
}
}

View File

@ -64,6 +64,7 @@ public :
: mWriter(aWriter)
, mAudioEncoder(aAudioEncoder)
, mVideoEncoder(aVideoEncoder)
, mStartTime(TimeStamp::Now())
, mMIMEType(aMIMEType)
, mState(MediaEncoder::ENCODE_METADDATA)
, mShutdown(false)
@ -91,8 +92,12 @@ public :
* to create the encoder. For now, default aMIMEType to "audio/ogg" and use
* Ogg+Opus if it is empty.
*/
static already_AddRefed<MediaEncoder> CreateEncoder(const nsAString& aMIMEType);
static already_AddRefed<MediaEncoder> CreateEncoder(const nsAString& aMIMEType,
uint8_t aTrackTypes = ContainerWriter::HAS_AUDIO);
/**
* Check if run on Encoder thread
*/
static bool OnEncoderThread();
/**
* Encodes the raw track data and returns the final container data. Assuming
* it is called on a single worker thread. The buffer of container data is
@ -128,12 +133,24 @@ public :
}
private:
// Get encoded data from trackEncoder and write to muxer
nsresult WriteEncodedDataToMuxer(TrackEncoder *aTrackEncoder);
// Get metadata from trackEncoder and copy to muxer
nsresult CopyMetadataToMuxer(TrackEncoder* aTrackEncoder);
nsAutoPtr<ContainerWriter> mWriter;
nsAutoPtr<AudioTrackEncoder> mAudioEncoder;
nsAutoPtr<VideoTrackEncoder> mVideoEncoder;
TimeStamp mStartTime;
nsString mMIMEType;
int mState;
bool mShutdown;
// Get duration from create encoder, for logging purpose
double GetEncodeTimeStamp()
{
TimeDuration decodeTime;
decodeTime = TimeStamp::Now() - mStartTime;
return decodeTime.ToMilliseconds();
}
};
}

View File

@ -162,7 +162,9 @@ OggWriter::GetContainerData(nsTArray<nsTArray<uint8_t> >* aOutputBufs,
if (rc) {
ProduceOggPage(aOutputBufs);
}
if (aFlags & ContainerWriter::FLUSH_NEEDED) {
mIsWritingComplete = true;
}
return (rc > 0) ? NS_OK : NS_ERROR_FAILURE;
}

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