merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2015-02-26 12:00:41 +01:00
Родитель f8ac7262e0 8e54462bcd
Коммит c658a426c3
134 изменённых файлов: 7197 добавлений и 1430 удалений

Просмотреть файл

@ -1,6 +1,7 @@
sudo: false
language: node_js
node_js:
- "iojs"
- "0.10"
notifications:

Просмотреть файл

@ -17,12 +17,14 @@ If you have code that you'd like to contribute the Jetpack project, follow these
3. Make your changes, per the Overview
4. Write a test ([intro][test intro], [API][test API])
5. Submit pull request with changes and a title in a form of `Bug XXX - description`
6. Copy the pull request link from GitHub and paste it in as an attachment to the bug
7. Each pull request should idealy contain only one commit, so squash the commits if necessary.
8. Flag the attachment for code review from one of the Jetpack reviewers listed below.
6. Make sure that [Travis CI](https://travis-ci.org/mozilla/addon-sdk/branches) tests are passing for your branch.
7. Copy the pull request link from GitHub and paste it in as an attachment to the bug
8. Each pull request should idealy contain only one commit, so squash the commits if necessary.
9. Flag the attachment for code review from one of the Jetpack reviewers listed below.
This step is optional, but could speed things up.
9. Address any nits (ie style changes), or other issues mentioned in the review.
10. Finally, once review is approved, a team member will do the merging
10. Address any nits (ie style changes), or other issues mentioned in the review.
Finally, once review is approved, a team member will do the merging
## Good First Bugs

Просмотреть файл

@ -81,14 +81,12 @@ function startup(reason, options) Startup.onceInitialized.then(() => {
// Exports data to a pseudo module so that api-utils/l10n/core
// can get access to it
definePseudo(options.loader, '@l10n/data', data ? data : null);
return ready;
}).then(function() {
run(options);
return ready.then(() => run(options, !!data));
}).then(null, console.exception);
return void 0; // otherwise we raise a warning, see bug 910304
});
function run(options) {
function run(options, hasL10n) {
try {
// Try initializing HTML localization before running main module. Just print
// an exception in case of error, instead of preventing addon to be run.
@ -96,7 +94,7 @@ function run(options) {
// Do not enable HTML localization while running test as it is hard to
// disable. Because unit tests are evaluated in a another Loader who
// doesn't have access to this current loader.
if (options.main !== 'sdk/test/runner') {
if (hasL10n && options.main !== 'sdk/test/runner') {
require('../l10n/html').enable();
}
}

Просмотреть файл

@ -340,7 +340,7 @@ function getPotentialLeaks() {
let item = {
path: matches[1],
principal: details[1],
location: details[2] ? details[2].replace("\\", "/", "g") : undefined,
location: details[2] ? details[2].replace(/\\/g, "/") : undefined,
source: details[3] ? details[3].split(" -> ").reverse() : undefined,
toString: function() this.location
};
@ -364,8 +364,8 @@ function getPotentialLeaks() {
let item = {
path: matches[1],
location: details[1].replace("\\", "/", "g"),
source: [details[1].replace("\\", "/", "g")],
location: details[1].replace(/\\/g, "/"),
source: [details[1].replace(/\\/g, "/")],
toString: function() this.location
};

Просмотреть файл

@ -12,6 +12,7 @@ const { emit } = require("../event/core");
const { WindowTracker, windowIterator } = require("../deprecated/window-utils");
const { DOMEventAssembler } = require("../deprecated/events/assembler");
const { Class } = require("../core/heritage");
const { Cu } = require("chrome");
// Event emitter objects used to register listeners and emit events on them
// when they occur.
@ -42,6 +43,9 @@ const Observer = Class({
* Keyboard event being emitted.
*/
handleEvent(event) {
// Ignore events from windows in the child process as they can't be top-level
if (Cu.isCrossProcessWrapper(event.target))
return;
emit(this, event.type, event.target, event);
}
});

Просмотреть файл

@ -703,7 +703,8 @@ Loader.main = main;
const Module = iced(function Module(id, uri) {
return create(null, {
id: { enumerable: true, value: id },
exports: { enumerable: true, writable: true, value: create(null) },
exports: { enumerable: true, writable: true, value: create(null),
configurable: true },
uri: { value: uri }
});
});
@ -784,6 +785,7 @@ function Loader(options) {
}
}, modules);
const builtinModuleExports = modules;
modules = keys(modules).reduce(function(result, id) {
// We resolve `uri` from `id` since modules are cached by `uri`.
let uri = resolveURI(id, mapping);
@ -792,7 +794,16 @@ function Loader(options) {
if (isNative && !uri)
uri = id;
let module = Module(id, uri);
module.exports = freeze(modules[id]);
// Lazily expose built-in modules in order to
// allow them to be loaded lazily.
Object.defineProperty(module, "exports", {
enumerable: true,
get: function() {
return builtinModuleExports[id];
}
});
result[uri] = freeze(module);
return result;
}, {});

Просмотреть файл

@ -60,6 +60,7 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report',
'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
'browser.selfsupport.url': 'http://localhost/repair-dummy',
'browser.trackingprotection.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
'browser.trackingprotection.updateURL': 'http://localhost/safebrowsing-dummy/update',

Просмотреть файл

@ -10,7 +10,8 @@ const { fromIterator } = require('sdk/util/array');
const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
'menu_socialSidebar',
'menu_historySidebar',
'menu_bookmarksSidebar'
'menu_bookmarksSidebar',
'menu_readingListSidebar'
];
function isSidebarShowing(window) {

Просмотреть файл

@ -129,8 +129,7 @@ exports.testDestroyEdgeCaseBugWithPrivateWindow = function(assert, done) {
assert.pass('onShow works for Sidebar');
loader.unload();
let sidebarMI = getSidebarMenuitems();
for (let mi of sidebarMI) {
for (let mi of getSidebarMenuitems()) {
assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
}

9
addon-sdk/source/test/fixtures/loader/lazy/main.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
exports.useFoo= function () {
return require('foo');
}

Просмотреть файл

@ -17,6 +17,7 @@
"browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
"browser.safebrowsing.reportURL": "http://localhost/safebrowsing-dummy/report",
"browser.safebrowsing.malware.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
"browser.selfsupport.url": "http://localhost/repair-dummy",
"browser.trackingprotection.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
"browser.trackingprotection.updateURL": "http://localhost/safebrowsing-dummy/update",
"browser.newtabpage.directory.source": "data:application/json,{'jetpack':1}",

Просмотреть файл

@ -16,7 +16,8 @@ const { fromIterator } = require('sdk/util/array');
const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
'menu_socialSidebar',
'menu_historySidebar',
'menu_bookmarksSidebar'
'menu_bookmarksSidebar',
'menu_readingListSidebar'
];
function isSidebarShowing(window) {

Просмотреть файл

@ -506,4 +506,22 @@ exports["test Cu.import in b2g style"] = (assert) => {
"Loader.Loader is a funciton");
};
exports['test lazy globals'] = function (assert) {
let uri = root + '/fixtures/loader/lazy/';
let gotFoo = false;
let foo = {};
let modules = {
get foo() {
gotFoo = true;
return foo;
}
};
let loader = Loader({ paths: { '': uri }, modules: modules});
assert.ok(!gotFoo, "foo hasn't been accessed during loader instanciation");
let program = main(loader, 'main');
assert.ok(!gotFoo, "foo hasn't been accessed during module loading");
assert.equal(program.useFoo(), foo, "foo mock works");
assert.ok(gotFoo, "foo has been accessed only when we first try to use it");
};
require('sdk/test').run(exports);

Просмотреть файл

@ -29,6 +29,17 @@ exports.testGetAndSet = function(assert) {
"preferences-service should read from " +
"application-wide preferences service");
// test getting a pref that does not exist,
// and where we provide no default
assert.equal(
prefs.get("test_dne_get_pref", "default"),
"default",
"default was used for a pref that does not exist");
assert.equal(
prefs.get("test_dne_get_pref"),
undefined,
"undefined was returned for a pref that does not exist with no default");
prefs.set("test_set_get_pref.integer", 1);
assert.equal(prefs.get("test_set_get_pref.integer"), 1,
"set/get integer preference should work");

Просмотреть файл

@ -96,9 +96,12 @@ exports.testSidebarBasicLifeCycle = function*(assert) {
sidebar.destroy();
sidebar.destroy();
let sidebarMI = getSidebarMenuitems();
for (let mi of sidebarMI) {
assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
for (let mi of getSidebarMenuitems()) {
let id = mi.getAttribute('id');
if (BUILTIN_SIDEBAR_MENUITEMS.indexOf(id) < 0) {
assert.fail('the menuitem "' + id + '" is not a built-in sidebar');
}
assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
}
@ -360,8 +363,7 @@ exports.testSidebarUnload = function*(assert) {
loader.unload();
let sidebarMI = getSidebarMenuitems();
for (let mi of sidebarMI) {
for (let mi of getSidebarMenuitems()) {
assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
}
@ -598,9 +600,12 @@ exports.testDestroyEdgeCaseBug = function*(assert) {
yield sidebar.show();
loader.unload();
let sidebarMI = getSidebarMenuitems();
for (let mi of sidebarMI) {
assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
for (let mi of getSidebarMenuitems()) {
let id = mi.getAttribute('id');
if (BUILTIN_SIDEBAR_MENUITEMS.indexOf(id) < 0) {
assert.fail('the menuitem "' + id + '" is not a built-in sidebar');
}
assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
}
assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
@ -762,6 +767,9 @@ exports.testURLSetterToSameValueReloadsSidebar = function*(assert) {
document = window.document;
assert.pass('new window was opened');
yield focus(window);
assert.pass('new window was focused');
yield sidebar1.show();
assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
@ -1490,23 +1498,29 @@ exports.testShowHideRawWindowArg = function*(assert) {
});
let mainWindow = getMostRecentBrowserWindow();
let newWindow = yield open().then(focus);
let newWindow = yield windowPromise(mainWindow.OpenBrowserWindow(), 'load');
assert.pass("Created the new window");
yield focus(newWindow);
assert.pass("Focused the new window");
yield focus(mainWindow);
assert.pass("Focused the old window");
yield sidebar.show(newWindow);
assert.pass('the sidebar was shown');
assert.ok(!isSidebarShowing(mainWindow), 'sidebar is not showing in main window');
assert.ok(isSidebarShowing(newWindow), 'sidebar is showing in new window');
assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
assert.equal(isSidebarShowing(newWindow), true, 'sidebar is showing in new window');
assert.ok(isFocused(mainWindow), 'main window is still focused');
yield sidebar.hide(newWindow);
assert.ok(isFocused(mainWindow), 'main window is still focused');
assert.ok(!isSidebarShowing(mainWindow), 'sidebar is not showing in main window');
assert.ok(!isSidebarShowing(newWindow), 'sidebar is not showing in new window');
assert.equal(isFocused(mainWindow), true, 'main window is still focused');
assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
assert.equal(isSidebarShowing(newWindow), false, 'sidebar is not showing in new window');
sidebar.destroy();
}
@ -1547,6 +1561,7 @@ before(exports, (name, assert) => {
});
after(exports, function*(name, assert) {
assert.pass("Cleaning new windows and tabs");
yield cleanUI();
assert.equal(isSidebarShowing(), false, 'no sidebar is showing');
});

Просмотреть файл

@ -5,10 +5,7 @@
const { Cu } = require('chrome');
const { Loader } = require('sdk/test/loader');
function gc() {
return new Promise(resolve => Cu.schedulePreciseGC(resolve));
};
const { gc } = require("sdk/test/memory");
exports['test adding item'] = function*(assert) {
let loader = Loader(module);
@ -121,31 +118,31 @@ exports['test adding non object or null item'] = function(assert) {
assert.throws(() => {
add(items, 'foo');
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(items, 0);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(items, undefined);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(items, null);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(items, true);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
};
@ -158,31 +155,31 @@ exports['test adding to non object or null item'] = function(assert) {
assert.throws(() => {
add('foo', item);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(0, item);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(undefined, item);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(null, item);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
assert.throws(() => {
add(true, item);
},
/^value is not a non-null object/,
/^\w+ is not a non-null object/,
'only non-null object are allowed');
};

Просмотреть файл

@ -1336,7 +1336,7 @@ pref("browser.devedition.theme.showCustomizeButton", false);
// Developer edition promo preferences
pref("devtools.devedition.promo.shown", false);
pref("devtools.devedition.promo.url", "https://mozilla.org/firefox/developer");
pref("devtools.devedition.promo.url", "https://www.mozilla.org/firefox/developer/?utm_source=firefox-dev-tools&utm_medium=firefox-browser&utm_content=betadoorhanger");
// Only potentially show in beta release
#if MOZ_UPDATE_CHANNEL == beta
@ -1438,6 +1438,9 @@ pref("devtools.timeline.enabled", true);
#else
pref("devtools.timeline.enabled", false);
#endif
// TODO remove `devtools.timeline.hiddenMarkers.` branches when performance
// tool lands (bug 1075567)
pref("devtools.timeline.hiddenMarkers", "[]");
// Enable perftools via build command
@ -1455,6 +1458,7 @@ pref("devtools.profiler.ui.show-platform-data", false);
pref("devtools.profiler.ui.show-idle-blocks", true);
// The default Performance UI settings
pref("devtools.performance.timeline.hidden-markers", "[]");
pref("devtools.performance.ui.invert-call-tree", true);
pref("devtools.performance.ui.invert-flame-graph", false);
pref("devtools.performance.ui.flatten-tree-recursion", true);

Просмотреть файл

@ -688,7 +688,6 @@ let SessionStoreInternal = {
Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null);
}
delete browser.__SS_restore_data;
delete browser.__SS_data;
SessionStoreInternal._resetLocalTabRestoringState(tab);
@ -2683,26 +2682,16 @@ let SessionStoreInternal = {
},
/**
* Restores the specified tab. If the tab can't be restored (eg, no history or
* calling gotoIndex fails), then state changes will be rolled back.
* This method will check if gTabsProgressListener is attached to the tab's
* window, ensuring that we don't get caught without one.
* This method removes the session history listener right before starting to
* attempt a load. This will prevent cases of "stuck" listeners.
* If this method returns false, then it is up to the caller to decide what to
* do. In the common case (restoreNextTab), we will want to then attempt to
* restore the next tab. In the other case (selecting the tab, reloading the
* tab), the caller doesn't actually want to do anything if no page is loaded.
* Kicks off restoring the given tab.
*
* @param aTab
* the tab to restore
*
* @returns true/false indicating whether or not a load actually happened
* @param aLoadArguments
* optional load arguments used for loadURI()
*/
restoreTabContent: function (aTab, aLoadArguments = null) {
let window = aTab.ownerDocument.defaultView;
let browser = aTab.linkedBrowser;
let tabData = browser.__SS_data;
// Make sure that this tab is removed from the priority queue.
TabRestoreQueue.remove(aTab);
@ -2715,17 +2704,6 @@ let SessionStoreInternal = {
browser.removeAttribute("pending");
aTab.removeAttribute("pending");
let activeIndex = tabData.index - 1;
// Attach data that will be restored on "load" event, after tab is restored.
if (tabData.entries.length) {
// 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] || {};
} else {
browser.__SS_restore_data = {};
}
browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
{loadArguments: aLoadArguments});
},

Просмотреть файл

@ -162,7 +162,9 @@ let SnapshotsListView = Heritage.extend(WidgetMethods, {
* The select listener for this container.
*/
_onSelect: function({ detail: snapshotItem }) {
if (!snapshotItem) {
// Check to ensure the attachment has an actor, like
// an in-progress recording.
if (!snapshotItem || !snapshotItem.attachment.actor) {
return;
}
let { calls, thumbnails, screenshot } = snapshotItem.attachment;

Просмотреть файл

@ -4,6 +4,7 @@ support-files =
doc_raf-begin.html
doc_settimeout.html
doc_no-canvas.html
doc_raf-no-canvas.html
doc_simple-canvas.html
doc_simple-canvas-bitmasks.html
doc_simple-canvas-deep-stack.html
@ -44,7 +45,9 @@ skip-if = e10s # bug 1102301 - leaks while running as a standalone directory in
[browser_canvas-frontend-reload-02.js]
[browser_canvas-frontend-slider-01.js]
[browser_canvas-frontend-slider-02.js]
[browser_canvas-frontend-snapshot-select.js]
[browser_canvas-frontend-snapshot-select-01.js]
[browser_canvas-frontend-snapshot-select-02.js]
[browser_canvas-frontend-stepping.js]
[browser_canvas-frontend-stop-01.js]
[browser_canvas-frontend-stop-02.js]
[browser_canvas-frontend-stop-03.js]

Просмотреть файл

@ -0,0 +1,30 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if selecting snapshots in the frontend displays the appropriate data
* respective to their recorded animation frame.
*/
function ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
SnapshotsListView._onRecordButtonClick();
let snapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
ok(true, "clicking in-progress snapshot does not fail");
let finished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield finished;
yield teardown(panel);
finish();
}

Просмотреть файл

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that a recording that has a rAF cycle, but no draw calls, fails
* after timeout.
*/
function ifTestingSupported() {
let { target, panel } = yield initCanvasDebuggerFrontend(RAF_NO_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
SnapshotsListView._onRecordButtonClick();
yield recordingStarted;
is($("#empty-notice").hidden, true, "Empty notice not shown");
is($("#waiting-notice").hidden, false, "Waiting notice shown");
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
yield promise.all([recordingFinished, recordingCancelled]);
ok(true, "Recording stopped and was considered failed.");
is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
is($("#empty-notice").hidden, false, "Empty notice shown");
is($("#waiting-notice").hidden, true, "Waiting notice not shown");
yield teardown(panel);
finish();
}

Просмотреть файл

@ -0,0 +1,18 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Canvas inspector test page</title>
</head>
<body>
<script>
function render () { window.requestAnimationFrame(render); }
window.requestAnimationFrame(render);
</script>
</body>
</html>

Просмотреть файл

@ -30,6 +30,7 @@ const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-u
const EXAMPLE_URL = "http://example.com/browser/browser/devtools/canvasdebugger/test/";
const SET_TIMEOUT_URL = EXAMPLE_URL + "doc_settimeout.html";
const NO_CANVAS_URL = EXAMPLE_URL + "doc_no-canvas.html";
const RAF_NO_CANVAS_URL = EXAMPLE_URL + "doc_raf-no-canvas.html";
const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html";
const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";

Просмотреть файл

@ -14,6 +14,11 @@
min-width: 100%;
}
body.dragging .tag-line {
cursor: grabbing;
-moz-user-select: none;
}
#root-wrapper:after {
content: "";
display: block;
@ -63,6 +68,40 @@
padding-left: 1001em;
}
/* Normally this element takes space in the layout even if it's position: relative
* by adding height: 0 we let surrounding elements to fill the blank space */
.child.dragging {
position: relative;
pointer-events: none;
opacity: 0.7;
height: 0;
}
/* Indicates a tag-line in the markup-view as being an active drop target by
* drawing a horizontal line where the dragged element would be inserted if
* dropped here */
.tag-line.drop-target::before, .tag-line.drag-target::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.tag-line.drag-target::before {
border-top: 2px dashed var(--theme-contrast-background);
}
.tag-line.drop-target::before {
border-top: 2px dashed var(--theme-content-color1);
}
/* In case the indicator is put on the closing .tag-line, the indentation level
* will become misleading, so we push it forward to match the indentation level */
ul.children + .tag-line::before {
margin-left: 14px;
}
.tag-line {
min-height: 1.4em;
line-height: 1.4em;

Просмотреть файл

@ -14,6 +14,10 @@ const COLLAPSE_ATTRIBUTE_LENGTH = 120;
const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
const COLLAPSE_DATA_URL_LENGTH = 60;
const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
const GRAB_DELAY = 400;
const DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE = 50;
const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 5;
const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 15;
const {UndoStack} = require("devtools/shared/undo");
const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
@ -62,6 +66,7 @@ function MarkupView(aInspector, aFrame, aControllerWindow) {
this._inspector = aInspector;
this.walker = this._inspector.walker;
this._frame = aFrame;
this.win = this._frame.contentWindow;
this.doc = this._frame.contentDocument;
this._elt = this.doc.querySelector("#root");
this.htmlEditor = new HTMLEditor(this.doc);
@ -94,6 +99,9 @@ function MarkupView(aInspector, aFrame, aControllerWindow) {
this._onMouseClick = this._onMouseClick.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
this.doc.body.addEventListener("mouseup", this._onMouseUp);
this._boundOnNewSelection = this._onNewSelection.bind(this);
this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
this._onNewSelection();
@ -156,7 +164,49 @@ MarkupView.prototype = {
}
},
isDragging: false,
_onMouseMove: function(event) {
if (this.isDragging) {
event.preventDefault();
this._dragStartEl = event.target;
let docEl = this.doc.documentElement;
if (this._scrollInterval) {
this.win.clearInterval(this._scrollInterval);
}
// Auto-scroll when the mouse approaches top/bottom edge
let distanceFromBottom = docEl.clientHeight - event.pageY + this.win.scrollY,
distanceFromTop = event.pageY - this.win.scrollY;
if (distanceFromBottom <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
// Map our distance from 0-50 to 5-15 range so the speed is kept
// in a range not too fast, not too slow
let speed = map(distanceFromBottom, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
// Here, we use minus because the value of speed - 15 is always negative
// and it makes the speed relative to the distance between mouse and edge
// the closer to the edge, the faster
this._scrollInterval = this.win.setInterval(() => {
docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
}, 0);
}
if (distanceFromTop <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
// refer to bottom edge's comments for more info
let speed = map(distanceFromTop, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
this._scrollInterval = this.win.setInterval(() => {
docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
}, 0);
}
return;
};
let target = event.target;
// Search target for a markupContainer reference, if not found, walk up
@ -198,6 +248,18 @@ MarkupView.prototype = {
}
},
_onMouseUp: function() {
if (this._lastDropTarget) {
this.indicateDropTarget(null);
}
if (this._lastDragTarget) {
this.indicateDragTarget(null);
}
if (this._scrollInterval) {
this.win.clearInterval(this._scrollInterval);
}
},
_hoveredNode: null,
/**
@ -218,6 +280,11 @@ MarkupView.prototype = {
},
_onMouseLeave: function() {
if (this._scrollInterval) {
this.win.clearInterval(this._scrollInterval);
}
if (this.isDragging) return;
this._hideBoxModel(true);
if (this._hoveredNode) {
this.getContainer(this._hoveredNode).hovered = false;
@ -790,6 +857,10 @@ MarkupView.prototype = {
*/
_expandContainer: function(aContainer) {
return this._updateChildren(aContainer, {expand: true}).then(() => {
if (this._destroyer) {
console.warn("Could not expand the node, the markup-view was destroyed");
return;
}
aContainer.expanded = true;
});
},
@ -1361,6 +1432,12 @@ MarkupView.prototype = {
this.tooltip.destroy();
this.tooltip = null;
this.win = null;
this.doc = null;
this._lastDropTarget = null;
this._lastDragTarget = null;
return this._destroyer;
},
@ -1448,6 +1525,80 @@ MarkupView.prototype = {
this._updatePreview();
this._previewBar.classList.remove("hide");
}, 1000);
},
/**
* Takes an element as it's only argument and marks the element
* as the drop target
*/
indicateDropTarget: function(el) {
if (this._lastDropTarget) {
this._lastDropTarget.classList.remove("drop-target");
}
if (!el) return;
let target = el.classList.contains("tag-line") ?
el : el.querySelector(".tag-line") || el.closest(".tag-line");
if (!target) return;
target.classList.add("drop-target");
this._lastDropTarget = target;
},
/**
* Takes an element to mark it as indicator of dragging target's initial place
*/
indicateDragTarget: function(el) {
if (this._lastDragTarget) {
this._lastDragTarget.classList.remove("drag-target");
}
if (!el) return;
let target = el.classList.contains("tag-line") ?
el : el.querySelector(".tag-line") || el.closest(".tag-line");
if (!target) return;
target.classList.add("drag-target");
this._lastDragTarget = target;
},
/**
* Used to get the nodes required to modify the markup after dragging the element (parent/nextSibling)
*/
get dropTargetNodes() {
let target = this._lastDropTarget;
if (!target) {
return null;
}
let parent, nextSibling;
if (this._lastDropTarget.previousElementSibling &&
this._lastDropTarget.previousElementSibling.nodeName.toLowerCase() === "ul") {
parent = target.parentNode.container.node;
nextSibling = null;
} else {
parent = target.parentNode.container.node.parentNode();
nextSibling = target.parentNode.container.node;
}
if (nextSibling && nextSibling.isBeforePseudoElement) {
nextSibling = target.parentNode.parentNode.children[1].container.node;
}
if (nextSibling && nextSibling.isAfterPseudoElement) {
parent = target.parentNode.container.node.parentNode();
nextSibling = null;
}
if (parent.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
return null;
}
return {parent, nextSibling};
}
};
@ -1481,6 +1632,7 @@ MarkupContainer.prototype = {
this.markup = markupView;
this.node = node;
this.undo = this.markup.undo;
this.win = this.markup._frame.contentWindow;
// The template will fill the following properties
this.elt = null;
@ -1491,15 +1643,16 @@ MarkupContainer.prototype = {
this.markup.template(templateID, this);
this.elt.container = this;
// Binding event listeners
this._onMouseDown = this._onMouseDown.bind(this);
this.elt.addEventListener("mousedown", this._onMouseDown, false);
this._onToggle = this._onToggle.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
// Expanding/collapsing the node on dblclick of the whole tag-line element
// Binding event listeners
this.elt.addEventListener("mousedown", this._onMouseDown, false);
this.markup.doc.body.addEventListener("mouseup", this._onMouseUp, true);
this.markup.doc.body.addEventListener("mousemove", this._onMouseMove, true);
this.elt.addEventListener("dblclick", this._onToggle, false);
if (this.expander) {
this.expander.addEventListener("click", this._onToggle, false);
}
@ -1618,24 +1771,109 @@ MarkupContainer.prototype = {
return this.elt.parentNode ? this.elt.parentNode.container : null;
},
_isMouseDown: false,
_isDragging: false,
_dragStartY: 0,
set isDragging(isDragging) {
this._isDragging = isDragging;
this.markup.isDragging = isDragging;
if (isDragging) {
this.elt.classList.add("dragging");
this.markup.doc.body.classList.add("dragging");
} else {
this.elt.classList.remove("dragging");
this.markup.doc.body.classList.remove("dragging");
}
},
get isDragging() {
return this._isDragging;
},
_onMouseDown: function(event) {
let target = event.target;
// Target may be a resource link (generated by the output-parser)
// The "show more nodes" button (already has its onclick).
if (target.nodeName === "button") {
return;
}
// output-parser generated links handling.
if (target.nodeName === "a") {
event.stopPropagation();
event.preventDefault();
let browserWin = this.markup._inspector.target
.tab.ownerDocument.defaultView;
browserWin.openUILinkIn(target.href, "tab");
return;
}
// Or it may be the "show more nodes" button (which already has its onclick)
// Else, it's the container itself
else if (target.nodeName !== "button") {
this.hovered = false;
this.markup.navigate(this);
event.stopPropagation();
// target is the MarkupContainer itself.
this._isMouseDown = true;
this.hovered = false;
this.markup.navigate(this);
event.stopPropagation();
// Start dragging the container after a delay.
this.markup._dragStartEl = target;
this.win.setTimeout(() => {
// Make sure the mouse is still down and on target.
if (!this._isMouseDown || this.markup._dragStartEl !== target ||
this.node.isPseudoElement || this.node.isAnonymous ||
!this.win.getSelection().isCollapsed) {
return;
}
this.isDragging = true;
this._dragStartY = event.pageY;
this.markup.indicateDropTarget(this.elt);
// If this is the last child, use the closing <div.tag-line> of parent as indicator
this.markup.indicateDragTarget(this.elt.nextElementSibling ||
this.markup.getContainer(this.node.parentNode()).closeTagLine);
}, GRAB_DELAY);
},
/**
* On mouse up, stop dragging.
*/
_onMouseUp: function(event) {
this._isMouseDown = false;
if (!this.isDragging) {
return;
}
this.isDragging = false;
this.elt.style.removeProperty("top");
let dropTargetNodes = this.markup.dropTargetNodes;
if(!dropTargetNodes) {
return;
}
this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
dropTargetNodes.nextSibling);
},
/**
* On mouse move, move the dragged element if any and indicate the drop target.
*/
_onMouseMove: function(event) {
if (!this.isDragging) {
return;
}
let diff = event.pageY - this._dragStartY;
this.elt.style.top = diff + "px";
let el = this.markup.doc.elementFromPoint(event.pageX - this.win.scrollX,
event.pageY - this.win.scrollY);
this.markup.indicateDropTarget(el);
},
/**
@ -1644,7 +1882,7 @@ MarkupContainer.prototype = {
*/
flashMutation: function() {
if (!this.selected) {
let contentWin = this.markup._frame.contentWindow;
let contentWin = this.win;
this.flashed = true;
if (this._flashMutationTimer) {
contentWin.clearTimeout(this._flashMutationTimer);
@ -1777,6 +2015,10 @@ MarkupContainer.prototype = {
// Remove event listeners
this.elt.removeEventListener("mousedown", this._onMouseDown, false);
this.elt.removeEventListener("dblclick", this._onToggle, false);
this.markup.doc.body.removeEventListener("mouseup", this._onMouseUp, true);
this.markup.doc.body.removeEventListener("mousemove", this._onMouseMove, true);
this.win = null;
if (this.expander) {
this.expander.removeEventListener("click", this._onToggle, false);
@ -2515,6 +2757,17 @@ function parseAttributeValues(attr, doc) {
return attributes.reverse();
}
/**
* Map a number from one range to another.
*/
function map(value, oldMin, oldMax, newMin, newMax) {
let ratio = oldMax - oldMin;
if (ratio == 0) {
return value;
}
return newMin + (newMax - newMin) * ((value - oldMin) / ratio);
}
loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle(
"chrome://browser/locale/devtools/inspector.properties"
));

Просмотреть файл

@ -2,6 +2,8 @@
subsuite = devtools
support-files =
doc_markup_anonymous.html
doc_markup_dragdrop.html
doc_markup_dragdrop_autoscroll.html
doc_markup_edit.html
doc_markup_events.html
doc_markup_events_jquery.html
@ -37,6 +39,11 @@ skip-if = e10s # scratchpad.xul is not loading in e10s window
[browser_markupview_anonymous_04.js]
[browser_markupview_copy_image_data.js]
[browser_markupview_css_completion_style_attribute.js]
[browser_markupview_dragdrop_autoscroll.js]
[browser_markupview_dragdrop_invalidNodes.js]
[browser_markupview_dragdrop_isDragging.js]
[browser_markupview_dragdrop_reorder.js]
[browser_markupview_dragdrop_textSelection.js]
[browser_markupview_events.js]
skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
[browser_markupview_events-overflow.js]

Просмотреть файл

@ -0,0 +1,61 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test: Dragging nodes near top/bottom edges of inspector
// should auto-scroll
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop_autoscroll.html";
const GRAB_DELAY = 400;
add_task(function*() {
let {inspector} = yield addTab(TEST_URL).then(openInspector);
let markup = inspector.markup;
let container = yield getContainerForSelector("#first", inspector);
let rect = container.elt.getBoundingClientRect();
info("Simulating mouseDown on #first");
container._onMouseDown({
target: container.tagLine,
pageX: 10,
pageY: rect.top,
stopPropagation: function() {},
preventDefault: function() {}
});
yield wait(GRAB_DELAY + 1);
let clientHeight = markup.doc.documentElement.clientHeight;
info("Simulating mouseMove on #first with pageY: " + clientHeight);
let ev = {
target: container.tagLine,
pageX: 10,
pageY: clientHeight,
preventDefault: function() {}
};
info("Listening on scroll event");
let scroll = onScroll(markup.win);
markup._onMouseMove(ev);
yield scroll;
container._onMouseUp(ev);
markup._onMouseUp(ev);
ok("Scroll event fired");
});
function onScroll(win) {
return new Promise((resolve, reject) => {
win.onscroll = function(e) {
resolve(e);
}
});
};

Просмотреть файл

@ -0,0 +1,58 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test: pseudo-elements and anonymous nodes should not be draggable
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
const GRAB_DELAY = 400;
add_task(function*() {
Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
let {inspector} = yield addTab(TEST_URL).then(openInspector);
info("Expanding #test");
let parentFront = yield getNodeFront("#test", inspector);
yield inspector.markup.expandNode(parentFront);
yield waitForMultipleChildrenUpdates(inspector);
let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
let beforePseudo = parentContainer.elt.children[1].firstChild.container;
parentContainer.elt.scrollIntoView(true);
info("Simulating mouseDown on #test::before");
beforePseudo._onMouseDown({
target: beforePseudo.tagLine,
stopPropagation: function() {},
preventDefault: function() {}
});
info("Waiting " + (GRAB_DELAY + 1) + "ms")
yield wait(GRAB_DELAY + 1);
is(beforePseudo.isDragging, false, "[pseudo-element] isDragging is false after GRAB_DELAY has passed");
let inputFront = yield getNodeFront("#anonymousParent", inspector);
yield inspector.markup.expandNode(inputFront);
yield waitForMultipleChildrenUpdates(inspector);
let inputContainer = yield getContainerForNodeFront(inputFront, inspector);
let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
inputContainer.elt.scrollIntoView(true);
info("Simulating mouseDown on input#anonymousParent div");
anonymousDiv._onMouseDown({
target: anonymousDiv.tagLine,
stopPropagation: function() {},
preventDefault: function() {}
});
info("Waiting " + (GRAB_DELAY + 1) + "ms")
yield wait(GRAB_DELAY + 1);
is(anonymousDiv.isDragging, false, "[anonymous element] isDragging is false after GRAB_DELAY has passed");
});

Просмотреть файл

@ -0,0 +1,41 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test drag mode's delay, it shouldn't enable dragging before
// GRAB_DELAY = 400 has passed
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
const GRAB_DELAY = 400;
add_task(function*() {
let {inspector} = yield addTab(TEST_URL).then(openInspector);
let el = yield getContainerForSelector("#test", inspector);
let rect = el.tagLine.getBoundingClientRect();
info("Simulating mouseDown on #test");
el._onMouseDown({
target: el.tagLine,
pageX: rect.x,
pageY: rect.y,
stopPropagation: function() {}
});
is(el.isDragging, false, "isDragging should not be set to true immedietly");
info("Waiting " + (GRAB_DELAY + 1) + "ms");
yield wait(GRAB_DELAY + 1);
ok(el.isDragging, "isDragging true after GRAB_DELAY has passed");
info("Simulating mouseUp on #test");
el._onMouseUp({
target: el.tagLine,
pageX: rect.x,
pageY: rect.y
});
is(el.isDragging, false, "isDragging false after mouseUp");
});

Просмотреть файл

@ -0,0 +1,109 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test different kinds of drag and drop node re-ordering
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
const GRAB_DELAY = 400;
add_task(function*() {
let {inspector} = yield addTab(TEST_URL).then(openInspector);
info("Expanding #test");
let parentFront = yield getNodeFront("#test", inspector);
let parent = yield getNode("#test");
let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
yield inspector.markup.expandNode(parentFront);
yield waitForMultipleChildrenUpdates(inspector);
parentContainer.elt.scrollIntoView(true);
info("Testing switching elements inside their parent");
yield moveElementDown("#firstChild", "#middleChild", inspector);
is(parent.children[0].id, "middleChild", "#firstChild is now the second child of #test");
is(parent.children[1].id, "firstChild", "#middleChild is now the first child of #test");
info("Testing switching elements with a last child");
yield moveElementDown("#firstChild", "#lastChild", inspector);
is(parent.children[1].id, "lastChild", "#lastChild is now the second child of #test");
is(parent.children[2].id, "firstChild", "#firstChild is now the last child of #test");
info("Testing appending element to a parent");
yield moveElementDown("#before", "#test", inspector);
is(parent.children.length, 4, "New element appended to #test");
is(parent.children[0].id, "before", "New element is appended at the right place (currently first child)");
info("Testing moving element to after it's parent");
yield moveElementDown("#firstChild", "#test", inspector);
is(parent.children.length, 3, "#firstChild is no longer #test's child");
is(parent.nextElementSibling.id, "firstChild", "#firstChild is now #test's nextElementSibling");
});
function* dragContainer(selector, targetOffset, inspector) {
info("Dragging the markup-container for node " + selector);
let container = yield getContainerForSelector(selector, inspector);
let updated = inspector.once("inspector-updated");
let rect = {
x: container.tagLine.offsetLeft,
y: container.tagLine.offsetTop
};
info("Simulating mouseDown on " + selector);
container._onMouseDown({
target: container.tagLine,
pageX: rect.x,
pageY: rect.y,
stopPropagation: function() {}
});
let targetX = rect.x + targetOffset.x,
targetY = rect.y + targetOffset.y;
setTimeout(() => {
info("Simulating mouseMove on " + selector +
" with pageX: " + targetX + " pageY: " + targetY);
container._onMouseMove({
target: container.tagLine,
pageX: targetX,
pageY: targetY
});
info("Simulating mouseUp on " + selector +
" with pageX: " + targetX + " pageY: " + targetY);
container._onMouseUp({
target: container.tagLine,
pageX: targetX,
pageY: targetY
});
container.markup._onMouseUp();
}, GRAB_DELAY+1);
return updated;
};
function* moveElementDown(selector, next, inspector) {
let onMutated = inspector.once("markupmutation");
let uiUpdate = inspector.once("inspector-updated");
let el = yield getContainerForSelector(next, inspector);
let height = el.tagLine.getBoundingClientRect().height;
info("Switching " + selector + ' with ' + next);
yield dragContainer(selector, {x: 0, y: Math.round(height) + 2}, inspector);
yield onMutated;
yield uiUpdate;
};

Просмотреть файл

@ -0,0 +1,48 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test: Nodes should not be draggable if there is a text selected
// (trying to move selected text around shouldn't trigger node drag and drop)
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
const GRAB_DELAY = 400;
add_task(function*() {
let {inspector} = yield addTab(TEST_URL).then(openInspector);
let markup = inspector.markup;
info("Expanding span#before");
let spanFront = yield getNodeFront("#before", inspector);
let spanContainer = yield getContainerForNodeFront(spanFront, inspector);
let span = yield getNode("#before");
yield inspector.markup.expandNode(spanFront);
yield waitForMultipleChildrenUpdates(inspector);
spanContainer.elt.scrollIntoView(true);
info("Selecting #before's text content");
let textContent = spanContainer.elt.children[1].firstChild.container;
let selectRange = markup.doc.createRange();
selectRange.selectNode(textContent.editor.elt.querySelector('[tabindex]'));
markup.doc.getSelection().addRange(selectRange);
info("Simulating mouseDown on #before");
spanContainer._onMouseDown({
pageX: 0,
pageY: 0,
target: spanContainer.tagLine,
stopPropagation: function() {},
preventDefault: function() {}
});
yield wait(GRAB_DELAY + 1);
is(spanContainer.isDragging, false, "isDragging should be false if there is a text selected");
});

Просмотреть файл

@ -31,15 +31,3 @@ add_task(function*() {
yield inspector.once("inspector-updated");
});
// The expand all operation of the markup-view calls itself recursively and
// there's not one event we can wait for to know when it's done
function* waitForMultipleChildrenUpdates(inspector) {
// As long as child updates are queued up while we wait for an update already
// wait again
if (inspector.markup._queuedChildUpdates &&
inspector.markup._queuedChildUpdates.size) {
yield waitForChildrenUpdated(inspector);
return yield waitForMultipleChildrenUpdates(inspector);
}
}

Просмотреть файл

@ -33,15 +33,3 @@ add_task(function*() {
"Container for node " + nodeFront.tagName + " is expanded");
}
});
// The expand all operation of the markup-view calls itself recursively and
// there's not one event we can wait for to know when it's done
function* waitForMultipleChildrenUpdates(inspector) {
// As long as child updates are queued up while we wait for an update already
// wait again
if (inspector.markup._queuedChildUpdates &&
inspector.markup._queuedChildUpdates.size) {
yield waitForChildrenUpdated(inspector);
return yield waitForMultipleChildrenUpdates(inspector);
}
}

Просмотреть файл

@ -0,0 +1,31 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=858038
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 858038</title>
<style>
#test::before {
content: 'This should not be draggable';
}
</style>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<input id="anonymousParent" />
<span id="before">Before</span>
<pre id="test">
<span id="firstChild">First</span>
<span id="middleChild">Middle</span>
<span id="lastChild">Last</span>
</pre>
<span id="after">After</span>
</body>
</html>

Просмотреть файл

@ -0,0 +1,51 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=858038
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 858038 - Autoscroll</title>
</head>
<body>
<div id="first"></div>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
<p id="display">Test</p>
<div id="content" style="display: none">
</div>
<!-- Make sure the markup-view has enough nodes shown by default that it has a scrollbar -->
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</body>
</html>

Просмотреть файл

@ -561,3 +561,16 @@ function checkFocusedAttribute(attrName, editMode) {
editMode ? "input": "span",
editMode ? attrName + " is in edit mode" : attrName + " is not in edit mode");
}
// The expand all operation of the markup-view calls itself recursively and
// there's not one event we can wait for to know when it's done
// so use this helper function to wait until all recursive children updates are done.
function* waitForMultipleChildrenUpdates(inspector) {
// As long as child updates are queued up while we wait for an update already
// wait again
if (inspector.markup._queuedChildUpdates &&
inspector.markup._queuedChildUpdates.size) {
yield waitForChildrenUpdated(inspector);
return yield waitForMultipleChildrenUpdates(inspector);
}
}

Просмотреть файл

@ -35,9 +35,6 @@ function test() {
function testPrefs() {
let { Prefs } = aMonitor.panelWin;
is(Prefs._root, "devtools.netmonitor",
"The preferences object should have a correct root path.");
is(Prefs.networkDetailsWidth,
Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
"Getting a pref should work correctly.");

Просмотреть файл

@ -3,6 +3,8 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cu, Cr } = require("chrome");
/**
* Utility functions for managing recording models and their internal data,
* such as filtering profile samples or offsetting timestamps.
@ -130,3 +132,50 @@ exports.RecordingUtils.getSamplesFromAllocations = function(allocations) {
gSamplesFromAllocationCache.set(allocations, samples);
return samples;
}
/**
* Gets the current timeline blueprint without the hidden markers.
*
* @param blueprint
* The default timeline blueprint.
* @param array hiddenMarkers
* A list of hidden markers' names.
* @return object
* The filtered timeline blueprint.
*/
exports.RecordingUtils.getFilteredBlueprint = function({ blueprint, hiddenMarkers }) {
let filteredBlueprint = Cu.cloneInto(blueprint, {});
let maybeRemovedGroups = new Set();
let removedGroups = new Set();
// 1. Remove hidden markers from the blueprint.
for (let hiddenMarkerName of hiddenMarkers) {
maybeRemovedGroups.add(filteredBlueprint[hiddenMarkerName].group);
delete filteredBlueprint[hiddenMarkerName];
}
// 2. Get a list of all the groups that will be removed.
for (let maybeRemovedGroup of maybeRemovedGroups) {
let markerNames = Object.keys(filteredBlueprint);
let isGroupRemoved = markerNames.every(e => filteredBlueprint[e].group != maybeRemovedGroup);
if (isGroupRemoved) {
removedGroups.add(maybeRemovedGroup);
}
}
// 3. Offset groups so that their indices are consecutive.
for (let removedGroup of removedGroups) {
let markerNames = Object.keys(filteredBlueprint);
for (let markerName of markerNames) {
let markerDetails = filteredBlueprint[markerName];
if (markerDetails.group > removedGroup) {
markerDetails.group--;
}
}
}
return filteredBlueprint;
};

Просмотреть файл

@ -57,10 +57,12 @@ const BRANCH_NAME = "devtools.performance.ui.";
// Events emitted by various objects in the panel.
const EVENTS = {
// Fired by the OptionsView when a preference changes.
// Fired by the PerformanceController and OptionsView when a pref changes.
PREF_CHANGED: "Performance:PrefChanged",
// Emitted by the PerformanceView when the state (display mode) changes.
// Emitted by the PerformanceView when the state (display mode) changes,
// for example when switching between "empty", "recording" or "recorded".
// This causes certain panels to be hidden or visible.
UI_STATE_CHANGED: "Performance:UI:StateChanged",
// Emitted by the PerformanceView on clear button click
@ -176,6 +178,16 @@ let PerformanceController = {
this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
this._onPrefChanged = this._onPrefChanged.bind(this);
// All boolean prefs should be handled via the OptionsView in the
// ToolbarView, so that they may be accessible via the "gear" menu.
// Every other pref should be registered here.
this._nonBooleanPrefs = new ViewHelpers.Prefs("devtools.performance", {
"hidden-markers": ["Json", "timeline.hidden-markers"]
});
this._nonBooleanPrefs.registerObserver();
this._nonBooleanPrefs.on("pref-changed", this._onPrefChanged);
ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
@ -195,6 +207,9 @@ let PerformanceController = {
* Remove events handled by the PerformanceController
*/
destroy: function() {
this._nonBooleanPrefs.unregisterObserver();
this._nonBooleanPrefs.off("pref-changed", this._onPrefChanged);
ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
@ -211,11 +226,32 @@ let PerformanceController = {
},
/**
* Get a preference setting from `prefName` via the underlying
* Get a boolean preference setting from `prefName` via the underlying
* OptionsView in the ToolbarView.
*
* @param string prefName
* @return boolean
*/
getOption: function (prefName) {
return ToolbarView.optionsView.getPref(prefName);
},
/**
* Get a preference setting from `prefName`.
* @param string prefName
* @return object
*/
getPref: function (prefName) {
return ToolbarView.optionsView.getPref(prefName);
return this._nonBooleanPrefs[prefName];
},
/**
* Set a preference setting from `prefName`.
* @param string prefName
* @param object prefValue
*/
setPref: function (prefName, prefValue) {
this._nonBooleanPrefs[prefName] = prefValue;
},
/**
@ -223,9 +259,9 @@ let PerformanceController = {
* when the front has started to record.
*/
startRecording: Task.async(function *() {
let withMemory = this.getPref("enable-memory");
let withTicks = this.getPref("enable-framerate");
let withAllocations = this.getPref("enable-memory");
let withMemory = this.getOption("enable-memory");
let withTicks = this.getOption("enable-framerate");
let withAllocations = this.getOption("enable-memory");
let recording = this._createRecording({ withMemory, withTicks, withAllocations });
@ -241,7 +277,7 @@ let PerformanceController = {
* when the front has stopped recording.
*/
stopRecording: Task.async(function *() {
let recording = this._getLatestRecording();
let recording = this.getLatestRecording();
this.emit(EVENTS.RECORDING_WILL_STOP, recording);
yield recording.stopRecording();
@ -267,7 +303,7 @@ let PerformanceController = {
* Emits `EVENTS.RECORDINGS_CLEARED` when complete so other components can clean up.
*/
clearRecordings: Task.async(function* () {
let latest = this._getLatestRecording();
let latest = this.getLatestRecording();
if (latest && latest.isRecording()) {
yield this.stopRecording();
@ -335,13 +371,23 @@ let PerformanceController = {
* Get most recently added recording that was triggered manually (via UI).
* @return RecordingModel
*/
_getLatestRecording: function () {
getLatestRecording: function () {
for (let i = this._recordings.length - 1; i >= 0; i--) {
return this._recordings[i];
}
return null;
},
/**
* Gets the current timeline blueprint without the hidden markers.
* @return object
*/
getTimelineBlueprint: function() {
let blueprint = TIMELINE_BLUEPRINT;
let hiddenMarkers = this.getPref("hidden-markers");
return RecordingUtils.getFilteredBlueprint({ blueprint, hiddenMarkers });
},
/**
* Fired whenever the PerformanceFront emits markers, memory or ticks.
*/
@ -362,8 +408,8 @@ let PerformanceController = {
* Fired when the ToolbarView fires a PREF_CHANGED event.
* with the value.
*/
_onPrefChanged: function (_, prefName, value) {
this.emit(EVENTS.PREF_CHANGED, prefName, value);
_onPrefChanged: function (_, prefName, prefValue) {
this.emit(EVENTS.PREF_CHANGED, prefName, prefValue);
},
toString: () => "[object PerformanceController]"

Просмотреть файл

@ -29,6 +29,7 @@
<script type="application/javascript" src="performance/views/recordings.js"/>
<popupset id="performance-options-popupset">
<menupopup id="performance-filter-menupopup"/>
<menupopup id="performance-options-menupopup">
<menuitem id="option-show-platform-data"
type="checkbox"
@ -85,6 +86,12 @@
<vbox id="performance-pane" flex="1">
<toolbar id="performance-toolbar" class="devtools-toolbar">
<hbox id="performance-toolbar-control-other" class="devtools-toolbarbutton-group">
<toolbarbutton id="filter-button"
popup="performance-filter-menupopup"
class="devtools-toolbarbutton"
tooltiptext="&profilerUI.options.filter.tooltiptext;"/>
</hbox>
<hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
<toolbarbutton id="select-waterfall-view"
class="devtools-toolbarbutton devtools-button"
@ -112,7 +119,7 @@
<toolbarbutton id="performance-options-button"
class="devtools-toolbarbutton devtools-option-toolbarbutton"
popup="performance-options-menupopup"
tooltiptext="&profilerUI.options.tooltiptext;"/>
tooltiptext="&profilerUI.options.gear.tooltiptext;"/>
</hbox>
</toolbar>

Просмотреть файл

@ -97,3 +97,4 @@ support-files =
[browser_profiler_tree-view-06.js]
[browser_profiler_tree-view-07.js]
[browser_timeline_blueprint.js]
[browser_timeline_filters.js]

Просмотреть файл

@ -5,28 +5,29 @@
* Tests markers filtering mechanism.
*/
add_task(function*() {
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
let { $, $$, TimelineController, TimelineView } = panel.panelWin;
function spawnTest () {
let { panel } = yield initPerformance(SIMPLE_URL);
let { $, $$, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
let { MARKERS_GRAPH_ROW_HEIGHT } = panel.panelWin;
yield TimelineController.toggleRecording();
yield startRecording(panel);
ok(true, "Recording has started.");
yield waitUntil(() => {
// Wait until we get 3 different markers.
let markers = TimelineController.getMarkers();
let markers = PerformanceController.getCurrentRecording().getMarkers();
return markers.some(m => m.name == "Styles") &&
markers.some(m => m.name == "Reflow") &&
markers.some(m => m.name == "Paint");
});
yield TimelineController.toggleRecording();
yield stopRecording(panel);
let overview = TimelineView.markersOverview;
let waterfall = TimelineView.waterfall;
let overview = OverviewView.markersOverview;
let waterfall = WaterfallView.waterfall;
// Select everything
overview.setSelection({ start: 0, end: overview.width })
OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE })
$("#filter-button").click();
@ -70,8 +71,7 @@ add_task(function*() {
yield waitUntil(() => !waterfall._outstandingMarkers.length);
// A row is 11px. See markers-overview.js
is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
is(overview.fixedHeight, heightBefore - MARKERS_GRAPH_ROW_HEIGHT, "Overview is smaller");
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)");
ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)");
@ -89,5 +89,6 @@ add_task(function*() {
is(overview.fixedHeight, originalHeight, "Overview restored");
$(".waterfall-marker-bar[type=Styles]");
});
yield teardown(panel);
finish();
}

Просмотреть файл

@ -5,13 +5,16 @@
<html>
<head>
<meta charset="utf-8"/>
<title>Profiler test page</title>
<title>Performance test page</title>
</head>
<body>
<script type="text/javascript">
var x = 1;
function test() {
var a = "Hello world!";
document.body.style.borderTop = x + "px solid red";
x = 1^x;
document.body.innerHeight; // flush pending reflows
}
// Prevent this script from being garbage collected.

Просмотреть файл

@ -59,10 +59,16 @@ let DetailsSubview = {
/**
* An array of preferences under `devtools.performance.ui.` that the view should
* rerender upon change.
* rerender and callback `this._onRerenderPrefChanged` upon change.
*/
rerenderPrefs: [],
/**
* An array of preferences under `devtools.performance.` that the view should
* observe and callback `this._onObservedPrefChange` upon change.
*/
observedPrefs: [],
/**
* Called when recording stops or is selected.
*/
@ -103,6 +109,10 @@ let DetailsSubview = {
* Fired when a preference in `devtools.performance.ui.` is changed.
*/
_onPrefChanged: function (_, prefName) {
if (~this.observedPrefs.indexOf(prefName) && this._onObservedPrefChange) {
this._onObservedPrefChange(_, prefName);
}
// All detail views require a recording to be complete, so do not
// attempt to render if recording is in progress or does not exist.
let recording = PerformanceController.getCurrentRecording();
@ -115,7 +125,7 @@ let DetailsSubview = {
}
if (this._onRerenderPrefChanged) {
this._onRerenderPrefChanged();
this._onRerenderPrefChanged(_, prefName);
}
if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {

Просмотреть файл

@ -64,8 +64,8 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
*/
_prepareCallTree: function (profile, { startTime, endTime }, options) {
let threadSamples = profile.threads[0].samples;
let contentOnly = !PerformanceController.getPref("show-platform-data");
let invertTree = PerformanceController.getPref("invert-call-tree");
let contentOnly = !PerformanceController.getOption("show-platform-data");
let invertTree = PerformanceController.getOption("invert-call-tree");
let threadNode = new ThreadNode(threadSamples,
{ startTime, endTime, contentOnly, invertTree });
@ -108,7 +108,7 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
// When platform data isn't shown, hide the cateogry labels, since they're
// only available for C++ frames.
let contentOnly = !PerformanceController.getPref("show-platform-data");
let contentOnly = !PerformanceController.getOption("show-platform-data");
root.toggleCategories(!contentOnly);
},

Просмотреть файл

@ -53,10 +53,10 @@ let JsFlameGraphView = Heritage.extend(DetailsSubview, {
let samples = profile.threads[0].samples;
let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
invertStack: PerformanceController.getPref("invert-flame-graph"),
flattenRecursion: PerformanceController.getPref("flatten-tree-recursion"),
filterFrames: !PerformanceController.getPref("show-platform-data") && FrameNode.isContent,
showIdleBlocks: PerformanceController.getPref("show-idle-blocks") && L10N.getStr("table.idle")
invertStack: PerformanceController.getOption("invert-flame-graph"),
flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
filterFrames: !PerformanceController.getOption("show-platform-data") && FrameNode.isContent,
showIdleBlocks: PerformanceController.getOption("show-idle-blocks") && L10N.getStr("table.idle")
});
this.graph.setData({ data,

Просмотреть файл

@ -62,7 +62,7 @@ let MemoryCallTreeView = Heritage.extend(DetailsSubview, {
*/
_prepareCallTree: function (allocations, { startTime, endTime }, options) {
let samples = RecordingUtils.getSamplesFromAllocations(allocations);
let invertTree = PerformanceController.getPref("invert-call-tree");
let invertTree = PerformanceController.getOption("invert-call-tree");
let threadNode = new ThreadNode(samples,
{ startTime, endTime, invertTree });

Просмотреть файл

@ -52,9 +52,9 @@ let MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
let samples = RecordingUtils.getSamplesFromAllocations(allocations);
let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
invertStack: PerformanceController.getPref("invert-flame-graph"),
flattenRecursion: PerformanceController.getPref("flatten-tree-recursion"),
showIdleBlocks: PerformanceController.getPref("show-idle-blocks") && L10N.getStr("table.idle")
invertStack: PerformanceController.getOption("invert-flame-graph"),
flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
showIdleBlocks: PerformanceController.getOption("show-idle-blocks") && L10N.getStr("table.idle")
});
this.graph.setData({ data,

Просмотреть файл

@ -8,6 +8,14 @@
*/
let WaterfallView = Heritage.extend(DetailsSubview, {
observedPrefs: [
"hidden-markers"
],
rerenderPrefs: [
"hidden-markers"
],
rangeChangeDebounceTime: 10, // ms
/**
@ -16,7 +24,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
initialize: function () {
DetailsSubview.initialize.call(this);
this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"), TIMELINE_BLUEPRINT);
this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"));
this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
this._onMarkerSelected = this._onMarkerSelected.bind(this);
@ -26,6 +34,8 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
this.waterfall.on("unselected", this._onMarkerSelected);
this.details.on("resize", this._onResize);
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
this.waterfall.recalculateBounds();
},
@ -79,5 +89,13 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
this.render();
},
/**
* Called whenever an observed pref is changed.
*/
_onObservedPrefChange: function(_, prefName) {
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
},
toString: () => "[object WaterfallView]"
});

Просмотреть файл

@ -4,19 +4,41 @@
"use strict";
/**
* Details view containing profiler call tree and markers waterfall. Manages
* subviews and toggles visibility between them.
* Details view containing call trees, flamegraphs and markers waterfall.
* Manages subviews and toggles visibility between them.
*/
let DetailsView = {
/**
* Name to node+object mapping of subviews.
* Name to (node id, view object, actor requirements, pref killswitch)
* mapping of subviews.
*/
components: {
"waterfall": { id: "waterfall-view", view: WaterfallView, requires: ["timeline"] },
"js-calltree": { id: "js-calltree-view", view: JsCallTreeView },
"js-flamegraph": { id: "js-flamegraph-view", view: JsFlameGraphView, requires: ["timeline"] },
"memory-calltree": { id: "memory-calltree-view", view: MemoryCallTreeView, requires: ["memory"], pref: "enable-memory" },
"memory-flamegraph": { id: "memory-flamegraph-view", view: MemoryFlameGraphView, requires: ["memory", "timeline"], pref: "enable-memory" }
"waterfall": {
id: "waterfall-view",
view: WaterfallView,
requires: ["timeline"]
},
"js-calltree": {
id: "js-calltree-view",
view: JsCallTreeView
},
"js-flamegraph": {
id: "js-flamegraph-view",
view: JsFlameGraphView,
requires: ["timeline"]
},
"memory-calltree": {
id: "memory-calltree-view",
view: MemoryCallTreeView,
requires: ["memory"],
pref: "enable-memory"
},
"memory-flamegraph": {
id: "memory-flamegraph-view",
view: MemoryFlameGraphView,
requires: ["memory", "timeline"],
pref: "enable-memory"
}
},
/**
@ -66,12 +88,13 @@ let DetailsView = {
*/
setAvailableViews: Task.async(function* () {
let mocks = gFront.getMocksInUse();
for (let [name, { view, pref, requires }] of Iterator(this.components)) {
let recording = PerformanceController.getCurrentRecording();
let isRecorded = recording && !recording.isRecording();
// View is enabled view prefs
let isEnabled = !pref || PerformanceController.getPref(pref);
// View is enabled by its corresponding pref
let isEnabled = !pref || PerformanceController.getOption(pref);
// View is supported by the server actor, and the requried actor is not being mocked
let isSupported = !requires || requires.every(r => !mocks[r]);

Просмотреть файл

@ -40,8 +40,8 @@ let OverviewView = {
// Toggle the initial visibility of memory and framerate graph containers
// based off of prefs.
$("#memory-overview").hidden = !PerformanceController.getPref("enable-memory");
$("#time-framerate").hidden = !PerformanceController.getPref("enable-framerate");
$("#memory-overview").hidden = !PerformanceController.getOption("enable-memory");
$("#time-framerate").hidden = !PerformanceController.getOption("enable-framerate");
PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceController.on(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
@ -55,6 +55,16 @@ let OverviewView = {
* Unbinds events.
*/
destroy: function () {
if (this.markersOverview) {
this.markersOverview.destroy();
}
if (this.memoryOverview) {
this.memoryOverview.destroy();
}
if (this.framerateGraph) {
this.framerateGraph.destroy();
}
PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
PerformanceController.off(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
@ -86,7 +96,7 @@ let OverviewView = {
* Sets the time interval selection for all graphs in this overview.
*
* @param object interval
* The { starTime, endTime }, in milliseconds.
* The { startTime, endTime }, in milliseconds.
*/
setTimeInterval: function(interval, options = {}) {
if (this.isDisabled()) {
@ -138,7 +148,8 @@ let OverviewView = {
yield this.markersOverview.ready();
return true;
}
this.markersOverview = new MarkersOverview($("#markers-overview"), TIMELINE_BLUEPRINT);
let blueprint = PerformanceController.getTimelineBlueprint();
this.markersOverview = new MarkersOverview($("#markers-overview"), blueprint);
this.markersOverview.headerHeight = MARKERS_GRAPH_HEADER_HEIGHT;
this.markersOverview.rowHeight = MARKERS_GRAPH_ROW_HEIGHT;
this.markersOverview.groupPadding = MARKERS_GROUP_VERTICAL_PADDING;
@ -155,7 +166,7 @@ let OverviewView = {
* ready to use, `false` if the graph is disabled.
*/
_memoryGraphAvailable: Task.async(function *() {
if (!PerformanceController.getPref("enable-memory")) {
if (!PerformanceController.getOption("enable-memory")) {
return false;
}
if (this.memoryOverview) {
@ -179,7 +190,7 @@ let OverviewView = {
* ready to use, `false` if the graph is disabled.
*/
_framerateGraphAvailable: Task.async(function *() {
if (!PerformanceController.getPref("enable-framerate")) {
if (!PerformanceController.getOption("enable-framerate")) {
return false;
}
if (this.framerateGraph) {
@ -340,14 +351,26 @@ let OverviewView = {
* Called whenever a preference in `devtools.performance.ui.` changes. Used
* to toggle the visibility of memory and framerate graphs.
*/
_onPrefChanged: function (_, prefName) {
if (prefName === "enable-memory") {
$("#memory-overview").hidden = !PerformanceController.getPref("enable-memory");
_onPrefChanged: Task.async(function* (_, prefName, prefValue) {
switch (prefName) {
case "enable-memory": {
$("#memory-overview").hidden = !prefValue;
break;
}
case "enable-framerate": {
$("#time-framerate").hidden = !prefValue;
break;
}
case "hidden-markers": {
if (yield this._markersGraphAvailable()) {
let blueprint = PerformanceController.getTimelineBlueprint();
this.markersOverview.setBlueprint(blueprint);
this.markersOverview.refresh({ force: true });
}
break;
}
}
if (prefName === "enable-framerate") {
$("#time-framerate").hidden = !PerformanceController.getPref("enable-framerate");
}
},
}),
toString: () => "[object OverviewView]"
};

Просмотреть файл

@ -11,6 +11,9 @@ let ToolbarView = {
* Sets up the view with event binding.
*/
initialize: Task.async(function *() {
this._onFilterPopupShowing = this._onFilterPopupShowing.bind(this);
this._onFilterPopupHiding = this._onFilterPopupHiding.bind(this);
this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this);
this._onPrefChanged = this._onPrefChanged.bind(this);
this.optionsView = new OptionsView({
@ -20,16 +23,87 @@ let ToolbarView = {
yield this.optionsView.initialize();
this.optionsView.on("pref-changed", this._onPrefChanged);
this._buildMarkersFilterPopup();
this._updateHiddenMarkersPopup();
$("#performance-filter-menupopup").addEventListener("popupshowing", this._onFilterPopupShowing);
$("#performance-filter-menupopup").addEventListener("popuphiding", this._onFilterPopupHiding);
}),
/**
* Unbinds events and cleans up view.
*/
destroy: function () {
$("#performance-filter-menupopup").removeEventListener("popupshowing", this._onFilterPopupShowing);
$("#performance-filter-menupopup").removeEventListener("popuphiding", this._onFilterPopupHiding);
this.optionsView.off("pref-changed", this._onPrefChanged);
this.optionsView.destroy();
},
/**
* Creates the timeline markers filter popup.
*/
_buildMarkersFilterPopup: function() {
for (let [markerName, markerDetails] of Iterator(TIMELINE_BLUEPRINT)) {
let menuitem = document.createElement("menuitem");
menuitem.setAttribute("closemenu", "none");
menuitem.setAttribute("type", "checkbox");
menuitem.setAttribute("align", "center");
menuitem.setAttribute("flex", "1");
menuitem.setAttribute("label", markerDetails.label);
menuitem.setAttribute("marker-type", markerName);
menuitem.addEventListener("command", this._onHiddenMarkersChanged);
// Style used by pseudo element ::before in performance.inc.css
let bulletStyle = `--bullet-bg: ${markerDetails.fill};`
bulletStyle += `--bullet-border: ${markerDetails.stroke}`;
menuitem.setAttribute("style", bulletStyle);
$("#performance-filter-menupopup").appendChild(menuitem);
}
},
/**
* Updates the menu items checked state in the timeline markers filter popup.
*/
_updateHiddenMarkersPopup: function() {
let menuItems = $$("#performance-filter-menupopup menuitem[marker-type]");
let hiddenMarkers = PerformanceController.getPref("hidden-markers");
for (let menuitem of menuItems) {
if (~hiddenMarkers.indexOf(menuitem.getAttribute("marker-type"))) {
menuitem.removeAttribute("checked");
} else {
menuitem.setAttribute("checked", "true");
}
}
},
/**
* Fired when the markers filter popup starts to show.
*/
_onFilterPopupShowing: function() {
$("#filter-button").setAttribute("open", "true");
},
/**
* Fired when the markers filter popup starts to hide.
*/
_onFilterPopupHiding: function() {
$("#filter-button").removeAttribute("open");
},
/**
* Fired when a menu item in the markers filter popup is checked or unchecked.
*/
_onHiddenMarkersChanged: function() {
let checkedMenuItems = $$("#performance-filter-menupopup menuitem[marker-type]:not([checked])");
let hiddenMarkers = Array.map(checkedMenuItems, e => e.getAttribute("marker-type"));
PerformanceController.setPref("hidden-markers", hiddenMarkers);
},
/**
* Fired when a preference changes in the underlying OptionsView.
* Propogated by the PerformanceController.

Просмотреть файл

@ -148,8 +148,6 @@ let PrefObserver = {
this.branch.removeObserver("", this);
},
observe: function(subject, topic, pref) {
Prefs.refresh();
if (pref == "ui.show-platform-data") {
RecordingsListView.forceSelect(RecordingsListView.selectedItem);
}

Просмотреть файл

@ -20,14 +20,23 @@ function test() {
"The pref was was correctly changed (2).");
Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
info("The pref value was reset.");
info("The pref value was reset (1).");
is(Prefs.foo, !originalPrefValue,
"The cached pref value hasn't changed yet.");
"The cached pref value hasn't changed yet (1).");
Prefs.refresh();
Services.prefs.setBoolPref("devtools.debugger.enabled", !originalPrefValue);
info("The pref value was reset (2).");
is(Prefs.foo, !originalPrefValue,
"The cached pref value hasn't changed yet (2).");
Prefs.registerObserver();
Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
info("The pref value was reset (3).");
is(Prefs.foo, originalPrefValue,
"The cached pref value has changed now.");
Prefs.unregisterObserver();
finish();
}

Просмотреть файл

@ -17,6 +17,7 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
this.EXPORTED_SYMBOLS = [
"Heritage", "ViewHelpers", "WidgetMethods",
@ -392,25 +393,57 @@ ViewHelpers.L10N.prototype = {
* let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
* myIntPref: ["Int", "leaf.path.to.my-int-pref"],
* myCharPref: ["Char", "leaf.path.to.my-char-pref"],
* myJsonPref: ["Json", "leaf.path.to.my-json-pref"]
* ...
* });
*
* Get/set:
* prefs.myCharPref = "foo";
* let aux = prefs.myCharPref;
*
* Observe:
* prefs.registerObserver();
* prefs.on("pref-changed", (prefName, prefValue) => {
* ...
* });
*
* @param string aPrefsRoot
* The root path to the required preferences branch.
* @param object aPrefsObject
* @param object aPrefsBlueprint
* An object containing { accessorName: [prefType, prefName] } keys.
*/
ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsObject = {}) {
this._root = aPrefsRoot;
this._cache = new Map();
ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsBlueprint = {}) {
EventEmitter.decorate(this);
for (let accessorName in aPrefsObject) {
let [prefType, prefName] = aPrefsObject[accessorName];
this.map(accessorName, prefType, prefName);
this._cache = new Map();
let self = this;
for (let [accessorName, [prefType, prefName]] of Iterator(aPrefsBlueprint)) {
this._map(accessorName, prefType, aPrefsRoot, prefName);
}
let observer = {
register: function() {
this.branch = Services.prefs.getBranch(aPrefsRoot + ".");
this.branch.addObserver("", this, false);
},
unregister: function() {
this.branch.removeObserver("", this);
},
observe: function(_, __, aPrefName) {
// If this particular pref isn't handled by the blueprint object,
// even though it's in the specified branch, ignore it.
let accessor = self._accessor(aPrefsBlueprint, aPrefName);
if (!(accessor in self)) {
return;
}
self._cache.delete(aPrefName);
self.emit("pref-changed", accessor, self[accessor]);
}
};
this.registerObserver = () => observer.register();
this.unregisterObserver = () => observer.unregister();
};
ViewHelpers.Prefs.prototype = {
@ -418,15 +451,16 @@ ViewHelpers.Prefs.prototype = {
* Helper method for getting a pref value.
*
* @param string aType
* @param string aPrefsRoot
* @param string aPrefName
* @return any
*/
_get: function(aType, aPrefName) {
_get: function(aType, aPrefsRoot, aPrefName) {
let cachedPref = this._cache.get(aPrefName);
if (cachedPref !== undefined) {
return cachedPref;
}
let value = Services.prefs["get" + aType + "Pref"](aPrefName);
let value = Services.prefs["get" + aType + "Pref"]([aPrefsRoot, aPrefName].join("."));
this._cache.set(aPrefName, value);
return value;
},
@ -435,11 +469,12 @@ ViewHelpers.Prefs.prototype = {
* Helper method for setting a pref value.
*
* @param string aType
* @param string aPrefsRoot
* @param string aPrefName
* @param any aValue
*/
_set: function(aType, aPrefName, aValue) {
Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
_set: function(aType, aPrefsRoot, aPrefName, aValue) {
Services.prefs["set" + aType + "Pref"]([aPrefsRoot, aPrefName].join("."), aValue);
this._cache.set(aPrefName, aValue);
},
@ -450,26 +485,36 @@ ViewHelpers.Prefs.prototype = {
*
* @param string aAccessorName
* @param string aType
* @param string aPrefsRoot
* @param string aPrefName
* @param array aSerializer
*/
map: function(aAccessorName, aType, aPrefName, aSerializer = { in: e => e, out: e => e }) {
_map: function(aAccessorName, aType, aPrefsRoot, aPrefName, aSerializer = { in: e => e, out: e => e }) {
if (aPrefName in this) {
throw new Error(`Can't use ${aPrefName} because it's already a property.`);
}
if (aType == "Json") {
this.map(aAccessorName, "Char", aPrefName, { in: JSON.parse, out: JSON.stringify });
this._map(aAccessorName, "Char", aPrefsRoot, aPrefName, { in: JSON.parse, out: JSON.stringify });
return;
}
Object.defineProperty(this, aAccessorName, {
get: () => aSerializer.in(this._get(aType, [this._root, aPrefName].join("."))),
set: (e) => this._set(aType, [this._root, aPrefName].join("."), aSerializer.out(e))
get: () => aSerializer.in(this._get(aType, aPrefsRoot, aPrefName)),
set: (e) => this._set(aType, aPrefsRoot, aPrefName, aSerializer.out(e))
});
},
/**
* Clears all the cached preferences' values.
* Finds the accessor in this object for the provided property name,
* based on the blueprint object used in the constructor.
*/
refresh: function() {
this._cache.clear();
_accessor: function(aPrefsBlueprint, aPrefName) {
for (let [accessorName, [, prefName]] of Iterator(aPrefsBlueprint)) {
if (prefName == aPrefName) {
return accessorName;
}
}
return null;
}
};

Просмотреть файл

@ -138,9 +138,9 @@ function finishUp() {
inspector.destroy();
inspector = null;
completer = null;
gBrowser.removeCurrentTab();
finish();
});
progress = null;
progressDiv = null;
gBrowser.removeCurrentTab();
finish();
}

Просмотреть файл

@ -5,7 +5,6 @@ support-files =
head.js
[browser_timeline_aaa_run_first_leaktest.js]
[browser_timeline_filters.js]
[browser_timeline_overview-initial-selection-01.js]
[browser_timeline_overview-initial-selection-02.js]
[browser_timeline_overview-update.js]

Просмотреть файл

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

Просмотреть файл

@ -1,63 +1,56 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that the WebAudioInspector's Width is saved as
* a preference
*/
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
let gVars = InspectorView._propsView;
let started = once(gFront, "start-context");
reload(target);
let [actors] = yield Promise.all([
get3(gFront, "create-node"),
waitForGraphRendered(panelWin, 3, 2)
]);
let nodeIds = actors.map(actor => actor.actorID);
ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
// Open inspector pane
$("#inspector-pane-toggle").click();
yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
let newInspectorWidth = 500;
// Setting width to new_inspector_width
$("#web-audio-inspector").setAttribute("width", newInspectorWidth);
reload(target);
//Width should be 500 after reloading
[actors] = yield Promise.all([
get3(gFront, "create-node"),
waitForGraphRendered(panelWin, 3, 2)
]);
nodeIds = actors.map(actor => actor.actorID);
// Open inspector pane
$("#inspector-pane-toggle").click();
yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
let nodeSet = Promise.all([
once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
once(panelWin, EVENTS.UI_PROPERTIES_TAB_RENDERED),
once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED)
]);
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield nodeSet;
yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]));
// Getting the width of the audio inspector
let width = $("#web-audio-inspector").getAttribute("width");
is(width, newInspectorWidth, "WebAudioEditor's Inspector width should be saved as a preference");
yield teardown(target);
});
});

Просмотреть файл

@ -29,12 +29,7 @@ add_task(function*() {
"InspectorView tabs view should be hidden when no node's selected.");
// Wait for the node to be set as well as the inspector to come fully into the view
let nodeSet = Promise.all([
once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
]);
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield nodeSet;
yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]), true);
ok(InspectorView.isVisible(), "InspectorView shown once node selected.");
ok(!isVisible($("#web-audio-editor-details-pane-empty")),
@ -45,9 +40,7 @@ add_task(function*() {
is($("#web-audio-editor-tabs").selectedIndex, 0,
"default tab selected should be the parameters tab.");
nodeSet = once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
click(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield nodeSet;
yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[2]));
yield teardown(target);
});

Просмотреть файл

@ -110,6 +110,7 @@ function once(aTarget, aEventName, aUseCapture = false) {
if ((add in aTarget) && (remove in aTarget)) {
aTarget[add](aEventName, function onEvent(...aArgs) {
aTarget[remove](aEventName, onEvent, aUseCapture);
info("Got event: '" + aEventName + "' on " + aTarget + ".");
deferred.resolve(...aArgs);
}, aUseCapture);
break;
@ -352,13 +353,15 @@ function wait (n) {
/**
* Clicks a graph node based on actorID or passing in an element.
* Returns a promise that resolves once
* UI_INSPECTOR_NODE_SET is fired.
* Returns a promise that resolves once UI_INSPECTOR_NODE_SET is fired and
* the tabs have rendered, completing all RDP requests for the node.
*/
function clickGraphNode (panelWin, el, waitForToggle = false) {
let { promise, resolve } = Promise.defer();
let promises = [
once(panelWin, panelWin.EVENTS.UI_INSPECTOR_NODE_SET)
once(panelWin, panelWin.EVENTS.UI_INSPECTOR_NODE_SET),
once(panelWin, panelWin.EVENTS.UI_PROPERTIES_TAB_RENDERED),
once(panelWin, panelWin.EVENTS.UI_AUTOMATION_TAB_RENDERED)
];
if (waitForToggle) {

Просмотреть файл

@ -39,7 +39,7 @@ let test = asyncTest(function* () {
],
});
testClickOpenNewTab(hud, results);
yield testClickOpenNewTab(hud, results);
});
function pushPrefEnv()
@ -57,18 +57,5 @@ function testClickOpenNewTab(hud, results) {
let warningNode = results[0].clickableElements[0];
ok(warningNode, "link element");
ok(warningNode.classList.contains("learn-more-link"), "link class name");
// Invoke the click event and check if a new tab would open to the correct page.
let linkOpened = false;
let oldOpenUILinkIn = window.openUILinkIn;
window.openUILinkIn = function(aLink) {
if (aLink == LEARN_MORE_URI) {
linkOpened = true;
}
}
EventUtils.synthesizeMouse(warningNode, 2, 2, {},
warningNode.ownerDocument.defaultView);
ok(linkOpened, "Clicking the Learn More Warning node opens the desired page");
window.openUILinkIn = oldOpenUILinkIn;
return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
}

Просмотреть файл

@ -43,11 +43,11 @@ let test = asyncTest(function* () {
],
});
testClickOpenNewTab(hud, results[0]);
yield testClickOpenNewTab(hud, results[0]);
let results2 = yield mixedContentOverrideTest2(hud, browser);
testClickOpenNewTab(hud, results2[0]);
yield testClickOpenNewTab(hud, results2[0]);
});
function pushPrefEnv()
@ -118,19 +118,5 @@ function testClickOpenNewTab(hud, match) {
let warningNode = match.clickableElements[0];
ok(warningNode, "link element");
ok(warningNode.classList.contains("learn-more-link"), "link class name");
// Invoke the click event and check if a new tab would
// open to the correct page.
let linkOpened = false;
let oldOpenUILinkIn = window.openUILinkIn;
window.openUILinkIn = function(aLink) {
if (aLink == LEARN_MORE_URI) {
linkOpened = true;
}
}
EventUtils.synthesizeMouse(warningNode, 2, 2, {},
warningNode.ownerDocument.defaultView);
ok(linkOpened, "Clicking the Learn More Warning node opens the desired page");
window.openUILinkIn = oldOpenUILinkIn;
return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
}

Просмотреть файл

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632275-getters.html";
let getterValue = null;

Просмотреть файл

@ -12,6 +12,7 @@
const TEST_URI = "data:text/html;charset=utf8,Web Console mixed content test";
const TEST_HTTPS_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html";
const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/MixedContent";
let test = asyncTest(function* () {
Services.prefs.setBoolPref("security.mixed_content.block_display_content", false);
@ -27,45 +28,33 @@ let test = asyncTest(function* () {
Services.prefs.clearUserPref("security.mixed_content.block_active_content");
});
function testMixedContent(hud) {
let testMixedContent = Task.async(function*(hud) {
content.location = TEST_HTTPS_URI;
return waitForMessages({
let results = yield waitForMessages({
webconsole: hud,
messages: [{
text: "example.com",
category: CATEGORY_NETWORK,
severity: SEVERITY_WARNING,
}],
}).then((results) => {
let msg = [...results[0].matched][0];
ok(msg, "page load logged");
ok(msg.classList.contains("mixed-content"), ".mixed-content element");
let link = msg.querySelector(".learn-more-link");
ok(link, "mixed content link element");
is(link.textContent, "[Mixed Content]", "link text is accurate");
let oldOpenLink = hud.openLink;
let linkOpened = false;
hud.openLink = (url) => {
is(url, "https://developer.mozilla.org/docs/Security/MixedContent",
"url opened");
linkOpened = true;
};
EventUtils.synthesizeMouse(link, 2, 2, {}, link.ownerDocument.defaultView);
ok(linkOpened, "clicking the Mixed Content link opened a page");
hud.openLink = oldOpenLink;
ok(!msg.classList.contains("filtered-by-type"), "message is not filtered");
hud.setFilterState("netwarn", false);
ok(msg.classList.contains("filtered-by-type"), "message is filtered");
hud.setFilterState("netwarn", true);
});
}
let msg = [...results[0].matched][0];
ok(msg, "page load logged");
ok(msg.classList.contains("mixed-content"), ".mixed-content element");
let link = msg.querySelector(".learn-more-link");
ok(link, "mixed content link element");
is(link.textContent, "[Mixed Content]", "link text is accurate");
yield simulateMessageLinkClick(link, LEARN_MORE_URI);
ok(!msg.classList.contains("filtered-by-type"), "message is not filtered");
hud.setFilterState("netwarn", false);
ok(msg.classList.contains("filtered-by-type"), "message is filtered");
hud.setFilterState("netwarn", true);
});

Просмотреть файл

@ -40,26 +40,12 @@ let test = asyncTest(function* () {
],
});
testClickOpenNewTab(hud, result);
yield testClickOpenNewTab(hud, result);
});
function testClickOpenNewTab(hud, [result]) {
let msg = [...result.matched][0];
let warningNode = msg.querySelector(".learn-more-link");
ok(warningNode, "learn more link");
// Invoke the click event and check if a new tab would open to the correct
// page
let linkOpened = false;
let oldOpenUILinkIn = window.openUILinkIn;
window.openUILinkIn = function(aLink) {
if (aLink == INSECURE_PASSWORDS_URI) {
linkOpened = true;
}
}
EventUtils.synthesizeMouse(warningNode, 2, 2, {},
warningNode.ownerDocument.defaultView);
ok(linkOpened, "Clicking the Insecure Passwords Warning node opens the desired page");
window.openUILinkIn = oldOpenUILinkIn;
return simulateMessageLinkClick(warningNode, INSECURE_PASSWORDS_URI);
}

Просмотреть файл

@ -6,45 +6,30 @@ const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/t
const HSTS_INVALID_HEADER_MSG = "The site specified an invalid Strict-Transport-Security header.";
const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
function test()
{
loadTab(TEST_URI).then(() => {
openConsole().then((hud) => {
waitForMessages({
webconsole: hud,
messages: [
{
name: "Invalid HSTS header error displayed successfully",
text: HSTS_INVALID_HEADER_MSG,
category: CATEGORY_SECURITY,
severity: SEVERITY_WARNING,
objects: true,
},
],
}).then((results) => testClickOpenNewTab(hud, results));
})
let test = asyncTest(function* () {
yield loadTab(TEST_URI);
let hud = yield openConsole();
let results = yield waitForMessages({
webconsole: hud,
messages: [
{
name: "Invalid HSTS header error displayed successfully",
text: HSTS_INVALID_HEADER_MSG,
category: CATEGORY_SECURITY,
severity: SEVERITY_WARNING,
objects: true,
},
],
});
}
yield testClickOpenNewTab(hud, results);
});
function testClickOpenNewTab(hud, results) {
let warningNode = results[0].clickableElements[0];
ok(warningNode, "link element");
ok(warningNode.classList.contains("learn-more-link"), "link class name");
// Invoke the click event and check if a new tab would
// open to the correct page.
let linkOpened = false;
let oldOpenUILinkIn = window.openUILinkIn;
window.openUILinkIn = function(aLink) {
if (aLink == LEARN_MORE_URI) {
linkOpened = true;
}
}
EventUtils.synthesizeMouse(warningNode, 2, 2, {},
warningNode.ownerDocument.defaultView);
ok(linkOpened, "Clicking the Learn More Warning node opens the desired page");
window.openUILinkIn = oldOpenUILinkIn;
finishTest();
return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
}

Просмотреть файл

@ -1646,3 +1646,32 @@ function getSourceActor(aSources, aURL) {
let item = aSources.getItemForAttachment(a => a.source.url === aURL);
return item && item.value;
}
/**
* Verify that clicking on a link from a popup notification message tries to
* open the expected URL.
*/
function simulateMessageLinkClick(element, expectedLink) {
let deferred = promise.defer();
// Invoke the click event and check if a new tab would
// open to the correct page.
let oldOpenUILinkIn = window.openUILinkIn;
window.openUILinkIn = function(link) {
if (link == expectedLink) {
ok(true, "Clicking the message link opens the desired page");
window.openUILinkIn = oldOpenUILinkIn;
deferred.resolve();
}
};
let event = new MouseEvent("click", {
detail: 1,
button: 0,
bubbles: true,
cancelable: true
});
element.dispatchEvent(event);
return deferred.promise;
}

Просмотреть файл

@ -153,6 +153,15 @@ function loadAddonManager() {
startupManager();
}
// Starts the addon manager without creating app info. We can't directly use
// |loadAddonManager| defined above in test_conditions.js as it would make the test fail.
function startAddonManagerOnly() {
let addonManager = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.nsIObserver)
.QueryInterface(Ci.nsITimerCallback);
addonManager.observe(null, "addons-startup", null);
}
function getExperimentAddons(previous=false) {
let deferred = Promise.defer();

Просмотреть файл

@ -5,8 +5,14 @@
Cu.import("resource:///modules/experiments/Experiments.jsm");
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
() => Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject);
const FILE_MANIFEST = "experiments.manifest";
const SEC_IN_ONE_DAY = 24 * 60 * 60;
const MS_IN_ONE_DAY = SEC_IN_ONE_DAY * 1000;
@ -45,6 +51,17 @@ function applicableFromManifestData(data, policy) {
return entry.isApplicable();
}
function initialiseTelemetry() {
// Send the needed startup notifications to the datareporting service
// to ensure that it has been initialized.
if ("@mozilla.org/datareporting/service;1" in Cc) {
gDatareportingService.observe(null, "app-startup", null);
gDatareportingService.observe(null, "profile-after-change", null);
}
return TelemetryPing.setup().then(TelemetrySession.setup);
}
function run_test() {
run_next_test();
}
@ -52,7 +69,8 @@ function run_test() {
add_task(function* test_setup() {
createAppInfo();
gProfileDir = do_get_profile();
yield TelemetrySession.setup();
startAddonManagerOnly();
yield initialiseTelemetry();
gPolicy = new Experiments.Policy();
gReporter = yield getReporter("json_payload_simple");
@ -312,5 +330,5 @@ add_task(function* test_times() {
});
add_task(function* test_shutdown() {
yield TelemetrySession.shutdown();
yield TelemetrySession.shutdown(false);
});

Просмотреть файл

@ -64,9 +64,13 @@
- on the "+" (new tab) button for a profile when a selection is available. -->
<!ENTITY profilerUI.newtab.tooltiptext "Add new tab from selection">
<!-- LOCALIZATION NOTE (profilerUI.toolbar.filter.tooltiptext): This string
- is displayed next to the filter button-->
<!ENTITY profilerUI.options.filter.tooltiptext "Select what data to display in the timeline">
<!-- LOCALIZATION NOTE (profilerUI.options.tooltiptext): This is the tooltip
- for the options button. -->
<!ENTITY profilerUI.options.tooltiptext "Configure performance preferences.">
<!ENTITY profilerUI.options.gear.tooltiptext "Configure performance preferences.">
<!-- LOCALIZATION NOTE (profilerUI.invertTree): This is the label shown next to
- a checkbox that inverts and un-inverts the profiler's call tree. -->

Просмотреть файл

@ -25,6 +25,10 @@
-moz-border-end-color: var(--theme-splitter-color);
}
#performance-toolbar-control-other {
-moz-padding-end: 5px;
}
#performance-toolbar-controls-detail-views > toolbarbutton {
min-width: 0;
}
@ -34,6 +38,31 @@
-moz-padding-end: 8px;
}
#filter-button {
list-style-image: url(timeline-filter.svg#filter);
min-width: 24px;
}
#filter-button[disabled] {
list-style-image: url(timeline-filter.svg#filter-disabled);
}
#filter-button[open] {
list-style-image: url(timeline-filter.svg#filter-open);
}
#performance-filter-menupopup > menuitem:before {
content: "";
display: block;
width: 8px;
height: 8px;
margin: 0 8px;
border: 1px solid;
border-radius: 1px;
background-color: var(--bullet-bg);
border-color: var(--bullet-border);
}
/* Recording Notice */
#performance-view .notice-container {

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 14 KiB

После

Ширина:  |  Высота:  |  Размер: 14 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 7.6 KiB

После

Ширина:  |  Высота:  |  Размер: 7.4 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 21 KiB

После

Ширина:  |  Высота:  |  Размер: 20 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 31 KiB

После

Ширина:  |  Высота:  |  Размер: 36 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 18 KiB

После

Ширина:  |  Высота:  |  Размер: 17 KiB

Просмотреть файл

@ -33,12 +33,48 @@ body {
font-family: serif;
}
.font-size1 {
font-size: 10px;
}
.font-size2 {
font-size: 12px;
}
.font-size3 {
font-size: 14px;
}
.font-size4 {
font-size: 16px;
}
.font-size5 {
font-size: 18px;
}
.font-size6 {
font-size: 20px;
}
.font-size7 {
font-size: 22px;
}
.font-size8 {
font-size: 24px;
}
.font-size9 {
font-size: 26px;
}
.message {
margin-top: 40px;
display: none;
text-align: center;
width: 100%;
font-size: 16px;
font-size: 0.9rem;
}
.header {
@ -65,7 +101,7 @@ body {
}
.header > h1 {
font-size: 2.625em;
font-size: 1.33rem;
font-weight: 700;
line-height: 1.1em;
width: 100%;
@ -107,66 +143,19 @@ body {
color: #aaaaaa;
}
.font-size1 > body > .header > h1 {
font-size: 27px;
}
.font-size2 > body > .header > h1 {
font-size: 29px;
}
.font-size3 > body > .header > h1 {
font-size: 31px;
}
.font-size4 > body > .header > h1 {
font-size: 33px;
}
.font-size5 > body > .header > h1 {
font-size: 35px;
}
/* This covers caption, domain, and credits
texts in the reader UI */
.font-size1 > body > .content .wp-caption-text,
.font-size1 > body > .content figcaption,
.font-size1 > body > .header > .domain,
.font-size1 > body > .header > .credits {
font-size: 10px;
}
.font-size2 > body > .content .wp-caption-text,
.font-size2 > body > .content figcaption,
.font-size2 > body > .header > .domain,
.font-size2 > body > .header > .credits {
font-size: 13px;
}
.font-size3 > body > .content .wp-caption-text,
.font-size3 > body > .content figcaption,
.font-size3 > body > .header > .domain,
.font-size3 > body > .header > .credits {
font-size: 15px;
}
.font-size4 > body > .content .wp-caption-text,
.font-size4 > body > .content figcaption,
.font-size4 > body > .header > .domain,
.font-size4 > body > .header > .credits {
font-size: 17px;
}
.font-size5 > body > .content .wp-caption-text,
.font-size5 > body > .content figcaption,
.font-size5 > body > .header > .domain,
.font-size5 > body > .header > .credits {
font-size: 19px;
.content .wp-caption-text,
.content figcaption,
.header > .domain,
.header > .credits {
font-size: 0.9rem;
}
.content {
display: none;
font-size: 1rem;
}
.content a {
@ -295,31 +284,6 @@ body {
list-style: decimal !important;
}
.font-size1-sample,
.font-size1 > body > .content {
font-size: 14px !important;
}
.font-size2-sample,
.font-size2 > body > .content {
font-size: 16px !important;
}
.font-size3-sample,
.font-size3 > body > .content {
font-size: 18px !important;
}
.font-size4-sample,
.font-size4 > body > .content {
font-size: 20px !important;
}
.font-size5-sample,
.font-size5 > body > .content {
font-size: 22px !important;
}
.toolbar {
font-family: "Clear Sans",sans-serif;
transition-property: visibility, opacity;
@ -361,7 +325,8 @@ body {
}
/* Remove dotted border when button is focused */
.button::-moz-focus-inner {
.button::-moz-focus-inner,
#font-size-buttons > button::-moz-focus-inner {
border: 0;
}
@ -427,6 +392,7 @@ body {
}
#font-type-buttons,
#font-size-buttons,
.segmented-button {
display: flex;
flex-direction: row;
@ -436,13 +402,22 @@ body {
}
#font-type-buttons > li,
#font-size-buttons > button,
.segmented-button > li {
width: 50px; /* combined with flex, this acts as a minimum width */
flex: 1 0 auto;
text-align: center;
line-height: 20px;
}
#font-type-buttons > li,
#font-size-buttons > button {
width: 100px; /* combined with flex, this acts as a minimum width */
}
.segmented-button > li {
width: 50px; /* combined with flex, this acts as a minimum width */
}
#font-type-buttons > li {
padding: 10px 0;
}
@ -451,6 +426,7 @@ body {
border-left: 1px solid #B5B5B5;
}
#font-size-buttons > button:first-child,
.segmented-button > li:first-child {
border-left: 0px;
}
@ -496,6 +472,24 @@ body {
font-size: 12px;
}
.minus-button,
.plus-button {
background-color: transparent;
border: 0;
height: 30px;
background-size: 18px 18px;
background-repeat: no-repeat;
background-position: center;
}
.minus-button {
background-size: 24px 6px;
}
.plus-button {
background-size: 24px 24px;
}
/* desktop-only controls */
.close-button,
.list-button {
@ -518,6 +512,14 @@ body {
background-image: url('chrome://browser/skin/images/reader-style-icon-mdpi.png');
}
.minus-button {
background-image: url('chrome://browser/skin/images/reader-minus-mdpi.png');
}
.plus-button {
background-image: url('chrome://browser/skin/images/reader-plus-mdpi.png');
}
@media screen and (min-resolution: 1.25dppx) {
.dropdown-arrow {
background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-hdpi.png');
@ -538,6 +540,14 @@ body {
.style-button {
background-image: url('chrome://browser/skin/images/reader-style-icon-hdpi.png');
}
.minus-button {
background-image: url('chrome://browser/skin/images/reader-minus-hdpi.png');
}
.plus-button {
background-image: url('chrome://browser/skin/images/reader-plus-hdpi.png');
}
}
@media screen and (min-resolution: 2dppx) {
@ -560,6 +570,14 @@ body {
.style-button {
background-image: url('chrome://browser/skin/images/reader-style-icon-xhdpi.png');
}
.minus-button {
background-image: url('chrome://browser/skin/images/reader-minus-xhdpi.png');
}
.plus-button {
background-image: url('chrome://browser/skin/images/reader-plus-xhdpi.png');
}
}
@media screen and (orientation: portrait) {

Двоичные данные
mobile/android/themes/core/images/reader-minus-hdpi.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 195 B

Двоичные данные
mobile/android/themes/core/images/reader-minus-mdpi.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 155 B

Двоичные данные
mobile/android/themes/core/images/reader-minus-xhdpi.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 223 B

Двоичные данные
mobile/android/themes/core/images/reader-plus-hdpi.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 506 B

Двоичные данные
mobile/android/themes/core/images/reader-plus-mdpi.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 394 B

Двоичные данные
mobile/android/themes/core/images/reader-plus-xhdpi.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 609 B

Просмотреть файл

@ -89,6 +89,12 @@ chrome.jar:
skin/images/reader-dropdown-arrow-mdpi.png (images/reader-dropdown-arrow-mdpi.png)
skin/images/reader-dropdown-arrow-hdpi.png (images/reader-dropdown-arrow-hdpi.png)
skin/images/reader-dropdown-arrow-xhdpi.png (images/reader-dropdown-arrow-xhdpi.png)
skin/images/reader-minus-mdpi.png (images/reader-minus-mdpi.png)
skin/images/reader-minus-hdpi.png (images/reader-minus-hdpi.png)
skin/images/reader-minus-xhdpi.png (images/reader-minus-xhdpi.png)
skin/images/reader-plus-mdpi.png (images/reader-plus-mdpi.png)
skin/images/reader-plus-hdpi.png (images/reader-plus-hdpi.png)
skin/images/reader-plus-xhdpi.png (images/reader-plus-xhdpi.png)
skin/images/reader-toggle-on-icon-mdpi.png (images/reader-toggle-on-icon-mdpi.png)
skin/images/reader-toggle-on-icon-hdpi.png (images/reader-toggle-on-icon-hdpi.png)
skin/images/reader-toggle-on-icon-xhdpi.png (images/reader-toggle-on-icon-xhdpi.png)

Просмотреть файл

@ -4560,8 +4560,8 @@ pref("reader.parse-on-load.enabled", true);
// is disabled by default.
pref("reader.parse-on-load.force-enabled", false);
// The default relative font size in reader mode (1-5)
pref("reader.font_size", 3);
// The default relative font size in reader mode (1-9)
pref("reader.font_size", 5);
// The default color scheme in reader mode (light, dark, sepia, auto)
// auto = color automatically adjusts according to ambient light level

Просмотреть файл

@ -230,6 +230,11 @@ user_pref('browser.tiles.reportURL', 'http://%(server)s/tests/robocop/robocop_ti
// We want to collect telemetry, but we don't want to send in the results.
user_pref('toolkit.telemetry.server', 'https://%(server)s/telemetry-dummy/');
// A couple of preferences with default values to test that telemetry preference
// watching is working.
user_pref('toolkit.telemetry.test.pref1', true);
user_pref('toolkit.telemetry.test.pref2', false);
// We don't want to hit the real Firefox Accounts server for tests. We don't
// actually need a functioning FxA server, so just set it to something that
// resolves and accepts requests, even if they all fail.

Просмотреть файл

@ -169,6 +169,29 @@ var PrintUtils = {
let printSettings = this.getPrintSettings();
let mm = aBrowser.messageManager;
mm.addMessageListener("Printing:Print:Done", function onPrintingDone(msg) {
mm.removeMessageListener("Printing:Print:Done", onPrintingDone);
let rv = msg.data.rv;
let printSettings = msg.objects.printSettings;
if (rv != Components.results.NS_OK &&
rv != Components.results.NS_ERROR_ABORT) {
Cu.reportError(`In Printing:Print:Done handler, got unexpected rv
${rv}.`);
}
if (gPrintSettingsAreGlobal && gSavePrintSettings) {
let PSSVC =
Components.classes["@mozilla.org/gfx/printsettings-service;1"]
.getService(Components.interfaces.nsIPrintSettingsService);
PSSVC.savePrintSettingsToPrefs(printSettings, true,
printSettings.kInitSaveAll);
PSSVC.savePrintSettingsToPrefs(printSettings, false,
printSettings.kInitSavePrinterName);
}
});
mm.sendAsyncMessage("Printing:Print", null, {
printSettings: printSettings,
contentWindow: aWindow,

Просмотреть файл

@ -74,43 +74,22 @@ let AboutReader = function(mm, win) {
let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample");
let fontTypeOptions = [
{ name: fontTypeSample,
description: gStrings.GetStringFromName("aboutReader.fontType.serif"),
value: "serif",
linkClass: "serif" },
{ name: fontTypeSample,
description: gStrings.GetStringFromName("aboutReader.fontType.sans-serif"),
value: "sans-serif",
linkClass: "sans-serif"
},
{ name: fontTypeSample,
description: gStrings.GetStringFromName("aboutReader.fontType.serif"),
value: "serif",
linkClass: "serif" },
];
let fontType = Services.prefs.getCharPref("reader.font_type");
this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this));
this._setFontType(fontType);
let fontSizeSample = gStrings.GetStringFromName("aboutReader.fontSizeSample");
let fontSizeOptions = [
{ name: fontSizeSample,
value: 1,
linkClass: "font-size1-sample" },
{ name: fontSizeSample,
value: 2,
linkClass: "font-size2-sample" },
{ name: fontSizeSample,
value: 3,
linkClass: "font-size3-sample" },
{ name: fontSizeSample,
value: 4,
linkClass: "font-size4-sample" },
{ name: fontSizeSample,
value: 5,
linkClass: "font-size5-sample" }
];
let fontSize = Services.prefs.getIntPref("reader.font_size");
this._setupSegmentedButton("font-size-buttons", fontSizeOptions, fontSize, this._setFontSize.bind(this));
this._setFontSize(fontSize);
this._setupFontSizeButtons();
// Track status of reader toolbar add/remove toggle button
this._isReadingListItem = -1;
@ -307,6 +286,63 @@ AboutReader.prototype = {
});
},
_setupFontSizeButtons: function() {
const FONT_SIZE_MIN = 1;
const FONT_SIZE_MAX = 9;
let currentSize = Services.prefs.getIntPref("reader.font_size");
currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
let plusButton = this._doc.getElementById("font-size-plus");
let minusButton = this._doc.getElementById("font-size-minus");
function updateControls() {
if (currentSize === FONT_SIZE_MIN) {
minusButton.setAttribute("disabled", true);
} else {
minusButton.removeAttribute("disabled");
}
if (currentSize === FONT_SIZE_MAX) {
plusButton.setAttribute("disabled", true);
} else {
plusButton.removeAttribute("disabled");
}
}
updateControls();
this._setFontSize(currentSize);
plusButton.addEventListener("click", (event) => {
if (!event.isTrusted) {
return;
}
event.stopPropagation();
if (currentSize >= FONT_SIZE_MAX) {
return;
}
currentSize++;
updateControls();
this._setFontSize(currentSize);
}, true);
minusButton.addEventListener("click", (event) => {
if (!event.isTrusted) {
return;
}
event.stopPropagation();
if (currentSize <= FONT_SIZE_MIN) {
return;
}
currentSize--;
updateControls();
this._setFontSize(currentSize);
}, true);
},
_handleDeviceLight: function Reader_handleDeviceLight(newLux) {
// Desired size of the this._luxValues array.
let luxValuesSize = 10;

Просмотреть файл

@ -32,7 +32,10 @@
<li class="dropdown-popup">
<ul id="font-type-buttons"></ul>
<hr></hr>
<ul id="font-size-buttons" class="segmented-button"></ul>
<div id="font-size-buttons">
<button id="font-size-minus" class="minus-button"/>
<button id="font-size-plus" class="plus-button"/>
</div>
<hr></hr>
<ul id="color-scheme-buttons" class="segmented-button"></ul>
<div class="dropdown-arrow"/>

Просмотреть файл

@ -42,3 +42,14 @@ $(histogram_data_file): $(histograms_file) $(data_python_deps)
$(PYTHON) $(srcdir)/gen-histogram-data.py $< > $@
GARBAGE += $(histogram_enum_file)
# This is so hacky. Waiting on bug 988938.
addondir = $(srcdir)/tests/addons
testdir = $(abspath $(DEPTH)/_tests/xpcshell/toolkit/components/telemetry/tests/unit)
misc:: $(call mkdir_deps,$(testdir))
$(EXIT_ON_ERROR) \
for dir in $(addondir)/*; do \
base=`basename $$dir`; \
(cd $$dir && zip -qr $(testdir)/$$base.xpi *); \
done

Просмотреть файл

@ -84,6 +84,9 @@ using base::LinearHistogram;
using base::StatisticsRecorder;
const char KEYED_HISTOGRAM_NAME_SEPARATOR[] = "#";
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
const char SUBSESSION_HISTOGRAM_PREFIX[] = "sub#";
#endif
enum reflectStatus {
REFLECT_OK,
@ -699,6 +702,10 @@ private:
nsresult GetHistogramByName(const nsACString &name, Histogram **ret);
bool ShouldReflectHistogram(Histogram *h);
void IdentifyCorruptHistograms(StatisticsRecorder::Histograms &hs);
nsresult CreateHistogramSnapshots(JSContext *cx,
JS::MutableHandle<JS::Value> ret,
bool subsession,
bool clearSubsession);
typedef StatisticsRecorder::Histograms::iterator HistogramIterator;
struct AddonHistogramInfo {
@ -774,18 +781,24 @@ public:
KeyedHistogram(const nsACString &name, const nsACString &expiration,
uint32_t histogramType, uint32_t min, uint32_t max,
uint32_t bucketCount);
nsresult GetHistogram(const nsCString& name, Histogram** histogram);
Histogram* GetHistogram(const nsCString& name);
nsresult GetHistogram(const nsCString& name, Histogram** histogram, bool subsession);
Histogram* GetHistogram(const nsCString& name, bool subsession);
uint32_t GetHistogramType() const { return mHistogramType; }
nsresult GetDataset(uint32_t* dataset) const;
nsresult GetJSKeys(JSContext* cx, JS::CallArgs& args);
nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj);
void Clear();
nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
bool subsession, bool clearSubsession);
nsresult Add(const nsCString& key, uint32_t aSample);
void Clear(bool subsession);
private:
typedef nsBaseHashtableET<nsCStringHashKey, Histogram*> KeyedHistogramEntry;
typedef AutoHashtable<KeyedHistogramEntry> KeyedHistogramMapType;
KeyedHistogramMapType mHistogramMap;
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
KeyedHistogramMapType mSubsessionMap;
#endif
struct ReflectKeysArgs {
JSContext* jsContext;
@ -991,6 +1004,93 @@ GetHistogramByEnumId(Telemetry::ID id, Histogram **ret)
return NS_OK;
}
/**
* This clones a histogram |existing| with the id |existingId| to a
* new histogram with the name |newName|.
* For simplicity this is limited to registered histograms.
*/
Histogram*
CloneHistogram(const nsACString& newName, Telemetry::ID existingId,
Histogram& existing)
{
const TelemetryHistogram &info = gHistograms[existingId];
Histogram *clone = nullptr;
nsresult rv;
rv = HistogramGet(PromiseFlatCString(newName).get(), info.expiration(),
info.histogramType, existing.declared_min(),
existing.declared_max(), existing.bucket_count(),
true, &clone);
if (NS_FAILED(rv)) {
return nullptr;
}
Histogram::SampleSet ss;
existing.SnapshotSample(&ss);
clone->AddSampleSet(ss);
return clone;
}
/**
* This clones a histogram with the id |existingId| to a new histogram
* with the name |newName|.
* For simplicity this is limited to registered histograms.
*/
Histogram*
CloneHistogram(const nsACString& newName, Telemetry::ID existingId)
{
Histogram *existing = nullptr;
nsresult rv = GetHistogramByEnumId(existingId, &existing);
if (NS_FAILED(rv)) {
return nullptr;
}
return CloneHistogram(newName, existingId, *existing);
}
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
Histogram*
GetSubsessionHistogram(Histogram& existing)
{
Telemetry::ID id;
nsresult rv = TelemetryImpl::GetHistogramEnumId(existing.histogram_name().c_str(), &id);
if (NS_FAILED(rv) || gHistograms[id].keyed) {
return nullptr;
}
static Histogram* subsession[Telemetry::HistogramCount] = {};
if (subsession[id]) {
return subsession[id];
}
NS_NAMED_LITERAL_CSTRING(prefix, SUBSESSION_HISTOGRAM_PREFIX);
nsDependentCString existingName(gHistograms[id].id());
if (StringBeginsWith(existingName, prefix)) {
return nullptr;
}
nsCString subsessionName(prefix);
subsessionName.Append(existingName);
subsession[id] = CloneHistogram(subsessionName, id, existing);
return subsession[id];
}
#endif
nsresult
HistogramAdd(Histogram& histogram, int32_t value)
{
histogram.Add(value);
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
if (Histogram* subsession = GetSubsessionHistogram(histogram)) {
subsession->Add(value);
}
#endif
return NS_OK;
}
bool
FillRanges(JSContext *cx, JS::Handle<JSObject*> array, Histogram *h)
{
@ -1089,6 +1189,7 @@ JSHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp)
}
Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
MOZ_ASSERT(h);
Histogram::ClassType type = h->histogram_type();
int32_t value = 1;
@ -1110,7 +1211,7 @@ JSHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp)
}
if (TelemetryImpl::CanRecord()) {
h->Add(value);
HistogramAdd(*h, value);
}
return true;
@ -1152,8 +1253,32 @@ JSHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
return false;
}
bool onlySubsession = false;
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (args.length() >= 1) {
if (!args[0].isBoolean()) {
JS_ReportError(cx, "Not a boolean");
return false;
}
onlySubsession = JS::ToBoolean(args[0]);
}
#endif
Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
h->Clear();
MOZ_ASSERT(h);
if(!onlySubsession) {
h->Clear();
}
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
if (Histogram* subsession = GetSubsessionHistogram(*h)) {
subsession->Clear();
}
#endif
return true;
}
@ -1243,17 +1368,7 @@ JSKeyedHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp)
}
}
Histogram* h = nullptr;
nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h);
if (NS_FAILED(rv)) {
JS_ReportError(cx, "Failed to get histogram");
return false;
}
if (TelemetryImpl::CanRecord()) {
h->Add(value);
}
keyed->Add(NS_ConvertUTF16toUTF8(key), value);
return true;
}
@ -1275,7 +1390,8 @@ JSKeyedHistogram_Keys(JSContext *cx, unsigned argc, JS::Value *vp)
}
bool
JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
KeyedHistogram_SnapshotImpl(JSContext *cx, unsigned argc, JS::Value *vp,
bool subsession, bool clearSubsession)
{
JSObject *obj = JS_THIS_OBJECT(cx, vp);
if (!obj) {
@ -1296,7 +1412,7 @@ JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
return false;
}
if (!NS_SUCCEEDED(keyed->GetJSSnapshot(cx, snapshot))) {
if (!NS_SUCCEEDED(keyed->GetJSSnapshot(cx, snapshot, subsession, clearSubsession))) {
JS_ReportError(cx, "Failed to reflect keyed histograms");
return false;
}
@ -1312,7 +1428,7 @@ JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
}
Histogram* h = nullptr;
nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h);
nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h, subsession);
if (NS_FAILED(rv)) {
JS_ReportError(cx, "Failed to get histogram");
return false;
@ -1337,6 +1453,33 @@ JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
}
}
bool
JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
{
return KeyedHistogram_SnapshotImpl(cx, argc, vp, false, false);
}
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
bool
JSKeyedHistogram_SubsessionSnapshot(JSContext *cx, unsigned argc, JS::Value *vp)
{
return KeyedHistogram_SnapshotImpl(cx, argc, vp, true, false);
}
#endif
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
bool
JSKeyedHistogram_SnapshotSubsessionAndClear(JSContext *cx, unsigned argc, JS::Value *vp)
{
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (args.length() != 0) {
JS_ReportError(cx, "No key arguments supported for snapshotSubsessionAndClear");
}
return KeyedHistogram_SnapshotImpl(cx, argc, vp, true, true);
}
#endif
bool
JSKeyedHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
{
@ -1350,7 +1493,23 @@ JSKeyedHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
return false;
}
keyed->Clear();
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
bool onlySubsession = false;
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (args.length() >= 1) {
if (!(args[0].isNumber() || args[0].isBoolean())) {
JS_ReportError(cx, "Not a boolean");
return false;
}
onlySubsession = JS::ToBoolean(args[0]);
}
keyed->Clear(onlySubsession);
#else
keyed->Clear(false);
#endif
return true;
}
@ -1391,6 +1550,10 @@ WrapAndReturnKeyedHistogram(KeyedHistogram *h, JSContext *cx, JS::MutableHandle<
return NS_ERROR_FAILURE;
if (!(JS_DefineFunction(cx, obj, "add", JSKeyedHistogram_Add, 2, 0)
&& JS_DefineFunction(cx, obj, "snapshot", JSKeyedHistogram_Snapshot, 1, 0)
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
&& JS_DefineFunction(cx, obj, "subsessionSnapshot", JSKeyedHistogram_SubsessionSnapshot, 1, 0)
&& JS_DefineFunction(cx, obj, "snapshotSubsessionAndClear", JSKeyedHistogram_SnapshotSubsessionAndClear, 0, 0)
#endif
&& JS_DefineFunction(cx, obj, "keys", JSKeyedHistogram_Keys, 0, 0)
&& JS_DefineFunction(cx, obj, "clear", JSKeyedHistogram_Clear, 0, 0)
&& JS_DefineFunction(cx, obj, "dataset", JSKeyedHistogram_Dataset, 0, 0))) {
@ -1667,6 +1830,18 @@ mFailedLockCount(0)
mTrackedDBs.MarkImmutable();
#endif
// Populate the static histogram name->id cache.
// Note that the histogram names are statically allocated.
for (uint32_t i = 0; i < Telemetry::HistogramCount; i++) {
CharPtrEntryType *entry = mHistogramMap.PutEntry(gHistograms[i].id());
entry->mData = (Telemetry::ID) i;
}
#ifdef DEBUG
mHistogramMap.MarkImmutable();
#endif
// Create registered keyed histograms
for (size_t i = 0; i < ArrayLength(gHistograms); ++i) {
const TelemetryHistogram& h = gHistograms[i];
if (!h.keyed) {
@ -1796,21 +1971,8 @@ TelemetryImpl::GetHistogramEnumId(const char *name, Telemetry::ID *id)
return NS_ERROR_FAILURE;
}
// Cache names
// Note the histogram names are statically allocated
TelemetryImpl::HistogramMapType *map = &sTelemetry->mHistogramMap;
if (!map->Count()) {
for (uint32_t i = 0; i < Telemetry::HistogramCount; i++) {
CharPtrEntryType *entry = map->PutEntry(gHistograms[i].id());
if (MOZ_UNLIKELY(!entry)) {
map->Clear();
return NS_ERROR_OUT_OF_MEMORY;
}
entry->mData = (Telemetry::ID) i;
}
}
CharPtrEntryType *entry = map->GetEntry(name);
const TelemetryImpl::HistogramMapType& map = sTelemetry->mHistogramMap;
CharPtrEntryType *entry = map.GetEntry(name);
if (!entry) {
return NS_ERROR_INVALID_ARG;
}
@ -1843,25 +2005,12 @@ TelemetryImpl::HistogramFrom(const nsACString &name, const nsACString &existing_
if (NS_FAILED(rv)) {
return rv;
}
const TelemetryHistogram &p = gHistograms[id];
Histogram *existing;
rv = GetHistogramByEnumId(id, &existing);
if (NS_FAILED(rv)) {
return rv;
Histogram* clone = CloneHistogram(name, id);
if (!clone) {
return NS_ERROR_FAILURE;
}
Histogram *clone;
rv = HistogramGet(PromiseFlatCString(name).get(), p.expiration(),
p.histogramType, existing->declared_min(),
existing->declared_max(), existing->bucket_count(),
true, &clone);
if (NS_FAILED(rv))
return rv;
Histogram::SampleSet ss;
existing->SnapshotSample(&ss);
clone->AddSampleSet(ss);
return WrapAndReturnHistogram(clone, cx, ret);
}
@ -2044,8 +2193,11 @@ TelemetryImpl::UnregisterAddonHistograms(const nsACString &id)
return NS_OK;
}
NS_IMETHODIMP
TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret)
nsresult
TelemetryImpl::CreateHistogramSnapshots(JSContext *cx,
JS::MutableHandle<JS::Value> ret,
bool subsession,
bool clearSubsession)
{
JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx));
if (!root_obj)
@ -2086,6 +2238,16 @@ TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value>
continue;
}
Histogram* original = h;
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
if (subsession) {
h = GetSubsessionHistogram(*h);
if (!h) {
continue;
}
}
#endif
hobj = JS_NewPlainObject(cx);
if (!hobj) {
return NS_ERROR_FAILURE;
@ -2099,15 +2261,39 @@ TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value>
case REFLECT_FAILURE:
return NS_ERROR_FAILURE;
case REFLECT_OK:
if (!JS_DefineProperty(cx, root_obj, h->histogram_name().c_str(), hobj,
JSPROP_ENUMERATE)) {
if (!JS_DefineProperty(cx, root_obj, original->histogram_name().c_str(),
hobj, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
if (subsession && clearSubsession) {
h->Clear();
}
#endif
}
return NS_OK;
}
NS_IMETHODIMP
TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret)
{
return CreateHistogramSnapshots(cx, ret, false, false);
}
NS_IMETHODIMP
TelemetryImpl::SnapshotSubsessionHistograms(bool clearSubsession,
JSContext *cx,
JS::MutableHandle<JS::Value> ret)
{
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
return CreateHistogramSnapshots(cx, ret, true, clearSubsession);
#else
return NS_OK;
#endif
}
bool
TelemetryImpl::CreateHistogramForAddon(const nsACString &name,
AddonHistogramInfo &info)
@ -2213,7 +2399,7 @@ TelemetryImpl::KeyedHistogramsReflector(const nsACString& key, nsAutoPtr<KeyedHi
return PL_DHASH_STOP;
}
if (!NS_SUCCEEDED(entry->GetJSSnapshot(cx, snapshot))) {
if (!NS_SUCCEEDED(entry->GetJSSnapshot(cx, snapshot, false, false))) {
return PL_DHASH_STOP;
}
@ -3338,7 +3524,7 @@ Accumulate(ID aHistogram, uint32_t aSample)
Histogram *h;
nsresult rv = GetHistogramByEnumId(aHistogram, &h);
if (NS_SUCCEEDED(rv))
h->Add(aSample);
HistogramAdd(*h, aSample);
}
void
@ -3351,11 +3537,7 @@ Accumulate(ID aID, const nsCString& aKey, uint32_t aSample)
const TelemetryHistogram& th = gHistograms[aID];
KeyedHistogram* keyed = TelemetryImpl::GetKeyedHistogramById(nsDependentCString(th.id()));
MOZ_ASSERT(keyed);
Histogram* histogram = keyed->GetHistogram(aKey);
if (histogram) {
histogram->Add(aSample);
}
keyed->Add(aKey, aSample);
}
void
@ -3373,7 +3555,7 @@ Accumulate(const char* name, uint32_t sample)
Histogram *h;
rv = GetHistogramByEnumId(id, &h);
if (NS_SUCCEEDED(rv)) {
h->Add(sample);
HistogramAdd(*h, sample);
}
}
@ -3776,6 +3958,9 @@ KeyedHistogram::KeyedHistogram(const nsACString &name, const nsACString &expirat
uint32_t histogramType, uint32_t min, uint32_t max,
uint32_t bucketCount)
: mHistogramMap()
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
, mSubsessionMap()
#endif
, mName(name)
, mExpiration(expiration)
, mHistogramType(histogramType)
@ -3786,15 +3971,27 @@ KeyedHistogram::KeyedHistogram(const nsACString &name, const nsACString &expirat
}
nsresult
KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram)
KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram,
bool subsession)
{
KeyedHistogramEntry* entry = mHistogramMap.GetEntry(key);
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
#else
KeyedHistogramMapType& map = mHistogramMap;
#endif
KeyedHistogramEntry* entry = map.GetEntry(key);
if (entry) {
*histogram = entry->mData;
return NS_OK;
}
nsCString histogramName = mName;
nsCString histogramName;
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
if (subsession) {
histogramName.Append(SUBSESSION_HISTOGRAM_PREFIX);
}
#endif
histogramName.Append(mName);
histogramName.Append(KEYED_HISTOGRAM_NAME_SEPARATOR);
histogramName.Append(key);
@ -3810,7 +4007,7 @@ KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram)
h->SetFlags(Histogram::kExtendedStatisticsFlag);
*histogram = h;
entry = mHistogramMap.PutEntry(key);
entry = map.PutEntry(key);
if (MOZ_UNLIKELY(!entry)) {
return NS_ERROR_OUT_OF_MEMORY;
}
@ -3820,10 +4017,10 @@ KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram)
}
Histogram*
KeyedHistogram::GetHistogram(const nsCString& key)
KeyedHistogram::GetHistogram(const nsCString& key, bool subsession)
{
Histogram* h = nullptr;
if (NS_FAILED(GetHistogram(key, &h))) {
if (NS_FAILED(GetHistogram(key, &h, subsession))) {
return nullptr;
}
return h;
@ -3852,9 +4049,44 @@ KeyedHistogram::ClearHistogramEnumerator(KeyedHistogramEntry* entry, void*)
return PL_DHASH_NEXT;
}
void
KeyedHistogram::Clear()
nsresult
KeyedHistogram::Add(const nsCString& key, uint32_t sample)
{
if (!TelemetryImpl::CanRecord()) {
return NS_OK;
}
Histogram* histogram = GetHistogram(key, false);
MOZ_ASSERT(histogram);
if (!histogram) {
return NS_ERROR_FAILURE;
}
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
Histogram* subsession = GetHistogram(key, true);
MOZ_ASSERT(subsession);
if (!subsession) {
return NS_ERROR_FAILURE;
}
#endif
histogram->Add(sample);
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
subsession->Add(sample);
#endif
return NS_OK;
}
void
KeyedHistogram::Clear(bool onlySubsession)
{
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
mSubsessionMap.EnumerateEntries(&KeyedHistogram::ClearHistogramEnumerator, nullptr);
mSubsessionMap.Clear();
if (onlySubsession) {
return;
}
#endif
mHistogramMap.EnumerateEntries(&KeyedHistogram::ClearHistogramEnumerator, nullptr);
mHistogramMap.Clear();
}
@ -3920,11 +4152,23 @@ KeyedHistogram::ReflectKeyedHistogram(KeyedHistogramEntry* entry, JSContext* cx,
}
nsresult
KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj)
KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
bool subsession, bool clearSubsession)
{
if (!mHistogramMap.ReflectIntoJS(&KeyedHistogram::ReflectKeyedHistogram, cx, obj)) {
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
#else
KeyedHistogramMapType& map = mHistogramMap;
#endif
if (!map.ReflectIntoJS(&KeyedHistogram::ReflectKeyedHistogram, cx, obj)) {
return NS_ERROR_FAILURE;
}
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
if (subsession && clearSubsession) {
Clear(true);
}
#endif
return NS_OK;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -244,13 +244,9 @@ this.TelemetryFile = {
*
* @return {iterator}
*/
popPendingPings: function*(reason) {
popPendingPings: function*() {
while (pendingPings.length > 0) {
let data = pendingPings.pop();
// Send persisted pings to the test URL too.
if (reason == "test-ping") {
data.reason = reason;
}
yield data;
}
},
@ -263,7 +259,9 @@ this.TelemetryFile = {
///// Utility functions
function pingFilePath(ping) {
return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
// Support legacy ping formats, who don't have an "id" field, but a "slug" field.
let pingIdentifier = (ping.slug) ? ping.slug : ping.id;
return OS.Path.join(TelemetryFile.pingDirectoryPath, pingIdentifier);
}
function getPingDirectory() {

Просмотреть файл

@ -30,10 +30,14 @@ const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID"
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
const PING_FORMAT_VERSION = 4;
// Delay before intializing telemetry (ms)
const TELEMETRY_DELAY = 60000;
// Delay before initializing telemetry if we're testing (ms)
const TELEMETRY_TEST_DELAY = 100;
// The number of days to keep pings serialised on the disk in case of failures.
const DEFAULT_RETENTION_DAYS = 14;
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
"@mozilla.org/base/telemetry;1",
@ -46,6 +50,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
"resource://gre/modules/TelemetryLog.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ThirdPartyCookieProbe",
"resource://gre/modules/ThirdPartyCookieProbe.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
"resource://gre/modules/TelemetryEnvironment.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
/**
* Setup Telemetry logging. This function also gets called when loggin related
@ -86,6 +94,14 @@ function generateUUID() {
return str.substring(1, str.length - 1);
}
/**
* Determine if the ping has new ping format or a legacy one.
*/
function isNewPingFormat(aPing) {
return ("id" in aPing) && ("application" in aPing) &&
("version" in aPing) && (aPing.version >= 2);
}
this.EXPORTED_SYMBOLS = ["TelemetryPing"];
this.TelemetryPing = Object.freeze({
@ -131,9 +147,105 @@ this.TelemetryPing = Object.freeze({
/**
* Send payloads to the server.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} [aOptions] Options object.
* @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
* id, false otherwise.
* @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
* environment data.
* @returns {Promise} A promise that resolves when the ping is sent.
*/
send: function(aReason, aPingPayload) {
return Impl.send(aReason, aPingPayload);
send: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
options.addClientId = aOptions.addClientId || false;
options.addEnvironment = aOptions.addEnvironment || false;
return Impl.send(aType, aPayload, options);
},
/**
* Add the ping to the pending ping list and save all pending pings.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} [aOptions] Options object.
* @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
* id, false otherwise.
* @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
* environment data.
* @returns {Promise} A promise that resolves when the pings are saved.
*/
savePendingPings: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
options.addClientId = aOptions.addClientId || false;
options.addEnvironment = aOptions.addEnvironment || false;
return Impl.savePendingPings(aType, aPayload, options);
},
/**
* Save a ping to disk.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} [aOptions] Options object.
* @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
* id, false otherwise.
* @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
* environment data.
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
* if found.
*
* @returns {Promise} A promise that resolves when the ping is saved to disk.
*/
savePing: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
options.addClientId = aOptions.addClientId || false;
options.addEnvironment = aOptions.addEnvironment || false;
options.overwrite = aOptions.overwrite || false;
return Impl.savePing(aType, aPayload, options);
},
/**
* Only used for testing. Saves a ping to disk and return the ping id once done.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} [aOptions] Options object.
* @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
* id, false otherwise.
* @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
* environment data.
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
* if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
* saved to disk.
*/
testSavePingToFile: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
options.addClientId = aOptions.addClientId || false;
options.addEnvironment = aOptions.addEnvironment || false;
options.overwrite = aOptions.overwrite || false;
return Impl.testSavePingToFile(aType, aPayload, options);
},
/**
@ -144,23 +256,111 @@ this.TelemetryPing = Object.freeze({
get clientID() {
return Impl.clientID;
},
/**
* The AsyncShutdown.Barrier to synchronize with TelemetryPing shutdown.
*/
get shutdown() {
return Impl._shutdownBarrier.client;
},
});
let Impl = {
_initialized: false,
_initStarted: false, // Whether we started setting up TelemetryPing.
_log: null,
_prevValues: {},
// The previous build ID, if this is the first run with a new build.
// Undefined if this is not the first run, or the previous build ID is unknown.
_previousBuildID: undefined,
_clientID: null,
// A task performing delayed initialization
_delayedInitTask: null,
// The deferred promise resolved when the initialization task completes.
_delayedInitTaskDeferred: null,
popPayloads: function popPayloads(reason, externalPayload) {
_shutdownBarrier: new AsyncShutdown.Barrier("TelemetryPing: Waiting for clients."),
/**
* Get the data for the "application" section of the ping.
*/
_getApplicationSection: function() {
// Querying architecture and update channel can throw. Make sure to recover and null
// those fields.
let arch = null;
try {
arch = Services.sysinfo.get("arch");
} catch (e) {
this._log.trace("assemblePing - Unable to get system architecture.", e);
}
let updateChannel = null;
try {
updateChannel = UpdateChannel.get();
} catch (e) {
this._log.trace("assemblePing - Unable to get update channel.", e);
}
return {
architecture: arch,
buildId: Services.appinfo.appBuildID,
name: Services.appinfo.name,
version: Services.appinfo.version,
vendor: Services.appinfo.vendor,
platformVersion: Services.appinfo.platformVersion,
xpcomAbi: Services.appinfo.XPCOMABI,
channel: updateChannel,
};
},
/**
* Assemble a complete ping following the common ping format specification.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} aOptions Options object.
* @param {Boolean} aOptions.addClientId true if the ping should contain the client
* id, false otherwise.
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
*
* @returns Promise<Object> A promise that resolves when the ping is completely assembled.
*/
assemblePing: function assemblePing(aType, aPayload, aOptions = {}) {
this._log.trace("assemblePing - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
// Fill the common ping fields.
let pingData = {
type: aType,
id: generateUUID(),
creationDate: (new Date()).toISOString(),
version: PING_FORMAT_VERSION,
application: this._getApplicationSection(),
payload: aPayload,
};
if (aOptions.addClientId) {
pingData.clientId = this._clientID;
}
if (aOptions.addEnvironment) {
return TelemetryEnvironment.getEnvironmentData().then(environment => {
pingData.environment = environment;
return pingData;
},
error => {
this._log.error("assemblePing - Rejection", error);
});
}
return Promise.resolve(pingData);
},
popPayloads: function popPayloads() {
this._log.trace("popPayloads");
function payloadIter() {
if (externalPayload && reason != "overdue-flush") {
yield externalPayload;
}
let iterator = TelemetryFile.popPendingPings(reason);
let iterator = TelemetryFile.popPendingPings();
for (let data of iterator) {
yield data;
}
@ -178,29 +378,142 @@ let Impl = {
},
/**
* Send data to the server. Record success/send-time in histograms
* Build a complete ping and send data to the server. Record success/send-time in
* histograms.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} aOptions Options object.
* @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
* false otherwise.
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
*
* @returns {Promise} A promise that resolves when the ping is sent.
*/
send: function send(reason, aPayload) {
this._log.trace("send - Reason " + reason + ", Server " + this._server);
return this.sendPingsFromIterator(this._server, reason,
Iterator(this.popPayloads(reason, aPayload)));
send: function send(aType, aPayload, aOptions) {
this._log.trace("send - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => {
// Once ping is assembled, send it along with the persisted ping in the backlog.
let p = [
// Persist the ping if sending it fails.
this.doPing(pingData, false)
.catch(() => TelemetryFile.savePing(pingData, true)),
this.sendPersistedPings(),
];
return Promise.all(p);
},
error => this._log.error("send - Rejection", error));
},
sendPingsFromIterator: function sendPingsFromIterator(server, reason, i) {
let p = [data for (data in i)].map((data) =>
this.doPing(server, data).then(null, () => TelemetryFile.savePing(data, true)));
/**
* Send the persisted pings to the server.
*/
sendPersistedPings: function sendPersistedPings() {
this._log.trace("sendPersistedPings");
let pingsIterator = Iterator(this.popPayloads());
let p = [data for (data in pingsIterator)].map(data => this.doPing(data, true));
return Promise.all(p);
},
finishPingRequest: function finishPingRequest(success, startTime, ping) {
/**
* Saves all the pending pings, plus the passed one, to disk.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} aOptions Options object.
* @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
* false otherwise.
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
*
* @returns {Promise} A promise that resolves when all the pings are saved to disk.
*/
savePendingPings: function savePendingPings(aType, aPayload, aOptions) {
this._log.trace("savePendingPings - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => TelemetryFile.savePendingPings(pingData),
error => this._log.error("savePendingPings - Rejection", error));
},
/**
* Save a ping to disk.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} aOptions Options object.
* @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
* false otherwise.
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
*
* @returns {Promise} A promise that resolves when the ping is saved to disk.
*/
savePing: function savePing(aType, aPayload, aOptions) {
this._log.trace("savePing - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => TelemetryFile.savePing(pingData, aOptions.overwrite),
error => this._log.error("savePing - Rejection", error));
},
/**
* Save a ping to disk and return the ping id when done.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} aOptions Options object.
* @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
* false otherwise.
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise} A promise that resolves with the ping id when the ping is saved to
* disk.
*/
testSavePingToFile: function testSavePingToFile(aType, aPayload, aOptions) {
this._log.trace("testSavePingToFile - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => {
if (aOptions.filePath) {
return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
.then(() => { return pingData.id; });
} else {
return TelemetryFile.savePing(pingData, aOptions.overwrite)
.then(() => { return pingData.id; });
}
}, error => this._log.error("testSavePing - Rejection", error));
},
finishPingRequest: function finishPingRequest(success, startTime, ping, isPersisted) {
this._log.trace("finishPingRequest - Success " + success + ", Persisted " + isPersisted);
let hping = Telemetry.getHistogramById("TELEMETRY_PING");
let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
hsuccess.add(success);
hping.add(new Date() - startTime);
if (success) {
if (success && isPersisted) {
return TelemetryFile.cleanupPingFile(ping);
} else {
return Promise.resolve();
@ -208,23 +521,44 @@ let Impl = {
},
submissionPath: function submissionPath(ping) {
let slug;
if (!ping) {
slug = this._uuid;
// The new ping format contains an "application" section, the old one doesn't.
let pathComponents;
if (isNewPingFormat(ping)) {
// We insert the Ping id in the URL to simplify server handling of duplicated
// pings.
let app = ping.application;
pathComponents = [
ping.id, ping.type, app.name, app.version, app.channel, app.buildId
];
} else {
let info = ping.payload.info;
let pathComponents = [ping.slug, info.reason, info.appName,
info.appVersion, info.appUpdateChannel,
info.appBuildID];
slug = pathComponents.join("/");
// This is a ping in the old format.
if (!("slug" in ping)) {
// That's odd, we don't have a slug. Generate one so that TelemetryFile.jsm works.
ping.slug = generateUUID();
}
// Do we have enough info to build a submission URL?
let payload = ("payload" in ping) ? ping.payload : null;
if (payload && ("info" in payload)) {
let info = ping.payload.info;
pathComponents = [ ping.slug, info.reason, info.appName, info.appVersion,
info.appUpdateChannel, info.appBuildID ];
} else {
// Only use the UUID as the slug.
pathComponents = [ ping.slug ];
}
}
let slug = pathComponents.join("/");
return "/submit/telemetry/" + slug;
},
doPing: function doPing(server, ping) {
this._log.trace("doPing - Server " + server);
doPing: function doPing(ping, isPersisted) {
this._log.trace("doPing - Server " + this._server + ", Persisted " + isPersisted);
let deferred = Promise.defer();
let url = server + this.submissionPath(ping);
let isNewPing = isNewPingFormat(ping);
let version = isNewPing ? PING_FORMAT_VERSION : 1;
let url = this._server + this.submissionPath(ping) + "?v=" + version;
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.mozBackgroundRequest = true;
@ -235,24 +569,34 @@ let Impl = {
let startTime = new Date();
function handler(success) {
let handleCompletion = event => {
if (success) {
deferred.resolve();
} else {
deferred.reject(event);
}
};
return function(event) {
this.finishPingRequest(success, startTime, ping).then(() => {
if (success) {
deferred.resolve();
} else {
deferred.reject(event);
}
});
this.finishPingRequest(success, startTime, ping, isPersisted)
.then(() => handleCompletion(event),
error => {
this._log.error("doPing - Request Success " + success + ", Error " +
error);
handleCompletion(event);
});
};
}
request.addEventListener("error", handler(false).bind(this), false);
request.addEventListener("load", handler(true).bind(this), false);
// If that's a legacy ping format, just send its payload.
let networkPayload = isNewPing ? ping : ping.payload;
request.setRequestHeader("Content-Encoding", "gzip");
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(ping.payload));
let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(networkPayload));
utf8Payload += converter.Finish();
let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
@ -316,14 +660,32 @@ let Impl = {
/**
* Initializes telemetry within a timer. If there is no PREF_SERVER set, don't turn on telemetry.
*
* This delayed initialization means TelemetryPing init can be in the following states:
* 1) setupTelemetry was never called
* or it was called and
* 2) _delayedInitTask was scheduled, but didn't run yet.
* 3) _delayedInitTask is currently running.
* 4) _delayedInitTask finished running and is nulled out.
*/
setupTelemetry: function setupTelemetry(testing) {
this._initStarted = true;
if (testing && !this._log) {
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
}
this._log.trace("setupTelemetry");
if (this._delayedInitTask) {
this._log.error("setupTelemetry - init task already running");
return this._delayedInitTaskDeferred.promise;
}
if (this._initialized && !testing) {
this._log.error("setupTelemetry - already initialized");
return Promise.resolve();
}
// Initialize some probes that are kept in their own modules
this._thirdPartyCookies = new ThirdPartyCookieProbe();
this._thirdPartyCookies.init();
@ -342,41 +704,91 @@ let Impl = {
// Delay full telemetry initialization to give the browser time to
// run various late initializers. Otherwise our gathered memory
// footprint and other numbers would be too optimistic.
let deferred = Promise.defer();
let delayedTask = new DeferredTask(function* () {
this._initialized = true;
this._delayedInitTaskDeferred = Promise.defer();
this._delayedInitTask = new DeferredTask(function* () {
try {
this._initialized = true;
yield TelemetryFile.loadSavedPings();
// If we have any TelemetryPings lying around, we'll be aggressive
// and try to send them all off ASAP.
if (TelemetryFile.pingsOverdue > 0) {
this._log.trace("setupChromeProcess - Sending " + TelemetryFile.pingsOverdue +
" overdue pings now.");
// It doesn't really matter what we pass to this.send as a reason,
// since it's never sent to the server. All that this.send does with
// the reason is check to make sure it's not a test-ping.
yield this.send("overdue-flush");
yield TelemetryEnvironment.init();
yield TelemetryFile.loadSavedPings();
// If we have any TelemetryPings lying around, we'll be aggressive
// and try to send them all off ASAP.
if (TelemetryFile.pingsOverdue > 0) {
this._log.trace("setupChromeProcess - Sending " + TelemetryFile.pingsOverdue +
" overdue pings now.");
// It doesn't really matter what we pass to this.send as a reason,
// since it's never sent to the server. All that this.send does with
// the reason is check to make sure it's not a test-ping.
yield this.sendPersistedPings();
}
if ("@mozilla.org/datareporting/service;1" in Cc) {
let drs = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
this._clientID = yield drs.getClientID();
// Update cached client id.
Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
} else {
// Nuke potentially cached client id.
Preferences.reset(PREF_CACHED_CLIENTID);
}
Telemetry.asyncFetchTelemetryData(function () {});
this._delayedInitTaskDeferred.resolve();
} catch (e) {
this._delayedInitTaskDeferred.reject(e);
} finally {
this._delayedInitTask = null;
this._delayedInitTaskDeferred = null;
}
if ("@mozilla.org/datareporting/service;1" in Cc) {
let drs = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
this._clientID = yield drs.getClientID();
// Update cached client id.
Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
} else {
// Nuke potentially cached client id.
Preferences.reset(PREF_CACHED_CLIENTID);
}
Telemetry.asyncFetchTelemetryData(function () {});
deferred.resolve();
}.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
delayedTask.arm();
return deferred.promise;
AsyncShutdown.sendTelemetry.addBlocker("TelemetryPing: shutting down",
() => this.shutdown(),
() => this._getState());
this._delayedInitTask.arm();
return this._delayedInitTaskDeferred.promise;
},
shutdown: function() {
this._log.trace("shutdown");
let cleanup = () => {
if (!this._initialized) {
return;
}
let reset = () => {
this._initialized = false;
this._initStarted = false;
};
return this._shutdownBarrier.wait().then(
() => TelemetryEnvironment.shutdown().then(reset, reset));
};
// We can be in one the following states here:
// 1) setupTelemetry was never called
// or it was called and
// 2) _delayedInitTask was scheduled, but didn't run yet.
// 3) _delayedInitTask is running now.
// 4) _delayedInitTask finished running already.
// This handles 1).
if (!this._initStarted) {
return Promise.resolve();
}
// This handles 4).
if (!this._delayedInitTask) {
// We already ran the delayed initialization.
return cleanup();
}
// This handles 2) and 3).
this._delayedInitTask.disarm();
return this._delayedInitTask.finalize().then(cleanup);
},
/**
@ -412,4 +824,15 @@ let Impl = {
get clientID() {
return this._clientID;
},
/**
* Get an object describing the current state of this module for AsyncShutdown diagnostics.
*/
_getState: function() {
return {
initialized: this._initialized,
initStarted: this._initStarted,
haveDelayedInitTask: !!this._delayedInitTask,
};
},
};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -17,10 +17,10 @@ Finally, the structure also contains the `payload`, which is the specific data s
Structure::
{
type: <string>, // "main", "activation", "deletion", ...
type: <string>, // "main", "activation", "deletion", "saved-session", ...
id: <UUID>, // a UUID that identifies this ping
creationDate: <ISO date>, // the date the ping was generated
version: <number>, // the version of the ping format, currently 2
version: <number>, // the version of the ping format, currently 4
application: {
architecture: <string>, // build architecture, e.g. x86

Просмотреть файл

@ -14,6 +14,7 @@ Structure::
{
build: {
applicationId: <string>, // nsIXULAppInfo.ID
applicationName: <string>, // "Firefox"
architecture: <string>, // e.g. "x86", build architecture for the active build
architecturesInBinary: <string>, // e.g. "i386-x86_64", from nsIMacUtils.architecturesInBinary, only present for mac universal builds
buildId: <string>, // e.g. "20141126041045"
@ -24,22 +25,22 @@ Structure::
hotfixVersion: <string>, // e.g. "20141211.01"
},
settings: {
blocklistEnabled: <bool>, // false on failure
isDefaultBrowser: <bool>, // null on failure
blocklistEnabled: <bool>, // true on failure
isDefaultBrowser: <bool>, // null on failure, not available on Android
e10sEnabled: <bool>, // false on failure
telemetryEnabled: <bool>, // false on failure
locale: <string>, // e.g. "it", null on failure
update: {
channel: <string>, // e.g. "release", null on failure
enabled: <bool>, // false on failure
autoDownload: <bool>, // false on failure
enabled: <bool>, // true on failure
autoDownload: <bool>, // true on failure
},
userPrefs: {
// Two possible behaviours: values of the whitelisted prefs, or for some prefs we
// only record they are present with value being set to null.
},
},
profile: {
profile: { // This section is not available on Android.
creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
resetDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
},
@ -86,16 +87,16 @@ Structure::
},
hdd: {
profile: { // hdd where the profile folder is located
model: <string>, // null on failure
revision: <string>, // null on failure
model: <string>, // windows only or null on failure
revision: <string>, // windows only or null on failure
},
binary: { // hdd where the application binary is located
model: <string>, // null on failure
revision: <string>, // null on failure
model: <string>, // windows only or null on failure
revision: <string>, // windows only or null on failure
},
system: { // hdd where the system files are located
model: <string>, // null on failure
revision: <string>, // null on failure
model: <string>, // windows only or null on failure
revision: <string>, // windows only or null on failure
},
},
gfx: {
@ -122,7 +123,7 @@ Structure::
activeAddons: { // the currently enabled addons
<addon id>: {
blocklisted: <bool>,
description: <string>,
description: <string>, // null if not available
name: <string>,
userDisabled: <bool>,
appDisabled: <bool>,

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше