author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Thu, 21 Jul 2016 16:23:11 +0200 | |
changeset 306096 | e28e856b987380f55d699092f11f6997378f79a6 |
parent 306062 | 29ead859749af91a4e70d10a278a0ca3fca9d2b4 (current diff) |
parent 306095 | cab3629ad5fd8f7d6c960bdf966b14cfb06e7eb3 (diff) |
child 306097 | 6b180266ac16e3226be33319ff710ddfa85f5836 |
push id | 79765 |
push user | cbook@mozilla.com |
push date | Thu, 21 Jul 2016 14:26:34 +0000 |
treeherder | mozilla-inbound@ab54bfc55266 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 50.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/b2g/chrome/content/devtools/hud.js +++ b/b2g/chrome/content/devtools/hud.js @@ -20,17 +20,17 @@ XPCOMUtils.defineLazyGetter(this, 'Debug return devtools.require('devtools/shared/client/main').DebuggerClient; }); XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() { return devtools.require('devtools/shared/webconsole/utils').Utils; }); XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() { - return devtools.require('devtools/server/actors/eventlooplag').EventLoopLagFront; + return devtools.require('devtools/shared/fronts/eventlooplag').EventLoopLagFront; }); XPCOMUtils.defineLazyGetter(this, 'PerformanceEntriesFront', function() { return devtools.require('devtools/server/actors/performance-entries').PerformanceEntriesFront; }); XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() { return devtools.require('devtools/server/actors/memory').MemoryFront;
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -6,17 +6,17 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. <?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/content/usercontext/usercontext.css" type="text/css"?> <?xml-stylesheet href="chrome://devtools/skin/devtools-browser.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/controlcenter/panel.css" type="text/css"?> -<?xml-stylesheet href="chrome://browser/skin/customizableui/panelUIOverlay.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/customizableui/panelUI.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/browser-lightweightTheme.css" type="text/css"?> <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> <?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> <?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> # All DTD information is stored in a separate file so that it can be shared by
--- a/browser/components/extensions/ext-history.js +++ b/browser/components/extensions/ext-history.js @@ -94,27 +94,25 @@ var _observer; function getObserver() { if (!_observer) { _observer = { onDeleteURI: function(uri, guid, reason) { this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]}); }, onVisit: function(uri, visitId, time, sessionId, referringId, transitionType, guid, hidden, visitCount, typed) { - PlacesUtils.promisePlaceInfo(guid).then(placeInfo => { - let data = { - id: guid, - url: uri.spec, - title: placeInfo.title, - lastVisitTime: time / 1000, // time from Places is microseconds, - visitCount, - typedCount: typed, - }; - this.emit("visited", data); - }); + let data = { + id: guid, + url: uri.spec, + title: "", + lastVisitTime: time / 1000, // time from Places is microseconds, + visitCount, + typedCount: typed, + }; + this.emit("visited", data); }, onBeginUpdateBatch: function() {}, onEndUpdateBatch: function() {}, onTitleChanged: function() {}, onClearHistory: function() { this.emit("visitRemoved", {allHistory: true}); }, onPageChanged: function() {},
--- a/browser/components/extensions/test/browser/browser_ext_history.js +++ b/browser/components/extensions/test/browser/browser_ext_history.js @@ -454,17 +454,19 @@ add_task(function* test_on_visited() { yield PlacesUtils.history.insertMany(PAGE_INFOS); let onVisitedData = yield extension.awaitMessage("on-visited-data"); function checkOnVisitedData(index, expected) { let onVisited = onVisitedData[index]; ok(PlacesUtils.isValidGuid(onVisited.id), "onVisited received a valid id"); is(onVisited.url, expected.url, "onVisited received the expected url"); - is(onVisited.title, expected.title, "onVisited received the expected title"); + // Title will be blank until bug 1287928 lands + // https://bugzilla.mozilla.org/show_bug.cgi?id=1287928 + is(onVisited.title, "", "onVisited received a blank title"); is(onVisited.lastVisitTime, expected.time, "onVisited received the expected time"); is(onVisited.visitCount, expected.visitCount, "onVisited received the expected visitCount"); } let expected = { url: PAGE_INFOS[0].url, title: PAGE_INFOS[0].title, time: PAGE_INFOS[0].visits[0].date.getTime(),
rename from browser/themes/linux/customizableui/panelUIOverlay.css rename to browser/themes/linux/customizableui/panelUI.css --- a/browser/themes/linux/customizableui/panelUIOverlay.css +++ b/browser/themes/linux/customizableui/panelUI.css @@ -1,13 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -%include ../../shared/customizableui/panelUIOverlay.inc.css +%include ../../shared/customizableui/panelUI.inc.css .panel-subviews { background-color: -moz-dialog; } #BMB_bookmarksPopup > menuitem[type="checkbox"] { -moz-appearance: none !important; /* important, to override toolkit rule */ }
--- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -46,17 +46,17 @@ browser.jar: skin/classic/browser/Toolbar-inverted@2x.png skin/classic/browser/Toolbar-small.png skin/classic/browser/webRTC-indicator.css * skin/classic/browser/controlcenter/panel.css (controlcenter/panel.css) skin/classic/browser/customizableui/background-noise-toolbar.png (customizableui/background-noise-toolbar.png) skin/classic/browser/customizableui/customizeMode-gridTexture.png (customizableui/customizeMode-gridTexture.png) skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png (customizableui/customizeMode-separatorHorizontal.png) skin/classic/browser/customizableui/customizeMode-separatorVertical.png (customizableui/customizeMode-separatorVertical.png) -* skin/classic/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css) +* skin/classic/browser/customizableui/panelUI.css (customizableui/panelUI.css) * skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css) skin/classic/browser/downloads/buttons.png (downloads/buttons.png) skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png) skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png) skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png) skin/classic/browser/downloads/download-summary.png (downloads/download-summary.png) * skin/classic/browser/downloads/downloads.css (downloads/downloads.css) skin/classic/browser/feeds/feedIcon.png (feeds/feedIcon.png)
rename from browser/themes/osx/customizableui/panelUIOverlay.css rename to browser/themes/osx/customizableui/panelUI.css --- a/browser/themes/osx/customizableui/panelUIOverlay.css +++ b/browser/themes/osx/customizableui/panelUI.css @@ -1,13 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -%include ../../shared/customizableui/panelUIOverlay.inc.css +%include ../../shared/customizableui/panelUI.inc.css .panel-subviews { background-color: hsla(0,0%,100%,.97); } .panelUI-grid .toolbarbutton-1 { margin-right: 0; margin-left: 0;
--- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -67,17 +67,17 @@ browser.jar: skin/classic/browser/webRTC-indicator.css * skin/classic/browser/controlcenter/panel.css (controlcenter/panel.css) skin/classic/browser/customizableui/background-noise-toolbar.png (customizableui/background-noise-toolbar.png) skin/classic/browser/customizableui/customize-titleBar-toggle.png (customizableui/customize-titleBar-toggle.png) skin/classic/browser/customizableui/customize-titleBar-toggle@2x.png (customizableui/customize-titleBar-toggle@2x.png) skin/classic/browser/customizableui/customizeMode-gridTexture.png (customizableui/customizeMode-gridTexture.png) skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png (customizableui/customizeMode-separatorHorizontal.png) skin/classic/browser/customizableui/customizeMode-separatorVertical.png (customizableui/customizeMode-separatorVertical.png) -* skin/classic/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css) +* skin/classic/browser/customizableui/panelUI.css (customizableui/panelUI.css) * skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css) skin/classic/browser/downloads/buttons.png (downloads/buttons.png) skin/classic/browser/downloads/buttons@2x.png (downloads/buttons@2x.png) skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png) skin/classic/browser/downloads/download-glow-menuPanel@2x.png (downloads/download-glow-menuPanel@2x.png) skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png) skin/classic/browser/downloads/download-notification-finish@2x.png (downloads/download-notification-finish@2x.png) skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)
rename from browser/themes/shared/customizableui/panelUIOverlay.inc.css rename to browser/themes/shared/customizableui/panelUI.inc.css
rename from browser/themes/windows/customizableui/panelUIOverlay.css rename to browser/themes/windows/customizableui/panelUI.css --- a/browser/themes/windows/customizableui/panelUIOverlay.css +++ b/browser/themes/windows/customizableui/panelUI.css @@ -1,13 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -%include ../../shared/customizableui/panelUIOverlay.inc.css +%include ../../shared/customizableui/panelUI.inc.css .panel-subviews { background-color: -moz-field; } #PanelUI-contents #zoom-out-btn { padding-left: 12px; padding-right: 12px;
--- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -81,17 +81,17 @@ browser.jar: * skin/classic/browser/controlcenter/panel.css (controlcenter/panel.css) skin/classic/browser/customizableui/background-noise-toolbar.png (customizableui/background-noise-toolbar.png) skin/classic/browser/customizableui/customize-titleBar-toggle.png (customizableui/customize-titleBar-toggle.png) skin/classic/browser/customizableui/customize-titleBar-toggle@2x.png (customizableui/customize-titleBar-toggle@2x.png) skin/classic/browser/customizableui/customizeMode-gridTexture.png (customizableui/customizeMode-gridTexture.png) skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png (customizableui/customizeMode-separatorHorizontal.png) skin/classic/browser/customizableui/customizeMode-separatorVertical.png (customizableui/customizeMode-separatorVertical.png) skin/classic/browser/customizableui/menu-arrow.svg (customizableui/menu-arrow.svg) -* skin/classic/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css) +* skin/classic/browser/customizableui/panelUI.css (customizableui/panelUI.css) * skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css) skin/classic/browser/downloads/buttons.png (downloads/buttons.png) skin/classic/browser/downloads/buttons-XP.png (downloads/buttons-XP.png) skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png) skin/classic/browser/downloads/download-glow-menuPanel-XPVista7.png (downloads/download-glow-menuPanel-XPVista7.png) skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png) skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png) skin/classic/browser/downloads/download-summary.png (downloads/download-summary.png)
new file mode 100644 --- /dev/null +++ b/devtools/client/framework/location-store.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const SOURCE_TOKEN = "<:>"; + +function LocationStore (store) { + this._store = store || new Map(); +} + +/** + * Method to get a promised location from the Store. + * @param location + * @returns Promise<Object> + */ +LocationStore.prototype.get = function (location) { + this._safeAccessInit(location.url); + return this._store.get(location.url).get(location); +}; + +/** + * Method to set a promised location to the Store + * @param location + * @param promisedLocation + */ +LocationStore.prototype.set = function (location, promisedLocation = null) { + this._safeAccessInit(location.url); + this._store.get(location.url).set(serialize(location), promisedLocation); +}; + +/** + * Utility method to verify if key exists in Store before accessing it. + * If not, initializing it. + * @param url + * @private + */ +LocationStore.prototype._safeAccessInit = function (url) { + if (!this._store.has(url)) { + this._store.set(url, new Map()); + } +}; + +/** + * Utility proxy method to Map.clear() method + */ +LocationStore.prototype.clear = function () { + this._store.clear(); +}; + +/** + * Retrieves an object containing all locations to be resolved when `source-updated` + * event is triggered. + * @param url + * @returns {Array<String>} + */ +LocationStore.prototype.getByURL = function (url){ + if (this._store.has(url)) { + return [...this._store.get(url).keys()]; + } + return []; +}; + +/** + * Invalidates the stale location promises from the store when `source-updated` + * event is triggered, and when FrameView unsubscribes from a location. + * @param url + */ +LocationStore.prototype.clearByURL = function (url) { + this._safeAccessInit(url); + this._store.set(url, new Map()); +}; + +exports.LocationStore = LocationStore; +exports.serialize = serialize; +exports.deserialize = deserialize; + +/** + * Utility method to serialize the source + * @param source + * @returns {string} + */ +function serialize(source) { + let { url, line, column } = source; + line = line || 0; + column = column || 0; + return `${url}${SOURCE_TOKEN}${line}${SOURCE_TOKEN}${column}`; +}; + +/** + * Utility method to serialize the source + * @param source + * @returns Object + */ +function deserialize(source) { + let [ url, line, column ] = source.split(SOURCE_TOKEN); + line = parseInt(line); + column = parseInt(column); + if (column === 0) { + return { url, line }; + } + return { url, line, column }; +};
--- a/devtools/client/framework/moz.build +++ b/devtools/client/framework/moz.build @@ -11,21 +11,22 @@ TEST_HARNESS_FILES.xpcshell.devtools.cli DevToolsModules( 'about-devtools-toolbox.js', 'attach-thread.js', 'browser-menus.js', 'devtools-browser.js', 'devtools.js', 'gDevTools.jsm', + 'location-store.js', 'menu-item.js', 'menu.js', 'selection.js', 'sidebar.js', - 'source-location.js', + 'source-map-service.js', 'target-from-url.js', 'target.js', 'toolbox-highlighter-utils.js', 'toolbox-hosts.js', 'toolbox-options.js', 'toolbox.js', 'ToolboxProcess.jsm', )
rename from devtools/client/framework/source-location.js rename to devtools/client/framework/source-map-service.js --- a/devtools/client/framework/source-location.js +++ b/devtools/client/framework/source-map-service.js @@ -1,98 +1,171 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Task } = require("devtools/shared/task"); -const { assert } = require("devtools/shared/DevToolsUtils"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { LocationStore, serialize, deserialize } = require("./location-store"); /** * A manager class that wraps a TabTarget and listens to source changes * from source maps and resolves non-source mapped locations to the source mapped * versions and back and forth, and creating smart elements with a location that * auto-update when the source changes (from pretty printing, source maps loading, etc) * * @param {TabTarget} target */ -function SourceLocationController(target) { - this.target = target; - this.locations = new Set(); + +function SourceMapService(target) { + this._target = target; + this._locationStore = new LocationStore(); + this._isInitialResolve = true; + + EventEmitter.decorate(this); this._onSourceUpdated = this._onSourceUpdated.bind(this); + this._resolveLocation = this._resolveLocation.bind(this); + this._resolveAndUpdate = this._resolveAndUpdate.bind(this); + this.subscribe = this.subscribe.bind(this); + this.unsubscribe = this.unsubscribe.bind(this); this.reset = this.reset.bind(this); this.destroy = this.destroy.bind(this); target.on("source-updated", this._onSourceUpdated); target.on("navigate", this.reset); target.on("will-navigate", this.reset); target.on("close", this.destroy); } -SourceLocationController.prototype.reset = function () { - this.locations.clear(); +/** + * Clears the store containing the cached resolved locations and promises + */ +SourceMapService.prototype.reset = function () { + this._isInitialResolve = true; + this._locationStore.clear(); +}; + +SourceMapService.prototype.destroy = function () { + this.reset(); + this._target.off("source-updated", this._onSourceUpdated); + this._target.off("navigate", this.reset); + this._target.off("will-navigate", this.reset); + this._target.off("close", this.destroy); + this._isInitialResolve = null; + this._target = this._locationStore = null; }; -SourceLocationController.prototype.destroy = function () { - this.locations.clear(); - this.target.off("source-updated", this._onSourceUpdated); - this.target.off("navigate", this.reset); - this.target.off("will-navigate", this.reset); - this.target.off("close", this.destroy); - this.target = this.locations = null; +/** + * Sets up listener for the callback to update the FrameView and tries to resolve location + * @param location + * @param callback + */ +SourceMapService.prototype.subscribe = function (location, callback) { + this.on(serialize(location), callback); + this._locationStore.set(location); + if (this._isInitialResolve) { + this._resolveAndUpdate(location); + this._isInitialResolve = false; + } +}; + +/** + * Removes the listener for the location and clears cached locations + * @param location + * @param callback + */ +SourceMapService.prototype.unsubscribe = function (location, callback) { + this.off(serialize(location), callback); + this._locationStore.clearByURL(location.url); }; /** - * Add this `location` to be observed and register a callback - * whenever the underlying source is updated. - * - * @param {Object} location - * An object with a {String} url, {Number} line, and optionally - * a {Number} column. - * @param {Function} callback + * Tries to resolve the location and if successful, + * emits the resolved location and caches it + * @param location + * @private */ -SourceLocationController.prototype.bindLocation = function (location, callback) { - assert(location.url, "Location must have a url."); - assert(location.line, "Location must have a line."); - this.locations.add({ location, callback }); +SourceMapService.prototype._resolveAndUpdate = function (location) { + this._resolveLocation(location).then(resolvedLocation => { + // We try to source map the first console log to initiate the source-updated event from + // target. The isSameLocation check is to make sure we don't update the frame, if the + // location is not source-mapped. + if (resolvedLocation) { + if (this._isInitialResolve) { + if (!isSameLocation(location, resolvedLocation)) { + this.emit(serialize(location), location, resolvedLocation); + return; + } + } + this.emit(serialize(location), location, resolvedLocation); + } + }); }; /** - * Called when a new source occurs (a normal source, source maps) or an updated - * source (pretty print) occurs. - * - * @param {String} eventName - * @param {Object} sourceEvent + * Validates the location model, + * checks if there is existing promise to resolve location, if so returns cached promise + * if not promised to resolve, + * tries to resolve location and returns a promised location + * @param location + * @return Promise<Object> + * @private */ -SourceLocationController.prototype._onSourceUpdated = function (_, sourceEvent) { +SourceMapService.prototype._resolveLocation = Task.async(function* (location) { + // Location must have a url and a line + if (!location.url || !location.line) { + return null; + } + const cachedLocation = this._locationStore.get(location); + if (cachedLocation) { + return cachedLocation; + } else { + const promisedLocation = resolveLocation(this._target, location); + if (promisedLocation) { + this._locationStore.set(location, promisedLocation); + return promisedLocation; + } + } +}); + +/** + * Checks if the `source-updated` event is fired from the target. + * Checks to see if location store has the source url in its cache, + * if so, tries to update each stale location in the store. + * @param _ + * @param sourceEvent + * @private + */ +SourceMapService.prototype._onSourceUpdated = function (_, sourceEvent) { let { type, source } = sourceEvent; // If we get a new source, and it's not a source map, abort; - // we can ahve no actionable updates as this is just a new normal source. + // we can have no actionable updates as this is just a new normal source. // Also abort if there's no `url`, which means it's unsourcemappable anyway, // like an eval script. if (!source.url || type === "newSource" && !source.isSourceMapped) { return; } - - for (let locationItem of this.locations) { - if (isSourceRelated(locationItem.location, source)) { - this._updateSource(locationItem); + let sourceUrl = null; + if (source.generatedUrl && source.isSourceMapped) { + sourceUrl = source.generatedUrl; + } else if (source.url && source.isPrettyPrinted) { + sourceUrl = source.url; + } + const locationsToResolve = this._locationStore.getByURL(sourceUrl); + if (locationsToResolve.length) { + this._locationStore.clearByURL(sourceUrl); + for (let location of locationsToResolve) { + this._resolveAndUpdate(deserialize(location)); } } }; -SourceLocationController.prototype._updateSource = Task.async(function* (locationItem) { - let newLocation = yield resolveLocation(this.target, locationItem.location); - if (newLocation) { - let previousLocation = Object.assign({}, locationItem.location); - Object.assign(locationItem.location, newLocation); - locationItem.callback(previousLocation, newLocation); - } -}); +exports.SourceMapService = SourceMapService; /** * Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve * the location to the latest location (so a source mapped location, or if pretty print * status has been updated) * * @param {TabTarget} target * @param {Object} location @@ -100,38 +173,28 @@ SourceLocationController.prototype._upda */ function resolveLocation(target, location) { return Task.spawn(function* () { let newLocation = yield target.resolveLocation({ url: location.url, line: location.line, column: location.column || Infinity }); - // Source or mapping not found, so don't do anything if (newLocation.error) { return null; } return newLocation; }); } /** - * Takes a serialized SourceActor form and returns a boolean indicating - * if this source is related to this location, like if a location is a generated source, - * and the source map is loaded subsequently, the new source mapped SourceActor - * will be considered related to this location. Same with pretty printing new sources. - * - * @param {Object} location - * @param {Object} source - * @return {Boolean} + * Returns if the original location and resolved location are the same + * @param location + * @param resolvedLocation + * @returns {boolean} */ -function isSourceRelated(location, source) { - // Mapping location to subsequently loaded source map - return source.generatedUrl === location.url || - // Mapping source map loc to source map - source.url === location.url; -} - -exports.SourceLocationController = SourceLocationController; -exports.resolveLocation = resolveLocation; -exports.isSourceRelated = isSourceRelated; +function isSameLocation(location, resolvedLocation) { + return location.url === resolvedLocation.url && + location.line === resolvedLocation.line && + location.column === resolvedLocation.column; +}; \ No newline at end of file
--- a/devtools/client/framework/test/browser.ini +++ b/devtools/client/framework/test/browser.ini @@ -3,18 +3,22 @@ tags = devtools subsuite = devtools support-files = browser_toolbox_options_disable_js.html browser_toolbox_options_disable_js_iframe.html browser_toolbox_options_disable_cache.sjs browser_toolbox_sidebar_tool.xul browser_toolbox_window_title_changes_page.html browser_toolbox_window_title_frame_select_page.html + code_binary_search.coffee + code_binary_search.js + code_binary_search.map code_math.js code_ugly.js + doc_empty-tab-01.html head.js shared-head.js shared-redux-head.js helper_disable_cache.js doc_theme.css doc_viewsource.html browser_toolbox_options_enable_serviceworkers_testing_frame_script.js browser_toolbox_options_enable_serviceworkers_testing.html @@ -26,18 +30,18 @@ support-files = [browser_devtools_api_destroy.js] [browser_dynamic_tool_enabling.js] [browser_ignore_toolbox_network_requests.js] [browser_keybindings_01.js] [browser_keybindings_02.js] [browser_keybindings_03.js] [browser_menu_api.js] [browser_new_activation_workflow.js] -[browser_source-location-01.js] -[browser_source-location-02.js] +[browser_source_map-01.js] +[browser_source_map-02.js] [browser_target_from_url.js] [browser_target_events.js] [browser_target_remote.js] [browser_target_support.js] [browser_toolbox_custom_host.js] [browser_toolbox_dynamic_registration.js] [browser_toolbox_getpanelwhenready.js] [browser_toolbox_highlight.js]
rename from devtools/client/framework/test/browser_source-location-01.js rename to devtools/client/framework/test/browser_source_map-01.js --- a/devtools/client/framework/test/browser_source-location-01.js +++ b/devtools/client/framework/test/browser_source_map-01.js @@ -1,66 +1,65 @@ /* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ + http://creativecommons.org/publicdomain/zero/1.0/ */ // Whitelisting this test. // As part of bug 1077403, the leaking uncaught rejections should be fixed. thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]"); thisTestLeaksUncaughtRejectionsAndShouldBeFixed( "TypeError: this.transport is null"); /** - * Tests the SourceMapController updates generated sources when source maps + * Tests the SourceMapService updates generated sources when source maps * are subsequently found. Also checks when no column is provided, and * when tagging an already source mapped location initially. */ const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/"; // Empty page const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`; -const JS_URL = `${DEBUGGER_ROOT}code_binary_search.js`; -const COFFEE_URL = `${DEBUGGER_ROOT}code_binary_search.coffee`; -const { SourceLocationController } = require("devtools/client/framework/source-location"); +const JS_URL = `${URL_ROOT}code_binary_search.js`; +const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`; +const { SourceMapService } = require("devtools/client/framework/source-map-service"); add_task(function* () { - let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger"); + const toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger"); - let controller = new SourceLocationController(toolbox.target); + const service = new SourceMapService(toolbox.target); - let aggregator = []; + const aggregator = []; - function onUpdate(oldLoc, newLoc) { + function onUpdate(e, oldLoc, newLoc) { if (oldLoc.line === 6) { checkLoc1(oldLoc, newLoc); } else if (oldLoc.line === 8) { checkLoc2(oldLoc, newLoc); } else if (oldLoc.line === 2) { checkLoc3(oldLoc, newLoc); } else { throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`); } aggregator.push(newLoc); } let loc1 = { url: JS_URL, line: 6 }; let loc2 = { url: JS_URL, line: 8, column: 3 }; - let loc3 = { url: COFFEE_URL, line: 2, column: 0 }; - controller.bindLocation(loc1, onUpdate); - controller.bindLocation(loc2, onUpdate); - controller.bindLocation(loc3, onUpdate); + service.subscribe(loc1, onUpdate); + service.subscribe(loc2, onUpdate); // Inject JS script + let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_binary_search"); yield createScript(JS_URL); + yield sourceShown; - yield waitUntil(() => aggregator.length === 3); + yield waitUntil(() => aggregator.length === 2); ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location"); ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location"); - ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 2), "found third updated location"); yield toolbox.destroy(); gBrowser.removeCurrentTab(); finish(); }); function checkLoc1(oldLoc, newLoc) { is(oldLoc.line, 6, "Correct line for JS:6"); @@ -75,28 +74,37 @@ function checkLoc2(oldLoc, newLoc) { is(oldLoc.line, 8, "Correct line for JS:8:3"); is(oldLoc.column, 3, "Correct column for JS:8:3"); is(oldLoc.url, JS_URL, "Correct url for JS:8:3"); is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE"); is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE"); is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE"); } -function checkLoc3(oldLoc, newLoc) { - is(oldLoc.line, 2, "Correct line for COFFEE:2:0"); - is(oldLoc.column, 0, "Correct column for COFFEE:2:0"); - is(oldLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0"); - is(newLoc.line, 2, "Correct line for COFFEE:2:0 -> COFFEE"); - is(newLoc.column, 0, "Correct column for COFFEE:2:0 -> COFFEE"); - is(newLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0 -> COFFEE"); -} - function createScript(url) { info(`Creating script: ${url}`); let mm = getFrameScript(); let command = ` let script = document.createElement("script"); script.setAttribute("src", "${url}"); document.body.appendChild(script); null; `; return evalInDebuggee(mm, command); } + +function waitForSourceShown(debuggerPanel, url) { + let { panelWin } = debuggerPanel; + let deferred = defer(); + + info(`Waiting for source ${url} to be shown in the debugger...`); + panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) { + + let sourceUrl = source.url || source.generatedUrl; + if (sourceUrl.includes(url)) { + panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown); + info(`Source shown for ${url}`); + deferred.resolve(source); + } + }); + + return deferred.promise; +}
rename from devtools/client/framework/test/browser_source-location-02.js rename to devtools/client/framework/test/browser_source_map-02.js --- a/devtools/client/framework/test/browser_source-location-02.js +++ b/devtools/client/framework/test/browser_source_map-02.js @@ -1,63 +1,63 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ + http://creativecommons.org/publicdomain/zero/1.0/ */ /** - * Tests the SourceLocationController updates generated sources when pretty printing + * Tests the SourceMapService updates generated sources when pretty printing * and un pretty printing. */ const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/"; // Empty page const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`; const JS_URL = `${URL_ROOT}code_ugly.js`; -const { SourceLocationController } = require("devtools/client/framework/source-location"); +const { SourceMapService } = require("devtools/client/framework/source-map-service"); add_task(function* () { let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger"); - let controller = new SourceLocationController(toolbox.target); + let service = new SourceMapService(toolbox.target); let checkedPretty = false; let checkedUnpretty = false; - function onUpdate(oldLoc, newLoc) { + function onUpdate(e, oldLoc, newLoc) { if (oldLoc.line === 3) { checkPrettified(oldLoc, newLoc); checkedPretty = true; } else if (oldLoc.line === 9) { checkUnprettified(oldLoc, newLoc); checkedUnpretty = true; } else { throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`); } } - - controller.bindLocation({ url: JS_URL, line: 3 }, onUpdate); + const loc1 = { url: JS_URL, line: 3 }; + service.subscribe(loc1, onUpdate); // Inject JS script let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js"); yield createScript(JS_URL); yield sourceShown; let ppButton = toolbox.getCurrentPanel().panelWin.document.getElementById("pretty-print"); sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js"); ppButton.click(); yield sourceShown; yield waitUntil(() => checkedPretty); // TODO check unprettified change once bug 1177446 fixed - /* - sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js"); - ppButton.click(); - yield sourceShown; - yield waitUntil(() => checkedUnpretty); - */ + // info("Testing un-pretty printing."); + // sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js"); + // ppButton.click(); + // yield sourceShown; + // yield waitUntil(() => checkedUnpretty); + yield toolbox.destroy(); gBrowser.removeCurrentTab(); finish(); }); function checkPrettified(oldLoc, newLoc) { is(oldLoc.line, 3, "Correct line for JS:3");
copy from devtools/client/debugger/test/mochitest/code_binary_search.coffee copy to devtools/client/framework/test/code_binary_search.coffee
copy from devtools/client/debugger/test/mochitest/code_binary_search.js copy to devtools/client/framework/test/code_binary_search.js
copy from devtools/client/debugger/test/mochitest/code_binary_search.map copy to devtools/client/framework/test/code_binary_search.map
copy from devtools/client/debugger/test/mochitest/doc_empty-tab-01.html copy to devtools/client/framework/test/doc_empty-tab-01.html
--- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -6,16 +6,17 @@ const MAX_ORDINAL = 99; const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled"; const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight"; const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER"; const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER"; const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER"; const HTML_NS = "http://www.w3.org/1999/xhtml"; +const { SourceMapService } = require("./source-map-service"); var {Cc, Ci, Cu} = require("chrome"); var promise = require("promise"); var defer = require("devtools/shared/defer"); var Services = require("Services"); var {Task} = require("devtools/shared/task"); var {gDevTools} = require("devtools/client/framework/devtools"); var EventEmitter = require("devtools/shared/event-emitter"); @@ -113,16 +114,19 @@ const ToolboxButtons = exports.ToolboxBu * Type of host that will host the toolbox (e.g. sidebar, window) * @param {object} hostOptions * Options for host specifically */ function Toolbox(target, selectedTool, hostType, hostOptions) { this._target = target; this._toolPanels = new Map(); this._telemetry = new Telemetry(); + if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) { + this._sourceMapService = new SourceMapService(this._target); + } this._initInspector = null; this._inspector = null; // Map of frames (id => frame-info) and currently selected frame id. this.frameMap = new Map(); this.selectedFrameId = null; @@ -2026,16 +2030,20 @@ Toolbox.prototype = { this.off("ready", this._showDevEditionPromo); gDevTools.off("tool-registered", this._toolRegistered); gDevTools.off("tool-unregistered", this._toolUnregistered); gDevTools.off("pref-changed", this._prefChanged); this._lastFocusedElement = null; + if (this._sourceMapService) { + this._sourceMapService.destroy(); + this._sourceMapService = null; + } if (this.webconsolePanel) { this._saveSplitConsoleHeight(); this.webconsolePanel.removeEventListener("resize", this._saveSplitConsoleHeight); } this.closeButton.removeEventListener("click", this.destroy, true); this.textboxContextMenuPopup.removeEventListener("popupshowing",
--- a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js @@ -22,29 +22,29 @@ add_task(function* () { markup.isDragging = true; info("Simulate a mousemove on the view, at the bottom, and expect scrolling"); let onScrolled = waitForScrollStop(markup.doc); markup._onMouseMove({ preventDefault: () => {}, target: markup.doc.body, - pageY: viewHeight + pageY: viewHeight + markup.doc.defaultView.scrollY }); let bottomScrollPos = yield onScrolled; ok(bottomScrollPos > 0, "The view was scrolled down"); info("Simulate a mousemove at the top and expect more scrolling"); onScrolled = waitForScrollStop(markup.doc); markup._onMouseMove({ preventDefault: () => {}, target: markup.doc.body, - pageY: 0 + pageY: markup.doc.defaultView.scrollY }); let topScrollPos = yield onScrolled; ok(topScrollPos < bottomScrollPos, "The view was scrolled up"); is(topScrollPos, 0, "The view was scrolled up to the top"); info("Simulate a mouseup to stop dragging"); markup._onMouseUp();
--- a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js @@ -21,29 +21,28 @@ add_task(function* () { markup.isDragging = true; info("Simulate a mousemove on the view, at the bottom, and expect scrolling"); let onScrolled = waitForScrollStop(markup.doc); markup._onMouseMove({ preventDefault: () => {}, target: markup.doc.body, - pageY: viewHeight + pageY: viewHeight + markup.doc.defaultView.scrollY }); let bottomScrollPos = yield onScrolled; ok(bottomScrollPos > 0, "The view was scrolled down"); - info("Simulate a mousemove at the top and expect more scrolling"); onScrolled = waitForScrollStop(markup.doc); markup._onMouseMove({ preventDefault: () => {}, target: markup.doc.body, - pageY: 0 + pageY: markup.doc.defaultView.scrollY }); let topScrollPos = yield onScrolled; ok(topScrollPos < bottomScrollPos, "The view was scrolled up"); is(topScrollPos, 0, "The view was scrolled up to the top"); info("Simulate a mouseup to stop dragging"); markup._onMouseUp();
--- a/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html +++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html @@ -31,33 +31,10 @@ https://bugzilla.mozilla.org/show_bug.cg <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> - <div></div> </body> </html>
--- a/devtools/client/jsonview/components/text-panel.js +++ b/devtools/client/jsonview/components/text-panel.js @@ -66,29 +66,29 @@ define(function (require, exports, modul onCopy: function (event) { this.props.actions.onCopyJson(); }, render: function () { return ( Toolbar({}, ToolbarButton({ - className: "btn prettyprint", - onClick: this.onPrettify}, - Locale.$STR("jsonViewer.PrettyPrint") - ), - ToolbarButton({ className: "btn save", onClick: this.onSave}, Locale.$STR("jsonViewer.Save") ), ToolbarButton({ className: "btn copy", onClick: this.onCopy}, Locale.$STR("jsonViewer.Copy") + ), + ToolbarButton({ + className: "btn prettyprint", + onClick: this.onPrettify}, + Locale.$STR("jsonViewer.PrettyPrint") ) ) ); }, })); // Exports from this module exports.TextPanel = TextPanel;
--- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -291,16 +291,19 @@ pref("devtools.webconsole.timestampMessa // Web Console automatic multiline mode: |true| if you want incomplete statements // to automatically trigger multiline editing (equivalent to shift + enter). pref("devtools.webconsole.autoMultiline", true); // Enable the experimental webconsole frontend (work in progress) pref("devtools.webconsole.new-frontend-enabled", false); +// Enable the experimental support for source maps in console (work in progress) +pref("devtools.sourcemap.locations.enabled", false); + // The number of lines that are displayed in the web console. pref("devtools.hud.loglimit", 1000); // The number of lines that are displayed in the web console for the Net, // CSS, JS and Web Developer categories. These defaults should be kept in sync // with DEFAULT_LOG_LIMIT in the webconsole frontend. pref("devtools.hud.loglimit.network", 1000); pref("devtools.hud.loglimit.cssparser", 1000);
--- a/devtools/client/shared/components/frame.js +++ b/devtools/client/shared/components/frame.js @@ -29,59 +29,136 @@ module.exports = createClass({ // Option to display a function name even if it's anonymous. showAnonymousFunctionName: PropTypes.bool, // Option to display a host name after the source link. showHost: PropTypes.bool, // Option to display a host name if the filename is empty or just '/' showEmptyPathAsHost: PropTypes.bool, // Option to display a full source instead of just the filename. showFullSourceUrl: PropTypes.bool, + // Service to enable the source map feature for console. + sourceMapService: PropTypes.object, }, getDefaultProps() { return { showFunctionName: false, showAnonymousFunctionName: false, showHost: false, showEmptyPathAsHost: false, showFullSourceUrl: false, }; }, + componentWillMount() { + const sourceMapService = this.props.sourceMapService; + if (sourceMapService) { + const source = this.getSource(); + sourceMapService.subscribe(source, this.onSourceUpdated); + } + }, + + componentWillUnmount() { + const sourceMapService = this.props.sourceMapService; + if (sourceMapService) { + const source = this.getSource(); + sourceMapService.unsubscribe(source, this.onSourceUpdated); + } + }, + + /** + * Component method to update the FrameView when a resolved location is available + * @param event + * @param location + */ + onSourceUpdated(event, location, resolvedLocation) { + const frame = this.getFrame(resolvedLocation); + this.setState({ + frame, + isSourceMapped: true, + }); + }, + + /** + * Utility method to convert the Frame object to the + * Source Object model required by SourceMapService + * @param frame + * @returns {{url: *, line: *, column: *}} + */ + getSource(frame) { + frame = frame || this.props.frame; + const { source, line, column } = frame; + return { + url: source, + line, + column, + }; + }, + + /** + * Utility method to convert the Source object model to the + * Frame object model required by FrameView class. + * @param source + * @returns {{source: *, line: *, column: *, functionDisplayName: *}} + */ + getFrame(source) { + const { url, line, column } = source; + return { + source: url, + line, + column, + functionDisplayName: this.props.frame.functionDisplayName, + }; + }, + render() { + let frame, isSourceMapped; let { onClick, - frame, showFunctionName, showAnonymousFunctionName, showHost, showEmptyPathAsHost, showFullSourceUrl } = this.props; + if (this.state && this.state.isSourceMapped) { + frame = this.state.frame; + isSourceMapped = this.state.isSourceMapped; + } else { + frame = this.props.frame; + } + let source = frame.source ? String(frame.source) : ""; let line = frame.line != void 0 ? Number(frame.line) : null; let column = frame.column != void 0 ? Number(frame.column) : null; const { short, long, host } = getSourceNames(source); // Reparse the URL to determine if we should link this; `getSourceNames` // has already cached this indirectly. We don't want to attempt to // link to "self-hosted" and "(unknown)". However, we do want to link // to Scratchpad URIs. - const isLinkable = !!(isScratchpadScheme(source) || parseURL(source)); + // Source mapped sources might not necessary linkable, but they + // are still valid in the debugger. + const isLinkable = !!(isScratchpadScheme(source) || parseURL(source)) + || isSourceMapped; const elements = []; const sourceElements = []; let sourceEl; let tooltip = long; + + // If the source is linkable and line > 0 + const shouldDisplayLine = isLinkable && line; + // Exclude all falsy values, including `0`, as even // a number 0 for line doesn't make sense, and should not be displayed. // If source isn't linkable, don't attempt to append line and column // info, as this probably doesn't make sense. - if (isLinkable && line) { + if (shouldDisplayLine) { tooltip += `:${line}`; // Intentionally exclude 0 if (column) { tooltip += `:${column}`; } } let attributes = { @@ -99,26 +176,35 @@ module.exports = createClass({ elements.push( dom.span({ className: "frame-link-function-display-name" }, functionDisplayName) ); } } let displaySource = showFullSourceUrl ? long : short; - if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) { + // SourceMapped locations might not be parsed properly by parseURL. + // Eg: sourcemapped location could be /folder/file.coffee instead of a url + // and so the url parser would not parse non-url locations properly + // Check for "/" in displaySource. If "/" is in displaySource, + // take everything after last "/". + if (isSourceMapped) { + displaySource = displaySource.lastIndexOf("/") < 0 ? + displaySource : + displaySource.slice(displaySource.lastIndexOf("/") + 1); + } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) { displaySource = host; } sourceElements.push(dom.span({ className: "frame-link-filename", }, displaySource)); // If source is linkable, and we have a line number > 0 - if (isLinkable && line) { + if (shouldDisplayLine) { let lineInfo = `:${line}`; // Add `data-line` attribute for testing attributes["data-line"] = line; // Intentionally exclude 0 if (column) { lineInfo += `:${column}`; // Add `data-column` attribute for testing @@ -129,17 +215,17 @@ module.exports = createClass({ } // If source is not a URL (self-hosted, eval, etc.), don't make // it an anchor link, as we can't link to it. if (isLinkable) { sourceEl = dom.a({ onClick: e => { e.preventDefault(); - onClick(frame); + onClick(this.getSource(frame)); }, href: source, className: "frame-link-source", draggable: false, title: l10n.getFormatStr("frame.viewsourceindebugger", tooltip) }, sourceElements); } else { sourceEl = dom.span({
--- a/devtools/client/shared/components/reps/array.js +++ b/devtools/client/shared/components/reps/array.js @@ -122,17 +122,18 @@ define(function (require, exports, modul }, render: function () { let mode = this.props.mode || "short"; let object = this.props.object; let items; if (mode == "tiny") { - items = DOM.span({className: "length"}, object.length); + let isEmpty = object.length === 0; + items = DOM.span({className: "length"}, isEmpty ? "" : object.length); } else { let max = (mode == "short") ? 3 : 300; items = this.arrayIterator(object, max); } let objectLink = this.props.objectLink || DOM.span; return (
--- a/devtools/client/shared/components/reps/grip-array.js +++ b/devtools/client/shared/components/reps/grip-array.js @@ -103,17 +103,19 @@ define(function (require, exports, modul render: function () { let mode = this.props.mode || "short"; let object = this.props.object; let items; if (mode == "tiny") { - items = span({className: "length"}, this.getLength(object)); + let objectLength = this.getLength(object); + let isEmpty = objectLength === 0; + items = span({className: "length"}, isEmpty ? "" : objectLength); } else { let max = (mode == "short") ? 3 : 300; items = this.arrayIterator(object, max); } let objectLink = this.props.objectLink || span; return (
--- a/devtools/client/shared/components/test/mochitest/test_reps_array.html +++ b/devtools/client/shared/components/test/mochitest/test_reps_array.html @@ -52,17 +52,17 @@ window.onload = Task.async(function* () const modeTests = [ { mode: undefined, expectedOutput: defaultOutput, }, { mode: "tiny", - expectedOutput: `[0]`, + expectedOutput: `[]`, }, { mode: "short", expectedOutput: defaultOutput, }, { mode: "long", expectedOutput: defaultOutput,
--- a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html +++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html @@ -52,17 +52,17 @@ window.onload = Task.async(function* () const modeTests = [ { mode: undefined, expectedOutput: defaultOutput, }, { mode: "tiny", - expectedOutput: `[0]`, + expectedOutput: `[]`, }, { mode: "short", expectedOutput: defaultOutput, }, { mode: "long", expectedOutput: defaultOutput,
--- a/devtools/client/shared/components/tree/tree-view.css +++ b/devtools/client/shared/components/tree/tree-view.css @@ -18,16 +18,21 @@ /* TreeView Table*/ .treeTable .treeLabelCell { padding: 2px 0 2px 0px; vertical-align: top; white-space: nowrap; } +.treeTable .treeLabelCell::after { + content: ":"; + color: var(--object-color); +} + .treeTable .treeValueCell { padding: 2px 0 2px 5px; overflow: hidden; } .treeTable .treeLabel { cursor: default; overflow: hidden;
--- a/devtools/client/shared/widgets/TableWidget.js +++ b/devtools/client/shared/widgets/TableWidget.js @@ -47,35 +47,38 @@ const MAX_VISIBLE_STRING_SIZE = 100; * * @param {nsIDOMNode} node * The container element for the table widget. * @param {object} options * - initialColumns: map of key vs display name for initial columns of * the table. See @setupColumns for more info. * - uniqueId: the column which will be the unique identifier of each * entry in the table. Default: name. + * - wrapTextInElements: Don't ever use 'value' attribute on labels. + * Default: false. * - emptyText: text to display when no entries in the table to display. * - highlightUpdated: true to highlight the changed/added row. * - removableColumns: Whether columns are removeable. If set to false, * the context menu in the headers will not appear. * - firstColumn: key of the first column that should appear. * - cellContextMenuId: ID of a <menupopup> element to be set as a * context menu of every cell. */ function TableWidget(node, options = {}) { EventEmitter.decorate(this); this.document = node.ownerDocument; this.window = this.document.defaultView; this._parent = node; let {initialColumns, emptyText, uniqueId, highlightUpdated, removableColumns, - firstColumn, cellContextMenuId} = options; + firstColumn, wrapTextInElements, cellContextMenuId} = options; this.emptyText = emptyText || ""; this.uniqueId = uniqueId || "name"; + this.wrapTextInElements = wrapTextInElements || false; this.firstColumn = firstColumn || ""; this.highlightUpdated = highlightUpdated || false; this.removableColumns = removableColumns !== false; this.cellContextMenuId = cellContextMenuId; this.tbody = this.document.createElementNS(XUL_NS, "hbox"); this.tbody.className = "table-widget-body theme-body"; this.tbody.setAttribute("flex", "1"); @@ -959,16 +962,17 @@ module.exports.TableWidget = TableWidget * The displayed string on the column's header. */ function Column(table, id, header) { this.tbody = table.tbody; this.document = table.document; this.window = table.window; this.id = id; this.uniqueId = table.uniqueId; + this.wrapTextInElements = table.wrapTextInElements; this.table = table; this.cells = []; this.items = {}; this.highlightUpdated = table.highlightUpdated; // This wrapping element is required solely so that position:sticky works on // the headers of the columns. @@ -1441,16 +1445,17 @@ Column.prototype = { * can be a DOMNode that is appended or a string value. * @param {Cell} nextCell * The cell object which is next to this cell. null if this cell is last * cell of the column */ function Cell(column, item, nextCell) { let document = column.document; + this.wrapTextInElements = column.wrapTextInElements; this.label = document.createElementNS(XUL_NS, "label"); this.label.setAttribute("crop", "end"); this.label.className = "plain table-widget-cell"; if (nextCell) { column.column.insertBefore(this.label, nextCell.label); } else { column.column.appendChild(this.label); @@ -1494,16 +1499,22 @@ Cell.prototype = { set value(value) { this._value = value; if (value == null) { this.label.setAttribute("value", ""); return; } + if (this.wrapTextInElements && !(value instanceof Ci.nsIDOMNode)) { + let span = this.label.ownerDocument.createElementNS(HTML_NS, "span"); + span.textContent = value; + value = span; + } + if (!(value instanceof Ci.nsIDOMNode) && value.length > MAX_VISIBLE_STRING_SIZE) { value = value .substr(0, MAX_VISIBLE_STRING_SIZE) + "\u2026"; } if (value instanceof Ci.nsIDOMNode) { this.label.removeAttribute("value");
--- a/devtools/client/themes/webconsole.css +++ b/devtools/client/themes/webconsole.css @@ -493,16 +493,26 @@ a { border: 1px solid var(--theme-splitter-color); border-radius: 3px; } .consoletable { margin: 5px 0 0 0; } +/* Force cells to only show one row of contents. Getting normal ellipses + behavior has proven impossible so far, so this is better than letting + rows get out of vertical alignment when one cell has a lot of content. */ +.consoletable .table-widget-cell > span { + overflow: hidden; + display: flex; + height: 1.25em; + line-height: 1.25em; +} + .theme-light .message[severity=error] .stacktrace { background-color: rgba(255, 255, 255, 0.5); } .theme-dark .message[severity=error] .stacktrace { background-color: rgba(0, 0, 0, 0.5); }
--- a/devtools/client/webconsole/console-output.js +++ b/devtools/client/webconsole/console-output.js @@ -3615,16 +3615,17 @@ Widgets.Table.prototype = extend(Widgets if (this.element) { return this; } let result = this.element = this.document.createElementNS(XHTML_NS, "div"); result.className = "consoletable devtools-monospace"; this.table = new TableWidget(result, { + wrapTextInElements: true, initialColumns: this.columns, uniqueId: "_index", firstColumn: "_index" }); for (let row of this.data) { this.table.push(row); }
--- a/devtools/client/webconsole/webconsole.js +++ b/devtools/client/webconsole/webconsole.js @@ -2526,55 +2526,62 @@ WebConsoleFrame.prototype = { locationNode.className = "message-location devtools-monospace"; if (!url) { url = ""; } let fullURL = url.split(" -> ").pop(); // Make the location clickable. - let onClick = () => { + let onClick = ({ url, line }) => { let category = locationNode.closest(".message").category; let target = null; if (/^Scratchpad\/\d+$/.test(url)) { target = "scratchpad"; } else if (category === CATEGORY_CSS) { target = "styleeditor"; } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) { target = "jsdebugger"; - } else if (/\.js$/.test(fullURL)) { + } else if (/\.js$/.test(url)) { // If it ends in .js, let's attempt to open in debugger // anyway, as this falls back to normal view-source. target = "jsdebugger"; + } else { + // Point everything else to debugger, if source not available, + // it will fall back to view-source. + target = "jsdebugger"; } switch (target) { case "scratchpad": this.owner.viewSourceInScratchpad(url, line); return; case "jsdebugger": - this.owner.viewSourceInDebugger(fullURL, line); + this.owner.viewSourceInDebugger(url, line); return; case "styleeditor": - this.owner.viewSourceInStyleEditor(fullURL, line); + this.owner.viewSourceInStyleEditor(url, line); return; } // No matching tool found; use old school view-source - this.owner.viewSource(fullURL, line); + this.owner.viewSource(url, line); }; + const toolbox = gDevTools.getToolbox(this.owner.target); + this.ReactDOM.render(this.FrameView({ frame: { source: fullURL, line, column }, showEmptyPathAsHost: true, onClick, + sourceMapService: toolbox ? toolbox._sourceMapService : null, }), locationNode); return locationNode; }, /** * Adjusts the category and severity of the given message. *
--- a/devtools/server/actors/eventlooplag.js +++ b/devtools/server/actors/eventlooplag.js @@ -8,74 +8,53 @@ * The eventLoopLag actor emits "event-loop-lag" events when the event * loop gets unresponsive. The event comes with a "time" property (the * duration of the lag in milliseconds). */ const {Ci} = require("chrome"); const Services = require("Services"); const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); -const protocol = require("devtools/shared/protocol"); -const {method, Arg, RetVal} = protocol; +const {Actor, ActorClassWithSpec} = require("devtools/shared/protocol"); const events = require("sdk/event/core"); - -var EventLoopLagActor = exports.EventLoopLagActor = protocol.ActorClass({ - - typeName: "eventLoopLag", +const {eventLoopLagSpec} = require("devtools/shared/specs/eventlooplag"); +var EventLoopLagActor = exports.EventLoopLagActor = ActorClassWithSpec(eventLoopLagSpec, { _observerAdded: false, - events: { - "event-loop-lag" : { - type: "event-loop-lag", - time: Arg(0, "number") // duration of the lag in milliseconds. - } - }, - /** * Start tracking the event loop lags. */ - start: method(function () { + start: function () { if (!this._observerAdded) { Services.obs.addObserver(this, "event-loop-lag", false); this._observerAdded = true; } return Services.appShell.startEventLoopLagTracking(); - }, { - request: {}, - response: {success: RetVal("number")} - }), + }, /** * Stop tracking the event loop lags. */ - stop: method(function () { + stop: function () { if (this._observerAdded) { Services.obs.removeObserver(this, "event-loop-lag"); this._observerAdded = false; } Services.appShell.stopEventLoopLagTracking(); - }, {request: {}, response: {}}), + }, destroy: function () { this.stop(); - protocol.Actor.prototype.destroy.call(this); + Actor.prototype.destroy.call(this); }, // nsIObserver observe: function (subject, topic, data) { if (topic == "event-loop-lag") { // Forward event loop lag event events.emit(this, "event-loop-lag", data); } }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), }); - -exports.EventLoopLagFront = protocol.FrontClass(EventLoopLagActor, { - initialize: function (client, form) { - protocol.Front.prototype.initialize.call(this, client); - this.actorID = form.eventLoopLagActor; - this.manage(this); - }, -});
--- a/devtools/server/actors/script.js +++ b/devtools/server/actors/script.js @@ -16,17 +16,16 @@ const { ObjectActor, createValueGrip, lo const { SourceActor, getSourceURL } = require("devtools/server/actors/source"); const { DebuggerServer } = require("devtools/server/main"); const { ActorClassWithSpec } = require("devtools/shared/protocol"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { assert, dumpn, update, fetch } = DevToolsUtils; const promise = require("promise"); const PromiseDebugging = require("PromiseDebugging"); const xpcInspector = require("xpcInspector"); -const ScriptStore = require("./utils/ScriptStore"); const { DevToolsWorker } = require("devtools/shared/worker/worker"); const object = require("sdk/util/object"); const { threadSpec } = require("devtools/shared/specs/script"); const { defer, resolve, reject, all } = promise; loader.lazyGetter(this, "Debugger", () => { let Debugger = require("Debugger"); @@ -489,24 +488,16 @@ const ThreadActor = ActorClassWithSpec(t if (!this._threadLifetimePool) { this._threadLifetimePool = new ActorPool(this.conn); this.conn.addActorPool(this._threadLifetimePool); this._threadLifetimePool.objectActors = new WeakMap(); } return this._threadLifetimePool; }, - get scripts() { - if (!this._scripts) { - this._scripts = new ScriptStore(); - this._scripts.addScripts(this.dbg.findScripts()); - } - return this._scripts; - }, - get sources() { return this._parent.sources; }, get youngestFrame() { if (this.state != "paused") { return null; } @@ -640,18 +631,16 @@ const ThreadActor = ActorClassWithSpec(t try { // Put ourselves in the paused state. let packet = this._paused(); if (!packet) { return { error: "notAttached" }; } packet.why = { type: "attached" }; - this._restoreBreakpoints(); - // Send the response to the attach request now (rather than // returning it), because we're going to start a nested event loop // here. this.conn.send(packet); // Start a nested event loop. this._pushThreadPause(); @@ -1179,17 +1168,18 @@ const ThreadActor = ActorClassWithSpec(t _breakOnEnter: function (script) { let offsets = script.getAllOffsets(); for (let line = 0, n = offsets.length; line < n; line++) { if (offsets[line]) { // N.B. Hidden breakpoints do not have an original location, and are not // stored in the breakpoint actor map. let actor = new BreakpointActor(this); this.threadLifetimePool.addActor(actor); - let scripts = this.scripts.getScriptsBySourceAndLine(script.source, line); + + let scripts = this.dbg.findScripts({ source: script.source, line: line }); let entryPoints = findEntryPointsForLine(scripts, line); setBreakpointAtEntryPoints(actor, entryPoints); this._hiddenBreakpoints.set(actor.actorID, actor); break; } } }, @@ -1313,17 +1303,18 @@ const ThreadActor = ActorClassWithSpec(t }, /** * Get the script and source lists from the debugger. */ _discoverSources: function () { // Only get one script per Debugger.Source. const sourcesToScripts = new Map(); - const scripts = this.scripts.getAllScripts(); + const scripts = this.dbg.findScripts(); + for (let i = 0, len = scripts.length; i < len; i++) { let s = scripts[i]; if (s.source) { sourcesToScripts.set(s.source, s); } } return all([...sourcesToScripts.values()].map(script => { @@ -1919,46 +1910,27 @@ const ThreadActor = ActorClassWithSpec(t from: this.actorID, type: name, source: source.form() }); } }, /** - * Restore any pre-existing breakpoints to the sources that we have access to. - */ - _restoreBreakpoints: function () { - if (this.breakpointActorMap.size === 0) { - return; - } - - for (let s of this.scripts.getSources()) { - this._addSource(s); - } - }, - - /** * Add the provided source to the server cache. * * @param aSource Debugger.Source * The source that will be stored. * @returns true, if the source was added; false otherwise. */ _addSource: function (aSource) { if (!this.sources.allowSource(aSource) || this._debuggerSourcesSeen.has(aSource)) { return false; } - // The scripts must be added to the ScriptStore before restoring - // breakpoints. If we try to add them to the ScriptStore any later, we can - // accidentally set a breakpoint in a top level script as a "closest match" - // because we wouldn't have added the child scripts to the ScriptStore yet. - this.scripts.addScripts(this.dbg.findScripts({ source: aSource })); - let sourceActor = this.sources.createNonSourceMappedActor(aSource); let bpActors = [...this.breakpointActorMap.findActors()]; if (this._options.useSourceMaps) { let promises = []; // Go ahead and establish the source actors for this script, which // fetches sourcemaps if available and sends onNewSource
--- a/devtools/server/actors/source.js +++ b/devtools/server/actors/source.js @@ -180,17 +180,16 @@ let SourceActor = ActorClassWithSpec(sou get isInlineSource() { return this._isInlineSource; }, get threadActor() { return this._threadActor; }, get sources() { return this._threadActor.sources; }, get dbg() { return this.threadActor.dbg; }, - get scripts() { return this.threadActor.scripts; }, get source() { return this._source; }, get generatedSource() { return this._generatedSource; }, get breakpointActorMap() { return this.threadActor.breakpointActorMap; }, get url() { if (this.source) { return getSourceURL(this.source, this.threadActor._parent.window); } return this._originalUrl; @@ -439,17 +438,17 @@ let SourceActor = ActorClassWithSpec(sou /** * Extract all executable offsets from the given script * @param String url - extract offsets of the script with this url * @param Boolean onlyLine - will return only the line number * @return Set - Executable offsets/lines of the script **/ getExecutableOffsets: function (source, onlyLine) { let offsets = new Set(); - for (let s of this.threadActor.scripts.getScriptsBySource(source)) { + for (let s of this.dbg.findScripts({ source })) { for (let offset of s.getAllColumnOffsets()) { offsets.add(onlyLine ? offset.lineNumber : offset); } } return offsets; }, @@ -713,20 +712,27 @@ let SourceActor = ActorClassWithSpec(sou const { originalLocation } = actor; const { originalLine, originalSourceActor } = originalLocation; if (!this.isSourceMapped) { if (!this._setBreakpointAtGeneratedLocation( actor, GeneratedLocation.fromOriginalLocation(originalLocation) )) { - const scripts = this.scripts.getScriptsBySourceActorAndLine( - this, - originalLine - ); + const query = { line: originalLine }; + // For most cases, we have a real source to query for. The + // only time we don't is for HTML pages. In that case we want + // to query for scripts in an HTML page based on its URL, as + // there could be several sources within an HTML page. + if (this.source) { + query.source = this.source; + } else { + query.url = this.url; + } + const scripts = this.dbg.findScripts(query); // Never do breakpoint sliding for column breakpoints. // Additionally, never do breakpoint sliding if no scripts // exist on this line. // // Sliding can go horribly wrong if we always try to find the // next line with valid entry points in the entire file. // Scripts may be completely GCed and we never knew they @@ -831,21 +837,25 @@ let SourceActor = ActorClassWithSpec(sou _setBreakpointAtGeneratedLocation: function (actor, generatedLocation) { let { generatedSourceActor, generatedLine, generatedColumn, generatedLastColumn } = generatedLocation; - // Find all scripts that match the given source actor and line number. - let scripts = this.scripts.getScriptsBySourceActorAndLine( - generatedSourceActor, - generatedLine - ); + // Find all scripts that match the given source actor and line + // number. + const query = { line: generatedLine }; + if (generatedSourceActor.source) { + query.source = generatedSourceActor.source; + } else { + query.url = generatedSourceActor.url; + } + let scripts = this.dbg.findScripts(query); scripts = scripts.filter((script) => !actor.hasScript(script)); // Find all entry points that correspond to the given location. let entryPoints = []; if (generatedColumn === undefined) { // This is a line breakpoint, so we are interested in all offsets // that correspond to the given line number.
--- a/devtools/server/actors/stylesheets.js +++ b/devtools/server/actors/stylesheets.js @@ -455,17 +455,20 @@ var StyleSheetActor = protocol.ActorClas charset: this._getCSSCharset() }; // Bug 1282660 - We use the system principal to load the default internal // stylesheets instead of the content principal since such stylesheets // require system principal to load. At meanwhile, we strip the loadGroup // for preventing the assertion of the userContextId mismatching. // The default internal stylesheets load from the 'resource:' URL. - if (!/^resource:\/\//.test(this.href)) { + // Bug 1287607 - The 'chrome:' URL will be also loaded from here, so we do + // the same thing for such URLs as well. + if (!/^resource:\/\//.test(this.href) && + !/^chrome:\/\//.test(this.href)) { options.window = this.window; options.principal = this.document.nodePrincipal; } return fetch(this.href, options).then(({ content }) => { this.text = content; return content; });
deleted file mode 100644 --- a/devtools/server/actors/utils/ScriptStore.js +++ /dev/null @@ -1,219 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ -/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const { noop } = require("devtools/shared/DevToolsUtils"); - -/** - * A `ScriptStore` is a cache of `Debugger.Script` instances. It holds strong - * references to the cached scripts to alleviate the GC-sensitivity issues that - * plague `Debugger.prototype.findScripts`, but this means that its lifetime - * must be managed carefully. It is the `ScriptStore` user's responsibility to - * ensure that the `ScriptStore` stays up to date. - * - * Implementation Notes: - * - * The ScriptStore's prototype methods are very hot, in general. To help the - * JIT, they avoid ES6-isms and higher-order iteration functions, for the most - * part. You might be wondering why we don't maintain indices on, say, - * Debugger.Source for faster querying, if these methods are so hot. First, the - * hottest method is actually just getting all scripts; second, populating the - * store becomes prohibitively expensive. So we fall back to linear queries - * (which isn't so bad, because Debugger.prototype.findScripts is also linear). - */ -function ScriptStore() { - // Set of every Debugger.Script in the cache. - this._scripts = new NoDeleteSet; -} - -module.exports = ScriptStore; - -ScriptStore.prototype = { - // Populating a ScriptStore. - - /** - * Add one script to the cache. - * - * @param Debugger.Script script - */ - addScript(script) { - this._scripts.add(script); - }, - - /** - * Add many scripts to the cache at once. - * - * @param Array scripts - * The set of Debugger.Scripts to add to the cache. - */ - addScripts(scripts) { - for (var i = 0, len = scripts.length; i < len; i++) { - this.addScript(scripts[i]); - } - }, - - // Querying a ScriptStore. - - /** - * Get all the sources for which we have scripts cached. - * - * @returns Array of Debugger.Source - */ - getSources() { - return [...new Set(this._scripts.items.map(s => s.source))]; - }, - - /** - * Get all the scripts in the cache. - * - * @returns read-only Array of Debugger.Script. - * - * NB: The ScriptStore retains ownership of the returned array, and the - * ScriptStore's consumers MUST NOT MODIFY its contents! - */ - getAllScripts() { - return this._scripts.items; - }, - - getScriptsBySourceActor(sourceActor) { - return sourceActor.source ? - this.getScriptsBySource(sourceActor.source) : - this.getScriptsByURL(sourceActor._originalUrl); - }, - - getScriptsBySourceActorAndLine(sourceActor, line) { - return sourceActor.source ? - this.getScriptsBySourceAndLine(sourceActor.source, line) : - this.getScriptsByURLAndLine(sourceActor._originalUrl, line); - }, - - /** - * Get all scripts produced from the given source. - * - * @oaram Debugger.Source source - * @returns Array of Debugger.Script - */ - getScriptsBySource(source) { - var results = []; - var scripts = this._scripts.items; - var length = scripts.length; - for (var i = 0; i < length; i++) { - if (scripts[i].source === source) { - results.push(scripts[i]); - } - } - return results; - }, - - /** - * Get all scripts produced from the given source whose source code definition - * spans the given line. - * - * @oaram Debugger.Source source - * @param Number line - * @returns Array of Debugger.Script - */ - getScriptsBySourceAndLine(source, line) { - var results = []; - var scripts = this._scripts.items; - var length = scripts.length; - for (var i = 0; i < length; i++) { - var script = scripts[i]; - if (script.source === source && - script.startLine <= line && - (script.startLine + script.lineCount) > line) { - results.push(script); - } - } - return results; - }, - - /** - * Get all scripts defined by a source at the given URL. - * - * @param String url - * @returns Array of Debugger.Script - */ - getScriptsByURL(url) { - var results = []; - var scripts = this._scripts.items; - var length = scripts.length; - for (var i = 0; i < length; i++) { - if (scripts[i].url === url) { - results.push(scripts[i]); - } - } - return results; - }, - - /** - * Get all scripts defined by a source a the given URL and whose source code - * definition spans the given line. - * - * @param String url - * @param Number line - * @returns Array of Debugger.Script - */ - getScriptsByURLAndLine(url, line) { - var results = []; - var scripts = this._scripts.items; - var length = scripts.length; - for (var i = 0; i < length; i++) { - var script = scripts[i]; - if (script.url === url && - script.startLine <= line && - (script.startLine + script.lineCount) > line) { - results.push(script); - } - } - return results; - }, -}; - - -/** - * A set which can only grow, and does not support the delete operation. - * Provides faster iteration than the native Set by maintaining an array of all - * items, in addition to the internal set of all items, which allows direct - * iteration (without the iteration protocol and calling into C++, which are - * both expensive). - */ -function NoDeleteSet() { - this._set = new Set(); - this.items = []; -} - -NoDeleteSet.prototype = { - /** - * An array containing every item in the set for convenience and faster - * iteration. This is public for reading only, and consumers MUST NOT modify - * this array! - */ - items: null, - - /** - * Add an item to the set. - * - * @param any item - */ - add(item) { - if (!this._set.has(item)) { - this._set.add(item); - this.items.push(item); - } - }, - - /** - * Return true if the item is in the set, false otherwise. - * - * @param any item - * @returns Boolean - */ - has(item) { - return this._set.has(item); - } -};
--- a/devtools/server/actors/utils/TabSources.js +++ b/devtools/server/actors/utils/TabSources.js @@ -240,17 +240,17 @@ TabSources.prototype = { } } if (url in this._sourceMappedSourceActors) { return this._sourceMappedSourceActors[url]; } } - throw new Error("getSourceByURL: could not find source for " + url); + throw new Error("getSourceActorByURL: could not find source for " + url); return null; }, /** * Returns true if the URL likely points to a minified resource, false * otherwise. * * @param String aURL
--- a/devtools/server/actors/utils/moz.build +++ b/devtools/server/actors/utils/moz.build @@ -5,13 +5,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( 'actor-registry-utils.js', 'audionodes.json', 'automation-timeline.js', 'make-debugger.js', 'map-uri-to-addon-id.js', - 'ScriptStore.js', 'stack.js', 'TabSources.js', 'walker-search.js' )
--- a/devtools/server/actors/webbrowser.js +++ b/devtools/server/actors/webbrowser.js @@ -2086,51 +2086,45 @@ TabActor.prototype = { * @param {String} request.url * @param {Number} request.line * @param {Number?} request.column * @return {Promise<Object>} */ onResolveLocation(request) { let { url, line } = request; let column = request.column || 0; - let actor = this.sources.getSourceActorByURL(url); - - if (actor) { - // Get the generated source actor if this is source mapped - let generatedActor = actor.generatedSource ? - this.sources.createNonSourceMappedActor(actor.generatedSource) : - actor; - let generatedLocation = new GeneratedLocation( - generatedActor, line, column); + const scripts = this.threadActor.dbg.findScripts({ url }); - return this.sources.getOriginalLocation(generatedLocation).then(loc => { - // If no map found, return this packet - if (loc.originalLine == null) { - return { - from: this.actorID, - type: "resolveLocation", - error: "MAP_NOT_FOUND" - }; - } - - loc = loc.toJSON(); - return { - from: this.actorID, - url: loc.source.url, - column: loc.column, - line: loc.line - }; + if (!scripts[0] || !scripts[0].source) { + return promise.resolve({ + from: this.actorID, + type: "resolveLocation", + error: "SOURCE_NOT_FOUND" }); } + const source = scripts[0].source; + const generatedActor = this.sources.createNonSourceMappedActor(source); + let generatedLocation = new GeneratedLocation( + generatedActor, line, column); + return this.sources.getOriginalLocation(generatedLocation).then(loc => { + // If no map found, return this packet + if (loc.originalLine == null) { + return { + type: "resolveLocation", + error: "MAP_NOT_FOUND" + }; + } - // Fall back to this packet when source is not found - return promise.resolve({ - from: this.actorID, - type: "resolveLocation", - error: "SOURCE_NOT_FOUND" + loc = loc.toJSON(); + return { + from: this.actorID, + url: loc.source.url, + column: loc.column, + line: loc.line + }; }); }, }; /** * The request types this actor can handle. */ TabActor.prototype.requestTypes = {
deleted file mode 100644 --- a/devtools/server/tests/unit/test_ScriptStore.js +++ /dev/null @@ -1,168 +0,0 @@ -/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -// Test the functionality of ScriptStore. - -const ScriptStore = require("devtools/server/actors/utils/ScriptStore"); - -// Fixtures - -const firstSource = "firstSource"; -const secondSource = "secondSource"; -const thirdSource = "thirdSource"; - -const scripts = new Set([ - { - url: "a.js", - source: firstSource, - startLine: 1, - lineCount: 100, - global: "g1" - }, - { - url: "a.js", - source: firstSource, - startLine: 1, - lineCount: 40, - global: "g1" - }, - { - url: "a.js", - source: firstSource, - startLine: 50, - lineCount: 100, - global: "g1" - }, - { - url: "a.js", - source: firstSource, - startLine: 60, - lineCount: 90, - global: "g1" - }, - { - url: "index.html", - source: secondSource, - startLine: 150, - lineCount: 1, - global: "g2" - }, - { - url: "index.html", - source: thirdSource, - startLine: 200, - lineCount: 100, - global: "g2" - }, - { - url: "index.html", - source: thirdSource, - startLine: 250, - lineCount: 10, - global: "g2" - }, - { - url: "index.html", - source: thirdSource, - startLine: 275, - lineCount: 5, - global: "g2" - } -]); - -function contains(script, line) { - return script.startLine <= line && - line < script.startLine + script.lineCount; -} - -function run_test() { - testAddScript(); - testAddScripts(); - testGetSources(); - testGetScriptsBySource(); - testGetScriptsBySourceAndLine(); - testGetScriptsByURL(); - testGetScriptsByURLAndLine(); -} - -function testAddScript() { - const ss = new ScriptStore(); - - for (let s of scripts) { - ss.addScript(s); - } - - equal(ss.getAllScripts().length, scripts.size); - - for (let s of ss.getAllScripts()) { - ok(scripts.has(s)); - } -} - -function testAddScripts() { - const ss = new ScriptStore(); - ss.addScripts([...scripts]); - - equal(ss.getAllScripts().length, scripts.size); - - for (let s of ss.getAllScripts()) { - ok(scripts.has(s)); - } -} - -function testGetSources() { - const ss = new ScriptStore(); - ss.addScripts([...scripts]); - - const expected = new Set([firstSource, secondSource, thirdSource]); - const actual = ss.getSources(); - equal(expected.size, actual.length); - - for (let s of actual) { - ok(expected.has(s)); - expected.delete(s); - } -} - -function testGetScriptsBySource() { - const ss = new ScriptStore(); - ss.addScripts([...scripts]); - - const expected = [...scripts].filter(s => s.source === thirdSource); - const actual = ss.getScriptsBySource(thirdSource); - - deepEqual(actual, expected); -} - -function testGetScriptsBySourceAndLine() { - const ss = new ScriptStore(); - ss.addScripts([...scripts]); - - const expected = [...scripts].filter( - s => s.source === firstSource && contains(s, 65)); - const actual = ss.getScriptsBySourceAndLine(firstSource, 65); - - deepEqual(actual, expected); -} - -function testGetScriptsByURL() { - const ss = new ScriptStore(); - ss.addScripts([...scripts]); - - const expected = [...scripts].filter(s => s.url === "index.html"); - const actual = ss.getScriptsByURL("index.html"); - - deepEqual(actual, expected); -} - -function testGetScriptsByURLAndLine() { - const ss = new ScriptStore(); - ss.addScripts([...scripts]); - - const expected = [...scripts].filter( - s => s.url === "index.html" && contains(s, 250)); - const actual = ss.getScriptsByURLAndLine("index.html", 250); - - deepEqual(actual, expected); -}
--- a/devtools/server/tests/unit/test_eventlooplag_actor.js +++ b/devtools/server/tests/unit/test_eventlooplag_actor.js @@ -4,17 +4,17 @@ /** * Test the eventLoopLag actor. */ "use strict"; function run_test() { - let {EventLoopLagFront} = require("devtools/server/actors/eventlooplag"); + let {EventLoopLagFront} = require("devtools/shared/fronts/eventlooplag"); DebuggerServer.init(); DebuggerServer.addBrowserActors(); // As seen in EventTracer.cpp let threshold = 20; let interval = 10;
--- a/devtools/server/tests/unit/xpcshell.ini +++ b/devtools/server/tests/unit/xpcshell.ini @@ -32,17 +32,16 @@ support-files = setBreakpoint-on-line-with-no-offsets-in-gcd-script.js addons/web-extension/manifest.json addons/web-extension2/manifest.json [test_addon_reload.js] [test_addons_actor.js] [test_animation_name.js] [test_animation_type.js] -[test_ScriptStore.js] [test_actor-registry-actor.js] [test_nesting-01.js] [test_nesting-02.js] [test_nesting-03.js] [test_forwardingprefix.js] [test_getyoungestframe.js] [test_nsjsinspector.js] [test_dbgactor.js]
new file mode 100644 --- /dev/null +++ b/devtools/shared/fronts/eventlooplag.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { eventLoopLagSpec } = require("devtools/shared/specs/eventlooplag"); + +exports.EventLoopLagFront = FrontClassWithSpec(eventLoopLagSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client); + this.actorID = form.eventLoopLagActor; + this.manage(this); + }, +});
--- a/devtools/shared/fronts/moz.build +++ b/devtools/shared/fronts/moz.build @@ -10,16 +10,17 @@ DevToolsModules( 'animation.js', 'call-watcher.js', 'canvas.js', 'css-properties.js', 'csscoverage.js', 'device.js', 'director-manager.js', 'director-registry.js', + 'eventlooplag.js', 'framerate.js', 'gcli.js', 'highlighters.js', 'inspector.js', 'layout.js', 'memory.js', 'performance-recording.js', 'performance.js',
new file mode 100644 --- /dev/null +++ b/devtools/shared/specs/eventlooplag.js @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Arg, RetVal, generateActorSpec } = require("devtools/shared/protocol"); + +const eventLoopLagSpec = generateActorSpec({ + typeName: "eventLoopLag", + + events: { + "event-loop-lag": { + type: "event-loop-lag", + // duration of the lag in milliseconds. + time: Arg(0, "number") + } + }, + + methods: { + start: { + request: {}, + response: {success: RetVal("number")} + }, + stop: { + request: {}, + response: {} + } + } +}); + +exports.eventLoopLagSpec = eventLoopLagSpec;
--- a/devtools/shared/specs/moz.build +++ b/devtools/shared/specs/moz.build @@ -12,16 +12,17 @@ DevToolsModules( 'call-watcher.js', 'canvas.js', 'css-properties.js', 'csscoverage.js', 'device.js', 'director-manager.js', 'director-registry.js', 'environment.js', + 'eventlooplag.js', 'frame.js', 'framerate.js', 'gcli.js', 'heap-snapshot-file.js', 'highlighters.js', 'inspector.js', 'layout.js', 'memory.js',
--- a/devtools/shared/webconsole/test/common.js +++ b/devtools/shared/webconsole/test/common.js @@ -92,16 +92,19 @@ function _attachConsole(aListeners, aCal aCallback(aState, aResponse); return; } let tab = aResponse.tabs[aResponse.selected]; aState.dbgClient.attachTab(tab.actor, function (response, tabClient) { if (aAttachToWorker) { let workerName = "console-test-worker.js#" + new Date().getTime(); var worker = new Worker(workerName); + // Keep a strong reference to the Worker to avoid it being + // GCd during the test (bug 1237492). + aState._worker_ref = worker; worker.addEventListener("message", function listener() { worker.removeEventListener("message", listener); tabClient.listWorkers(function (response) { let worker = response.workers.filter(w => w.url == workerName)[0]; if (!worker) { console.error("listWorkers failed. Unable to find the " + "worker actor\n"); return;
--- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -3897,30 +3897,35 @@ HTMLInputElement::CancelRangeThumbDrag(b } } void HTMLInputElement::SetValueOfRangeForUserEvent(Decimal aValue) { MOZ_ASSERT(aValue.isFinite()); + Decimal oldValue = GetValueAsDecimal(); + nsAutoString val; ConvertNumberToString(aValue, val); // TODO: What should we do if SetValueInternal fails? (The allocation // is small, so we should be fine here.) SetValueInternal(val, nsTextEditorState::eSetValue_BySetUserInput | nsTextEditorState::eSetValue_Notify); nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame()); if (frame) { frame->UpdateForValueChange(); } - nsContentUtils::DispatchTrustedEvent(OwnerDoc(), - static_cast<nsIDOMHTMLInputElement*>(this), - NS_LITERAL_STRING("input"), true, - false); + + if (GetValueAsDecimal() != oldValue) { + nsContentUtils::DispatchTrustedEvent(OwnerDoc(), + static_cast<nsIDOMHTMLInputElement*>(this), + NS_LITERAL_STRING("input"), true, + false); + } } void HTMLInputElement::StartNumberControlSpinnerSpin() { MOZ_ASSERT(!mNumberControlSpinnerIsSpinning); mNumberControlSpinnerIsSpinning = true;
--- a/dom/html/test/mochitest.ini +++ b/dom/html/test/mochitest.ini @@ -452,16 +452,17 @@ skip-if = (toolkit == 'gonk' && debug) # [test_bug893537.html] [test_bug95530.html] [test_bug969346.html] [test_bug982039.html] [test_bug1003539.html] [test_bug1045270.html] [test_bug1146116.html] [test_bug1264157.html] +[test_bug1287321.html] [test_change_crossorigin.html] [test_checked.html] [test_dir_attributes_reflection.html] [test_dl_attributes_reflection.html] [test_element_prototype.html] [test_embed_attributes_reflection.html] [test_formData.html] [test_formSubmission.html]
new file mode 100644 --- /dev/null +++ b/dom/html/test/test_bug1287321.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1287321 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1287321</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1287321 **/ + + function test() { + var r = document.getElementById("range"); + var rect = r.getBoundingClientRect(); + var y = parseInt((rect.height / 2)); + var movement = parseInt(rect.width / 10); + var x = movement; + synthesizeMouse(r, x, y, { type: "mousedown" }); + x += movement; + var eventCount = 0; + r.oninput = function() { + ++eventCount; + } + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 1, "Got the expected input event"); + + x += movement; + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 2, "Got the expected input event"); + + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 2, "Got the expected input event"); + + x += movement; + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 3, "Got the expected input event"); + + synthesizeMouse(r, x, y, { type: "mouseup" }); + is(eventCount, 3, "Got the expected input event"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(test); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287321">Mozilla Bug 1287321</a> +<input type="range" id="range"> +</body> +</html>
--- a/gfx/gl/GLContext.cpp +++ b/gfx/gl/GLContext.cpp @@ -6,16 +6,20 @@ #include "GLContext.h" #include <algorithm> #include <stdio.h> #include <string.h> #include <ctype.h> #include <vector> +#ifdef MOZ_WIDGET_ANDROID +#include <fcntl.h> +#include <sys/mman.h> +#endif #include "GLBlitHelper.h" #include "GLReadTexImageHelper.h" #include "GLScreenBuffer.h" #include "gfxCrashReporterUtils.h" #include "gfxEnv.h" #include "gfxUtils.h" @@ -27,29 +31,34 @@ #include "prlink.h" #include "ScopedGLHelpers.h" #include "SharedSurfaceGL.h" #include "GfxTexturesReporter.h" #include "TextureGarbageBin.h" #include "gfx2DGlue.h" #include "gfxPrefs.h" #include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/gfx/Logging.h" #include "OGLShaderProgram.h" // for ShaderProgramType #include "mozilla/DebugOnly.h" #ifdef XP_MACOSX #include <CoreServices/CoreServices.h> #endif #if defined(MOZ_WIDGET_COCOA) #include "nsCocoaFeatures.h" #endif +#ifdef MOZ_WIDGET_ANDROID +#include "AndroidBridge.h" +#endif + namespace mozilla { namespace gl { using namespace mozilla::gfx; using namespace mozilla::layers; #ifdef MOZ_GL_DEBUG unsigned GLContext::sCurrentGLContextTLS = -1; @@ -456,16 +465,17 @@ GLContext::GLContext(CreateContextFlags mLockedSurface(nullptr), mMaxTextureSize(0), mMaxCubeMapTextureSize(0), mMaxTextureImageSize(0), mMaxRenderbufferSize(0), mMaxSamples(0), mNeedsTextureSizeChecks(false), mNeedsFlushBeforeDeleteFB(false), + mTextureAllocCrashesOnMapFailure(false), mWorkAroundDriverBugs(true), mHeavyGLCallsSinceLastFlush(false) { mMaxViewportDims[0] = 0; mMaxViewportDims[1] = 0; mOwningThreadId = PlatformThread::CurrentId(); } @@ -800,17 +810,19 @@ GLContext::InitWithPrefixImpl(const char // The order of these strings must match up with the order of the enum // defined in GLContext.h for renderer IDs. const char* rendererMatchStrings[size_t(GLRenderer::Other)] = { "Adreno 200", "Adreno 205", "Adreno (TM) 200", "Adreno (TM) 205", + "Adreno (TM) 305", "Adreno (TM) 320", + "Adreno (TM) 330", "Adreno (TM) 420", "Mali-400 MP", "PowerVR SGX 530", "PowerVR SGX 540", "NVIDIA Tegra", "Android Emulator", "Gallium 0.4 on llvmpipe", "Intel HD Graphics 3000 OpenGL Engine", @@ -1043,16 +1055,27 @@ GLContext::InitWithPrefixImpl(const char } #endif if (mWorkAroundDriverBugs && Renderer() == GLRenderer::AdrenoTM420) { // see bug 1194923. Calling glFlush before glDeleteFramebuffers // prevents occasional driver crash. mNeedsFlushBeforeDeleteFB = true; } +#ifdef MOZ_WIDGET_ANDROID + if (mWorkAroundDriverBugs && + (Renderer() == GLRenderer::AdrenoTM305 || + Renderer() == GLRenderer::AdrenoTM320 || + Renderer() == GLRenderer::AdrenoTM330) && + AndroidBridge::Bridge()->GetAPIVersion() < 21) { + // Bug 1164027. Driver crashes when functions such as + // glTexImage2D fail due to virtual memory exhaustion. + mTextureAllocCrashesOnMapFailure = true; + } +#endif mMaxTextureImageSize = mMaxTextureSize; if (IsSupported(GLFeature::framebuffer_multisample)) { fGetIntegerv(LOCAL_GL_MAX_SAMPLES, (GLint*)&mMaxSamples); } //////////////////////////////////////////////////////////////////////////// @@ -2845,16 +2868,68 @@ GLContext::fDeleteFramebuffers(GLsizei n if (n == 1 && *names == 0) { // Deleting framebuffer 0 causes hangs on the DROID. See bug 623228. } else { raw_fDeleteFramebuffers(n, names); } TRACKING_CONTEXT(DeletedFramebuffers(this, n, names)); } +#ifdef MOZ_WIDGET_ANDROID +/** + * Conservatively estimate whether there is enough available + * contiguous virtual address space to map a newly allocated texture. + */ +static bool +WillTextureMapSucceed(GLsizei width, GLsizei height, GLenum format, GLenum type) +{ + bool willSucceed = false; + // Some drivers leave large gaps between textures, so require + // there to be double the actual size of the texture available. + size_t size = width * height * GetBytesPerTexel(format, type) * 2; + + int fd = open("/dev/zero", O_RDONLY); + + void *p = mmap(nullptr, size, PROT_NONE, MAP_SHARED, fd, 0); + if (p != MAP_FAILED) { + willSucceed = true; + munmap(p, size); + } + + close(fd); + + return willSucceed; +} +#endif // MOZ_WIDGET_ANDROID + +void +GLContext::fTexImage2D(GLenum target, GLint level, GLint internalformat, + GLsizei width, GLsizei height, GLint border, + GLenum format, GLenum type, const GLvoid* pixels) { + if (!IsTextureSizeSafeToPassToDriver(target, width, height)) { + // pass wrong values to cause the GL to generate GL_INVALID_VALUE. + // See bug 737182 and the comment in IsTextureSizeSafeToPassToDriver. + level = -1; + width = -1; + height = -1; + border = -1; + } +#if MOZ_WIDGET_ANDROID + if (mTextureAllocCrashesOnMapFailure) { + // We have no way of knowing whether this texture already has + // storage allocated for it, and therefore whether this check + // is necessary. We must therefore assume it does not and + // always perform the check. + if (!WillTextureMapSucceed(width, height, internalformat, type)) { + return; + } + } +#endif + raw_fTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); +} GLuint GLContext::GetDrawFB() { if (mScreen) return mScreen->GetDrawFB(); GLuint ret = 0; @@ -2963,11 +3038,55 @@ CreateTextureForOffscreen(GLContext* aGL MOZ_ASSERT(unpackType == LOCAL_GL_UNSIGNED_BYTE); internalFormat = LOCAL_GL_BGRA_EXT; unpackFormat = LOCAL_GL_BGRA_EXT; } return CreateTexture(aGL, internalFormat, unpackFormat, unpackType, aSize); } +uint32_t +GetBytesPerTexel(GLenum format, GLenum type) +{ + // If there is no defined format or type, we're not taking up any memory + if (!format || !type) { + return 0; + } + + if (format == LOCAL_GL_DEPTH_COMPONENT) { + if (type == LOCAL_GL_UNSIGNED_SHORT) + return 2; + else if (type == LOCAL_GL_UNSIGNED_INT) + return 4; + } else if (format == LOCAL_GL_DEPTH_STENCIL) { + if (type == LOCAL_GL_UNSIGNED_INT_24_8_EXT) + return 4; + } + + if (type == LOCAL_GL_UNSIGNED_BYTE || type == LOCAL_GL_FLOAT || type == LOCAL_GL_UNSIGNED_INT_8_8_8_8_REV) { + uint32_t multiplier = type == LOCAL_GL_UNSIGNED_BYTE ? 1 : 4; + switch (format) { + case LOCAL_GL_ALPHA: + case LOCAL_GL_LUMINANCE: + return 1 * multiplier; + case LOCAL_GL_LUMINANCE_ALPHA: + return 2 * multiplier; + case LOCAL_GL_RGB: + return 3 * multiplier; + case LOCAL_GL_RGBA: + return 4 * multiplier; + default: + break; + } + } else if (type == LOCAL_GL_UNSIGNED_SHORT_4_4_4_4 || + type == LOCAL_GL_UNSIGNED_SHORT_5_5_5_1 || + type == LOCAL_GL_UNSIGNED_SHORT_5_6_5) + { + return 2; + } + + gfxCriticalError() << "Unknown texture type " << type << " or format " << format; + MOZ_CRASH(); + return 0; +} } /* namespace gl */ } /* namespace mozilla */
--- a/gfx/gl/GLContext.h +++ b/gfx/gl/GLContext.h @@ -165,17 +165,19 @@ enum class GLVendor { Other }; enum class GLRenderer { Adreno200, Adreno205, AdrenoTM200, AdrenoTM205, + AdrenoTM305, AdrenoTM320, + AdrenoTM330, AdrenoTM420, Mali400MP, SGX530, SGX540, Tegra, AndroidEmulator, GalliumLlvmpipe, IntelHD3000, @@ -1616,27 +1618,19 @@ private: ASSERT_NOT_PASSING_STACK_BUFFER_TO_GL(pixels); BEFORE_GL_CALL; mSymbols.fTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); AFTER_GL_CALL; mHeavyGLCallsSinceLastFlush = true; } public: - void fTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels) { - if (!IsTextureSizeSafeToPassToDriver(target, width, height)) { - // pass wrong values to cause the GL to generate GL_INVALID_VALUE. - // See bug 737182 and the comment in IsTextureSizeSafeToPassToDriver. - level = -1; - width = -1; - height = -1; - border = -1; - } - raw_fTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); - } + void fTexImage2D(GLenum target, GLint level, GLint internalformat, + GLsizei width, GLsizei height, GLint border, + GLenum format, GLenum type, const GLvoid* pixels); void fTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid* pixels) { ASSERT_NOT_PASSING_STACK_BUFFER_TO_GL(pixels); BEFORE_GL_CALL; mSymbols.fTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); AFTER_GL_CALL; mHeavyGLCallsSinceLastFlush = true; } @@ -3526,16 +3520,17 @@ protected: GLint mMaxTextureSize; GLint mMaxCubeMapTextureSize; GLint mMaxTextureImageSize; GLint mMaxRenderbufferSize; GLint mMaxViewportDims[2]; GLsizei mMaxSamples; bool mNeedsTextureSizeChecks; bool mNeedsFlushBeforeDeleteFB; + bool mTextureAllocCrashesOnMapFailure; bool mWorkAroundDriverBugs; bool IsTextureSizeSafeToPassToDriver(GLenum target, GLsizei width, GLsizei height) const { if (mNeedsTextureSizeChecks) { // some drivers incorrectly handle some large texture sizes that are below the // max texture size that they report. So we check ourselves against our own values // (mMax[CubeMap]TextureSize). // see bug 737182 for Mac Intel 2D textures @@ -3694,12 +3689,18 @@ GLuint CreateTextureForOffscreen(GLConte * GL_TEXTURE_MIN_FILTER = GL_LINEAR * GL_TEXTURE_MAG_FILTER = GL_LINEAR * GL_TEXTURE_WRAP_S = GL_CLAMP_TO_EDGE * GL_TEXTURE_WRAP_T = GL_CLAMP_TO_EDGE */ GLuint CreateTexture(GLContext* aGL, GLenum aInternalFormat, GLenum aFormat, GLenum aType, const gfx::IntSize& aSize, bool linear = true); +/** + * Helper function that calculates the number of bytes required per + * texel for a texture from its format and type. + */ +uint32_t GetBytesPerTexel(GLenum format, GLenum type); + } /* namespace gl */ } /* namespace mozilla */ #endif /* GLCONTEXT_H_ */
--- a/gfx/gl/GLUploadHelpers.cpp +++ b/gfx/gl/GLUploadHelpers.cpp @@ -376,61 +376,16 @@ TexImage2DHelper(GLContext* gl, format, type, pixels); gl->fPixelStorei(LOCAL_GL_UNPACK_ROW_LENGTH, 0); gl->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, 4); } } -static uint32_t -GetBytesPerTexel(GLenum format, GLenum type) -{ - // If there is no defined format or type, we're not taking up any memory - if (!format || !type) { - return 0; - } - - if (format == LOCAL_GL_DEPTH_COMPONENT) { - if (type == LOCAL_GL_UNSIGNED_SHORT) - return 2; - else if (type == LOCAL_GL_UNSIGNED_INT) - return 4; - } else if (format == LOCAL_GL_DEPTH_STENCIL) { - if (type == LOCAL_GL_UNSIGNED_INT_24_8_EXT) - return 4; - } - - if (type == LOCAL_GL_UNSIGNED_BYTE || type == LOCAL_GL_FLOAT || type == LOCAL_GL_UNSIGNED_INT_8_8_8_8_REV) { - uint32_t multiplier = type == LOCAL_GL_UNSIGNED_BYTE ? 1 : 4; - switch (format) { - case LOCAL_GL_ALPHA: - case LOCAL_GL_LUMINANCE: - return 1 * multiplier; - case LOCAL_GL_LUMINANCE_ALPHA: - return 2 * multiplier; - case LOCAL_GL_RGB: - return 3 * multiplier; - case LOCAL_GL_RGBA: - return 4 * multiplier; - default: - break; - } - } else if (type == LOCAL_GL_UNSIGNED_SHORT_4_4_4_4 || - type == LOCAL_GL_UNSIGNED_SHORT_5_5_5_1 || - type == LOCAL_GL_UNSIGNED_SHORT_5_6_5) - { - return 2; - } - - gfxCriticalError() << "Unknown texture type " << type << " or format " << format; - MOZ_CRASH(); - return 0; -} - SurfaceFormat UploadImageDataToTexture(GLContext* gl, unsigned char* aData, int32_t aStride, SurfaceFormat aFormat, const nsIntRegion& aDstRegion, GLuint& aTexture, size_t* aOutUploadSize,
--- a/testing/mozbase/mozfile/mozfile/mozfile.py +++ b/testing/mozbase/mozfile/mozfile/mozfile.py @@ -137,16 +137,18 @@ def rmtree(dir): return remove(dir) def _call_windows_retry(func, args=(), retry_max=5, retry_delay=0.5): """ It's possible to see spurious errors on Windows due to various things keeping a handle to the directory open (explorer, virus scanners, etc) So we try a few times if it fails with a known error. + retry_delay is multiplied by the number of failed attempts to increase + the likelihood of success in subsequent attempts. """ retry_count = 0 while True: try: func(*args) except OSError as e: # Error codes are defined in: # http://docs.python.org/2/library/errno.html#module-errno @@ -155,34 +157,34 @@ def _call_windows_retry(func, args=(), r if retry_count == retry_max: raise retry_count += 1 print '%s() failed for "%s". Reason: %s (%s). Retrying...' % \ (func.__name__, args, e.strerror, e.errno) - time.sleep(retry_delay) + time.sleep(retry_count * retry_delay) else: # If no exception has been thrown it should be done break def remove(path): """Removes the specified file, link, or directory tree. This is a replacement for shutil.rmtree that works better under windows. It does the following things: - check path access for the current user before trying to remove - retry operations on some known errors due to various things keeping a handle on file paths - like explorer, virus scanners, etc. The known errors are errno.EACCES and errno.ENOTEMPTY, and it will - retry up to 5 five times with a delay of 0.5 seconds between each - attempt. + retry up to 5 five times with a delay of (failed_attempts * 0.5) seconds + between each attempt. Note that no error will be raised if the given path does not exists. :param path: path to be removed """ import shutil
--- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -5075,16 +5075,42 @@ "kind": "count", "description": "a testing histogram; not meant to be touched" }, "TELEMETRY_TEST_COUNT_INIT_NO_RECORD": { "expires_in_version": "never", "kind": "count", "description": "a testing histogram; not meant to be touched - initially not recording" }, + "TELEMETRY_TEST_CATEGORICAL": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1188888], + "expires_in_version": "never", + "kind": "categorical", + "labels": [ + "CommonLabel", + "Label2", + "Label3" + ], + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_CATEGORICAL_OPTOUT": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1188888], + "expires_in_version": "never", + "releaseChannelCollection": "opt-out", + "kind": "categorical", + "labels": [ + "CommonLabel", + "Label4", + "Label5", + "Label6" + ], + "description": "a testing histogram; not meant to be touched" + }, "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD": { "expires_in_version": "never", "kind": "count", "keyed": true, "description": "a testing histogram; not meant to be touched - initially not recording" }, "TELEMETRY_TEST_KEYED_FLAG": { "expires_in_version": "never",
--- a/toolkit/components/telemetry/Telemetry.cpp +++ b/toolkit/components/telemetry/Telemetry.cpp @@ -2795,16 +2795,22 @@ Accumulate(const char* name, uint32_t sa void Accumulate(const char *name, const nsCString& key, uint32_t sample) { TelemetryHistogram::Accumulate(name, key, sample); } void +AccumulateCategorical(ID id, const nsCString& label) +{ + TelemetryHistogram::AccumulateCategorical(id, label); +} + +void AccumulateTimeDelta(ID aHistogram, TimeStamp start, TimeStamp end) { Accumulate(aHistogram, static_cast<uint32_t>((end - start).ToMilliseconds())); } void ClearHistogram(ID aId)
--- a/toolkit/components/telemetry/Telemetry.h +++ b/toolkit/components/telemetry/Telemetry.h @@ -46,66 +46,92 @@ void CreateStatisticsRecorder(); void DestroyStatisticsRecorder(); /** * Initialize the Telemetry service on the main thread at startup. */ void Init(); /** - * Adds sample to a histogram defined in TelemetryHistograms.h + * Adds sample to a histogram defined in TelemetryHistogramEnums.h * * @param id - histogram id * @param sample - value to record. */ void Accumulate(ID id, uint32_t sample); /** - * Adds sample to a keyed histogram defined in TelemetryHistograms.h + * Adds sample to a keyed histogram defined in TelemetryHistogramEnums.h * * @param id - keyed histogram id * @param key - the string key * @param sample - (optional) value to record, defaults to 1. */ void Accumulate(ID id, const nsCString& key, uint32_t sample = 1); /** - * Adds a sample to a histogram defined in TelemetryHistograms.h. + * Adds a sample to a histogram defined in TelemetryHistogramEnums.h. * This function is here to support telemetry measurements from Java, * where we have only names and not numeric IDs. You should almost * certainly be using the by-enum-id version instead of this one. * * @param name - histogram name * @param sample - value to record */ void Accumulate(const char* name, uint32_t sample); /** - * Adds a sample to a histogram defined in TelemetryHistograms.h. + * Adds a sample to a histogram defined in TelemetryHistogramEnums.h. * This function is here to support telemetry measurements from Java, * where we have only names and not numeric IDs. You should almost * certainly be using the by-enum-id version instead of this one. * * @param name - histogram name * @param key - the string key * @param sample - sample - (optional) value to record, defaults to 1. */ void Accumulate(const char *name, const nsCString& key, uint32_t sample = 1); /** - * Adds time delta in milliseconds to a histogram defined in TelemetryHistograms.h + * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h + * This is the typesafe - and preferred - way to use the categorical histograms + * by passing values from the corresponding Telemetry::LABELS_* enum. + * + * @param enumValue - Label value from one of the Telemetry::LABELS_* enums. + */ +template<class E> +void AccumulateCategorical(E enumValue) { + static_assert(IsCategoricalLabelEnum<E>::value, + "Only categorical label enum types are supported."); + Accumulate(static_cast<ID>(CategoricalLabelId<E>::value), + static_cast<uint32_t>(enumValue)); +}; + +/** + * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h + * This string will be matched against the labels defined in Histograms.json. + * If the string does not match a label defined for the histogram, nothing will + * be recorded. + * + * @param id - The histogram id. + * @param label - A string label value that is defined in Histograms.json for this histogram. + */ +void AccumulateCategorical(ID id, const nsCString& label); + +/** + * Adds time delta in milliseconds to a histogram defined in TelemetryHistogramEnums.h * * @param id - histogram id * @param start - start time * @param end - end time */ void AccumulateTimeDelta(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()); /** - * This clears the data for a histogram in TelemetryHistograms.h. + * This clears the data for a histogram in TelemetryHistogramEnums.h. * * @param id - histogram id */ void ClearHistogram(ID id); /** * Enable/disable recording for this histogram at runtime. * Recording is enabled by default, unless listed at kRecordingInitiallyDisabledIDs[].
--- a/toolkit/components/telemetry/TelemetryHistogram.cpp +++ b/toolkit/components/telemetry/TelemetryHistogram.cpp @@ -114,20 +114,23 @@ typedef nsClassHashtable<nsCStringHashKe struct HistogramInfo { uint32_t min; uint32_t max; uint32_t bucketCount; uint32_t histogramType; uint32_t id_offset; uint32_t expiration_offset; uint32_t dataset; + uint32_t label_index; + uint32_t label_count; bool keyed; const char *id() const; const char *expiration() const; + nsresult label_id(const char* label, uint32_t* labelId) const; }; struct AddonHistogramInfo { uint32_t min; uint32_t max; uint32_t bucketCount; uint32_t histogramType; Histogram *h; @@ -284,16 +287,41 @@ HistogramInfo::id() const } const char * HistogramInfo::expiration() const { return &gHistogramStringTable[this->expiration_offset]; } +nsresult +HistogramInfo::label_id(const char* label, uint32_t* labelId) const +{ + MOZ_ASSERT(label); + MOZ_ASSERT(this->histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL); + if (this->histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) { + return NS_ERROR_FAILURE; + } + + for (uint32_t i = 0; i < this->label_count; ++i) { + // gHistogramLabelTable contains the indices of the label strings in the + // gHistogramStringTable. + // They are stored in-order and consecutively, from the offset label_index + // to (label_index + label_count). + uint32_t string_offset = gHistogramLabelTable[this->label_index + i]; + const char* const str = &gHistogramStringTable[string_offset]; + if (::strcmp(label, str) == 0) { + *labelId = i; + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + } // namespace //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////// // // PRIVATE: Histogram Get, Add, Clone, Clear functions @@ -349,16 +377,17 @@ internal_HistogramGet(const char *name, histogramType = nsITelemetry::HISTOGRAM_LINEAR; } switch (histogramType) { case nsITelemetry::HISTOGRAM_EXPONENTIAL: *result = Histogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag); break; case nsITelemetry::HISTOGRAM_LINEAR: + case nsITelemetry::HISTOGRAM_CATEGORICAL: *result = LinearHistogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag); break; case nsITelemetry::HISTOGRAM_BOOLEAN: *result = BooleanHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag); break; case nsITelemetry::HISTOGRAM_FLAG: *result = FlagHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag); break; @@ -563,16 +592,33 @@ internal_HistogramAdd(Histogram& histogr return NS_OK; } dataset = gHistograms[id].dataset; } return internal_HistogramAdd(histogram, value, dataset); } +nsresult +internal_HistogramAddCategorical(mozilla::Telemetry::ID id, const nsCString& label) +{ + uint32_t labelId = 0; + if (NS_FAILED(gHistograms[id].label_id(label.get(), &labelId))) { + return NS_ERROR_ILLEGAL_VALUE; + } + + Histogram* h = nullptr; + nsresult rv = internal_GetHistogramByEnumId(id, &h); + if (NS_FAILED(rv)) { + return rv; + } + + return internal_HistogramAdd(*h, labelId); +} + void internal_HistogramClear(Histogram& aHistogram, bool onlySubsession) { if (!onlySubsession) { aHistogram.Clear(); } #if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) @@ -1042,49 +1088,77 @@ internal_GetKeyedHistogramById(const nsA // that seems preferable to risking deadlock. namespace { bool internal_JSHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp) { JSObject *obj = JS_THIS_OBJECT(cx, vp); + MOZ_ASSERT(obj); if (!obj) { return false; } Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj)); MOZ_ASSERT(h); Histogram::ClassType type = h->histogram_type(); JS::CallArgs args = CallArgsFromVp(argc, vp); + if (!internal_CanRecordBase()) { + return true; + } + // If we don't have an argument for the count histogram, assume an increment of 1. // Otherwise, make sure to run some sanity checks on the argument. - int32_t value = 1; - if ((type != base::CountHistogram::COUNT_HISTOGRAM) || args.length()) { - if (!args.length()) { - JS_ReportError(cx, "Expected one argument"); + if ((type == base::CountHistogram::COUNT_HISTOGRAM) && (args.length() == 0)) { + internal_HistogramAdd(*h, 1); + return true; + } + + // For categorical histograms we allow passing a string argument that specifies the label. + mozilla::Telemetry::ID id; + if (type == base::LinearHistogram::LINEAR_HISTOGRAM && + (args.length() > 0) && args[0].isString() && + NS_SUCCEEDED(internal_GetHistogramEnumId(h->histogram_name().c_str(), &id)) && + gHistograms[id].histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL) { + nsAutoJSString label; + if (!label.init(cx, args[0])) { + JS_ReportError(cx, "Invalid string parameter"); return false; } - if (!(args[0].isNumber() || args[0].isBoolean())) { - JS_ReportError(cx, "Not a number"); + nsresult rv = internal_HistogramAddCategorical(id, NS_ConvertUTF16toUTF8(label)); + if (NS_FAILED(rv)) { + JS_ReportError(cx, "Unknown label for categorical histogram"); return false; } - if (!JS::ToInt32(cx, args[0], &value)) { - return false; - } + return true; + } + + // All other accumulations expect one numerical argument. + int32_t value = 0; + if (!args.length()) { + JS_ReportError(cx, "Expected one argument"); + return false; } - if (internal_CanRecordBase()) { - internal_HistogramAdd(*h, value); + if (!(args[0].isNumber() || args[0].isBoolean())) { + JS_ReportError(cx, "Not a number"); + return false; } + if (!JS::ToInt32(cx, args[0], &value)) { + JS_ReportError(cx, "Failed to convert argument"); + return false; + } + + internal_HistogramAdd(*h, value); return true; } bool internal_JSHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); JSObject *obj = JS_THIS_OBJECT(cx, vp); @@ -1872,16 +1946,24 @@ TelemetryHistogram::Accumulate(const cha mozilla::Telemetry::ID id; nsresult rv = internal_GetHistogramEnumId(name, &id); if (NS_SUCCEEDED(rv)) { internal_Accumulate(id, key, sample); } } void +TelemetryHistogram::AccumulateCategorical(mozilla::Telemetry::ID aId, + const nsCString& label) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_HistogramAddCategorical(aId, label); +} + +void TelemetryHistogram::ClearHistogram(mozilla::Telemetry::ID aId) { StaticMutexAutoLock locker(gTelemetryHistogramMutex); if (!internal_CanRecordBase()) { return; } Histogram *h;
--- a/toolkit/components/telemetry/TelemetryHistogram.h +++ b/toolkit/components/telemetry/TelemetryHistogram.h @@ -35,16 +35,18 @@ void SetHistogramRecordingEnabled(mozill nsresult SetHistogramRecordingEnabled(const nsACString &id, bool aEnabled); void Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample); void Accumulate(mozilla::Telemetry::ID aID, const nsCString& aKey, uint32_t aSample); void Accumulate(const char* name, uint32_t sample); void Accumulate(const char* name, const nsCString& key, uint32_t sample); +void AccumulateCategorical(mozilla::Telemetry::ID aId, const nsCString& aLabel); + void ClearHistogram(mozilla::Telemetry::ID aId); nsresult GetHistogramById(const nsACString &name, JSContext *cx, JS::MutableHandle<JS::Value> ret); nsresult
--- a/toolkit/components/telemetry/gen-histogram-data.py +++ b/toolkit/components/telemetry/gen-histogram-data.py @@ -10,42 +10,66 @@ from shared_telemetry_utils import Strin import sys import histogram_tools import itertools banner = """/* This file is auto-generated, see gen-histogram-data.py. */ """ -def print_array_entry(output, histogram, name_index, exp_index): +def print_array_entry(output, histogram, name_index, exp_index, label_index, label_count): cpp_guard = histogram.cpp_guard() if cpp_guard: print("#if defined(%s)" % cpp_guard, file=output) - print(" { %s, %s, %s, %s, %d, %d, %s, %s }," \ - % (histogram.low(), histogram.high(), - histogram.n_buckets(), histogram.nsITelemetry_kind(), - name_index, exp_index, histogram.dataset(), + print(" { %s, %s, %s, %s, %d, %d, %s, %d, %d, %s }," \ + % (histogram.low(), + histogram.high(), + histogram.n_buckets(), + histogram.nsITelemetry_kind(), + name_index, + exp_index, + histogram.dataset(), + label_index, + label_count, "true" if histogram.keyed() else "false"), file=output) if cpp_guard: print("#endif", file=output) def write_histogram_table(output, histograms): - table = StringTable() + string_table = StringTable() + label_table = [] + label_count = 0 print("const HistogramInfo gHistograms[] = {", file=output) for histogram in histograms: - name_index = table.stringIndex(histogram.name()) - exp_index = table.stringIndex(histogram.expiration()) - print_array_entry(output, histogram, name_index, exp_index) + name_index = string_table.stringIndex(histogram.name()) + exp_index = string_table.stringIndex(histogram.expiration()) + + labels = histogram.labels() + label_index = 0 + if len(labels) > 0: + label_index = label_count + label_table.append((histogram.name(), string_table.stringIndexes(labels))) + label_count += len(labels) + + print_array_entry(output, histogram, + name_index, exp_index, + label_index, len(labels)) + print("};\n", file=output) + + strtab_name = "gHistogramStringTable" + string_table.writeDefinition(output, strtab_name) + static_assert(output, "sizeof(%s) <= UINT32_MAX" % strtab_name, + "index overflow") + + print("\nconst uint32_t gHistogramLabelTable[] = {", file=output) + for name,indexes in label_table: + print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output) print("};", file=output) - strtab_name = "gHistogramStringTable" - table.writeDefinition(output, strtab_name) - static_assert(output, "sizeof(%s) <= UINT32_MAX" % strtab_name, - "index overflow") # Write out static asserts for histogram data. We'd prefer to perform # these checks in this script itself, but since several histograms # (generally enumerated histograms) use compile-time constants for # their upper bounds, we have to let the compiler do the checking. def static_asserts_for_boolean(output, histogram): pass @@ -84,16 +108,17 @@ def write_histogram_static_asserts(outpu // compile time, so that incorrect histogram definitions // give compile-time errors, not runtime errors.""", file=output) table = { 'boolean' : static_asserts_for_boolean, 'flag' : static_asserts_for_flag, 'count': static_asserts_for_count, 'enumerated' : static_asserts_for_enumerated, + 'categorical' : static_asserts_for_enumerated, 'linear' : static_asserts_for_linear, 'exponential' : static_asserts_for_exponential, } for histogram in histograms: histogram_tools.table_dispatch(histogram.kind(), table, lambda f: f(output, histogram))
--- a/toolkit/components/telemetry/gen-histogram-enum.py +++ b/toolkit/components/telemetry/gen-histogram-enum.py @@ -16,31 +16,47 @@ from __future__ import print_function import histogram_tools import itertools import sys banner = """/* This file is auto-generated, see gen-histogram-enum.py. */ """ +header = """ +#ifndef mozilla_TelemetryHistogramEnums_h +#define mozilla_TelemetryHistogramEnums_h + +#include "mozilla/TemplateLib.h" + +namespace mozilla { +namespace Telemetry { +""" + +footer = """ +} // namespace mozilla +} // namespace Telemetry +#endif // mozilla_TelemetryHistogramEnums_h""" + def main(output, *filenames): + # Print header. print(banner, file=output) - print("#ifndef mozilla_TelemetryHistogramEnums_h", file=output); - print("#define mozilla_TelemetryHistogramEnums_h", file=output); - print("namespace mozilla {", file=output) - print("namespace Telemetry {", file=output) - print("enum ID : uint32_t {", file=output) + print(header, file=output) - groups = itertools.groupby(histogram_tools.from_files(filenames), + # Load the histograms. + all_histograms = list(histogram_tools.from_files(filenames)) + groups = itertools.groupby(all_histograms, lambda h: h.name().startswith("USE_COUNTER2_")) - seen_use_counters = False + # Print the histogram enums. # Note that histogram_tools.py guarantees that all of the USE_COUNTER2_* # histograms are defined in a contiguous block. We therefore assume # that there's at most one group for which use_counter_group is true. + print("enum ID : uint32_t {", file=output) + seen_use_counters = False for (use_counter_group, histograms) in groups: if use_counter_group: seen_use_counters = True # The HistogramDUMMY* enum variables are used to make the computation # of Histogram{First,Last}UseCounter easier. Otherwise, we'd have to # special case the first and last histogram in the group. if use_counter_group: @@ -62,14 +78,30 @@ def main(output, *filenames): print(" HistogramCount,", file=output) if seen_use_counters: print(" HistogramUseCounterCount = HistogramLastUseCounter - HistogramFirstUseCounter + 1", file=output) else: print(" HistogramFirstUseCounter = 0,", file=output) print(" HistogramLastUseCounter = 0,", file=output) print(" HistogramUseCounterCount = 0", file=output) print("};", file=output) - print("} // namespace mozilla", file=output) - print("} // namespace Telemetry", file=output) - print("#endif // mozilla_TelemetryHistogramEnums_h", file=output); + + # Write categorical label enums. + categorical = filter(lambda h: h.kind() == "categorical", all_histograms) + enums = [("LABELS_" + h.name(), h.labels(), h.name()) for h in categorical] + for name,labels,_ in enums: + print("\nenum class %s : uint32_t {" % name, file=output) + print(" %s" % ",\n ".join(labels), file=output) + print("};", file=output) + + print("\ntemplate<class T> struct IsCategoricalLabelEnum : FalseType {};", file=output) + for name,_,_ in enums: + print("template<> struct IsCategoricalLabelEnum<%s> : TrueType {};" % name, file=output) + + print("\ntemplate<class T> struct CategoricalLabelId {};", file=output) + for name,_,id in enums: + print("template<> struct CategoricalLabelId<%s> : IntegralConstant<uint32_t, %s> {};" % (name, id), file=output) + + # Footer. + print(footer, file=output) if __name__ == '__main__': main(sys.stdout, *sys.argv[1:])
--- a/toolkit/components/telemetry/histogram_tools.py +++ b/toolkit/components/telemetry/histogram_tools.py @@ -4,16 +4,20 @@ import collections import json import math import os import re import sys +# Constants. +MAX_LABEL_LENGTH = 20 +MAX_LABEL_COUNT = 100 + # histogram_tools.py is used by scripts from a mozilla-central build tree # and also by outside consumers, such as the telemetry server. We need # to ensure that importing things works in both contexts. Therefore, # unconditionally importing things that are local to the build tree, such # as buildconfig, is a no-no. try: import buildconfig @@ -102,23 +106,27 @@ symbol that should guard C/C++ definitio self._is_use_counter = name.startswith("USE_COUNTER2_") self.verify_attributes(name, definition) self._name = name self._description = definition['description'] self._kind = definition['kind'] self._cpp_guard = definition.get('cpp_guard') self._keyed = definition.get('keyed', False) self._expiration = definition.get('expires_in_version') + self._labels = definition.get('labels', []) self.compute_bucket_parameters(definition) - table = { 'boolean': 'BOOLEAN', - 'flag': 'FLAG', - 'count': 'COUNT', - 'enumerated': 'LINEAR', - 'linear': 'LINEAR', - 'exponential': 'EXPONENTIAL' } + table = { + 'boolean': 'BOOLEAN', + 'flag': 'FLAG', + 'count': 'COUNT', + 'enumerated': 'LINEAR', + 'categorical': 'CATEGORICAL', + 'linear': 'LINEAR', + 'exponential': 'EXPONENTIAL', + } table_dispatch(self.kind(), table, lambda k: self._set_nsITelemetry_kind(k)) datasets = { 'opt-in': 'DATASET_RELEASE_CHANNEL_OPTIN', 'opt-out': 'DATASET_RELEASE_CHANNEL_OPTOUT' } value = definition.get('releaseChannelCollection', 'opt-in') if not value in datasets: raise DefinitionException, "unknown release channel collection policy for " + name self._dataset = "nsITelemetry::" + datasets[value] @@ -128,17 +136,18 @@ symbol that should guard C/C++ definitio return self._name def description(self): """Return the description of the histogram.""" return self._description def kind(self): """Return the kind of the histogram. -Will be one of 'boolean', 'flag', 'count', 'enumerated', 'linear', or 'exponential'.""" +Will be one of 'boolean', 'flag', 'count', 'enumerated', 'categorical', 'linear', +or 'exponential'.""" return self._kind def expiration(self): """Return the expiration version of the histogram.""" return self._expiration def nsITelemetry_kind(self): """Return the nsITelemetry constant corresponding to the kind of @@ -168,123 +177,161 @@ associated with the histogram. Returns def keyed(self): """Returns True if this a keyed histogram, false otherwise.""" return self._keyed def dataset(self): """Returns the dataset this histogram belongs into.""" return self._dataset + def labels(self): + """Returns a list of labels for a categorical histogram, [] for others.""" + return self._labels + def ranges(self): """Return an array of lower bounds for each bucket in the histogram.""" - table = { 'boolean': linear_buckets, - 'flag': linear_buckets, - 'count': linear_buckets, - 'enumerated': linear_buckets, - 'linear': linear_buckets, - 'exponential': exponential_buckets } + table = { + 'boolean': linear_buckets, + 'flag': linear_buckets, + 'count': linear_buckets, + 'enumerated': linear_buckets, + 'categorical': linear_buckets, + 'linear': linear_buckets, + 'exponential': exponential_buckets, + } return table_dispatch(self.kind(), table, lambda p: p(self.low(), self.high(), self.n_buckets())) def compute_bucket_parameters(self, definition): table = { 'boolean': Histogram.boolean_flag_bucket_parameters, 'flag': Histogram.boolean_flag_bucket_parameters, 'count': Histogram.boolean_flag_bucket_parameters, 'enumerated': Histogram.enumerated_bucket_parameters, + 'categorical': Histogram.categorical_bucket_parameters, 'linear': Histogram.linear_bucket_parameters, - 'exponential': Histogram.exponential_bucket_parameters - } + 'exponential': Histogram.exponential_bucket_parameters, + } table_dispatch(self.kind(), table, lambda p: self.set_bucket_parameters(*p(definition))) def verify_attributes(self, name, definition): global always_allowed_keys general_keys = always_allowed_keys + ['low', 'high', 'n_buckets'] table = { 'boolean': always_allowed_keys, 'flag': always_allowed_keys, 'count': always_allowed_keys, 'enumerated': always_allowed_keys + ['n_values'], + 'categorical': always_allowed_keys + ['labels'], 'linear': general_keys, - 'exponential': general_keys - } + 'exponential': general_keys, + } # We removed extended_statistics_ok on the client, but the server-side, # where _strict_type_checks==False, has to deal with historical data. if not self._strict_type_checks: table['exponential'].append('extended_statistics_ok') table_dispatch(definition['kind'], table, lambda allowed_keys: Histogram.check_keys(name, definition, allowed_keys)) - # Check for the alert_emails field. Use counters don't have any mechanism - # to add them, so skip the check for them. - if not self._is_use_counter: - if 'alert_emails' not in definition: - if whitelists is not None and name not in whitelists['alert_emails']: - raise KeyError, 'New histogram "%s" must have an alert_emails field.' % name - elif not isinstance(definition['alert_emails'], list): - raise KeyError, 'alert_emails must be an array (in histogram "%s")' % name + self.check_name(name) + self.check_field_types(name, definition) + self.check_whitelistable_fields(name, definition) + self.check_expiration(name, definition) + self.check_label_values(name, definition) - Histogram.check_name(name) - self.check_field_types(name, definition) - Histogram.check_expiration(name, definition) - self.check_bug_numbers(name, definition) - - @staticmethod - def check_name(name): + def check_name(self, name): if '#' in name: raise ValueError, '"#" not permitted for %s' % (name) - @staticmethod - def check_expiration(name, definition): + # Avoid C++ identifier conflicts between histogram enums and label enum names. + if name.startswith("LABELS_"): + raise ValueError, "Histogram name '%s' can not start with LABELS_" % (name) + + # To make it easier to generate C++ identifiers from this etc., we restrict + # the histogram names to a strict pattern. + # We skip this on the server to avoid failures with old Histogram.json revisions. + if self._strict_type_checks: + pattern = '^[a-z][a-z0-9_]+[a-z0-9]$' + if not re.match(pattern, name, re.IGNORECASE): + raise ValueError, "Histogram name '%s' doesn't confirm to '%s'" % (name, pattern) + + def check_expiration(self, name, definition): expiration = definition.get('expires_in_version') if not expiration: return if re.match(r'^[1-9][0-9]*$', expiration): expiration = expiration + ".0a1" elif re.match(r'^[1-9][0-9]*\.0$', expiration): expiration = expiration + "a1" definition['expires_in_version'] = expiration - def check_bug_numbers(self, name, definition): - # Use counters don't have any mechanism to add the bug numbers field. + def check_label_values(self, name, definition): + labels = definition.get('labels') + if not labels: + return + + invalid = filter(lambda l: len(l) > MAX_LABEL_LENGTH, labels) + if len(invalid) > 0: + raise ValueError, 'Label values for %s exceed length limit of %d: %s' % \ + (name, MAX_LABEL_LENGTH, ', '.join(invalid)) + + if len(labels) > MAX_LABEL_COUNT: + raise ValueError, 'Label count for %s exceeds limit of %d' % \ + (name, MAX_LABEL_COUNT) + + # To make it easier to generate C++ identifiers from this etc., we restrict + # the label values to a strict pattern. + pattern = '^[a-z][a-z0-9_]+[a-z0-9]$' + invalid = filter(lambda l: not re.match(pattern, l, re.IGNORECASE), labels) + if len(invalid) > 0: + raise ValueError, 'Label values for %s are not matching pattern "%s": %s' % \ + (name, pattern, ', '.join(invalid)) + + # Check for the presence of fields that old histograms are whitelisted for. + def check_whitelistable_fields(self, name, definition): + # Use counters don't have any mechanism to add the fields checked here, + # so skip the check for them. if self._is_use_counter: return - bug_numbers = definition.get('bug_numbers') - if not bug_numbers: - if whitelists is None or name in whitelists['bug_numbers']: - return - else: - raise KeyError, 'New histogram "%s" must have a bug_numbers field.' % name - if not isinstance(bug_numbers, list): - raise ValueError, 'bug_numbers field for "%s" should be an array' % (name) + # In the pipeline we don't have whitelists available. + if whitelists is None: + return - if not all(type(num) is int for num in bug_numbers): - raise ValueError, 'bug_numbers array for "%s" should only contain integers' % (name) + for field in ['alert_emails', 'bug_numbers']: + if field not in definition and name not in whitelists[field]: + raise KeyError, 'New histogram "%s" must have a %s field.' % (name, field) def check_field_types(self, name, definition): # Define expected types for the histogram properties. type_checked_fields = { - "n_buckets": int, - "n_values": int, - "low": int, - "high": int, - "keyed": bool, - "expires_in_version": basestring, - "kind": basestring, - "description": basestring, - "cpp_guard": basestring, - "releaseChannelCollection": basestring - } + "n_buckets": int, + "n_values": int, + "low": int, + "high": int, + "keyed": bool, + "expires_in_version": basestring, + "kind": basestring, + "description": basestring, + "cpp_guard": basestring, + "releaseChannelCollection": basestring, + } + + # For list fields we check the items types. + type_checked_list_fields = { + "bug_numbers": int, + "alert_emails": basestring, + "labels": basestring, + } # For the server-side, where _strict_type_checks==False, we want to # skip the stricter type checks for these fields for dealing with # historical data. coerce_fields = ["low", "high", "n_values", "n_buckets"] if not self._strict_type_checks: def try_to_coerce_to_number(v): try: @@ -292,26 +339,34 @@ associated with the histogram. Returns except: return v for key in [k for k in coerce_fields if k in definition]: definition[key] = try_to_coerce_to_number(definition[key]) # This handles old "keyed":"true" definitions (bug 1271986). if definition.get("keyed", None) == "true": definition["keyed"] = True + def nice_type_name(t): + if t is basestring: + return "string" + return t.__name__ + for key, key_type in type_checked_fields.iteritems(): if not key in definition: continue if not isinstance(definition[key], key_type): - if key_type is basestring: - type_name = "string" - else: - type_name = key_type.__name__ raise ValueError, ('value for key "{0}" in Histogram "{1}" ' - 'should be {2}').format(key, name, type_name) + 'should be {2}').format(key, name, nice_type_name(key_type)) + + for key, key_type in type_checked_list_fields.iteritems(): + if not key in definition: + continue + if not all(isinstance(x, key_type) for x in definition[key]): + raise ValueError, ('all values for list "{0}" in Histogram "{1}" ' + 'should be {2}').format(key, name, nice_type_name(key_type)) @staticmethod def check_keys(name, definition, allowed_keys): for key in definition.iterkeys(): if key not in allowed_keys: raise KeyError, '%s not permitted for %s' % (key, name) def set_bucket_parameters(self, low, high, n_buckets): @@ -335,16 +390,21 @@ associated with the histogram. Returns definition['n_buckets']) @staticmethod def enumerated_bucket_parameters(definition): n_values = definition['n_values'] return (1, n_values, n_values + 1) @staticmethod + def categorical_bucket_parameters(definition): + n_values = len(definition['labels']) + return (1, n_values, n_values + 1) + + @staticmethod def exponential_bucket_parameters(definition): return (definition.get('low', 1), definition['high'], definition['n_buckets']) # We support generating histograms from multiple different input files, not # just Histograms.json. For each file's basename, we have a specific # routine to parse that file, and return a dictionary mapping histogram
--- a/toolkit/components/telemetry/nsITelemetry.idl +++ b/toolkit/components/telemetry/nsITelemetry.idl @@ -17,22 +17,24 @@ interface nsITelemetry : nsISupports { /** * Histogram types: * HISTOGRAM_EXPONENTIAL - buckets increase exponentially * HISTOGRAM_LINEAR - buckets increase linearly * HISTOGRAM_BOOLEAN - For storing 0/1 values * HISTOGRAM_FLAG - For storing a single value; its count is always == 1. * HISTOGRAM_COUNT - For storing counter values without bucketing. + * HISTOGRAM_CATEGORICAL - For storing enumerated values by label. */ const unsigned long HISTOGRAM_EXPONENTIAL = 0; const unsigned long HISTOGRAM_LINEAR = 1; const unsigned long HISTOGRAM_BOOLEAN = 2; const unsigned long HISTOGRAM_FLAG = 3; const unsigned long HISTOGRAM_COUNT = 4; + const unsigned long HISTOGRAM_CATEGORICAL = 5; /** * Scalar types: * SCALAR_COUNT - for storing a numeric value * SCALAR_STRING - for storing a string value * SCALAR_BOOLEAN - for storing a boolean value */ const unsigned long SCALAR_COUNT = 0;
--- a/toolkit/components/telemetry/shared_telemetry_utils.py +++ b/toolkit/components/telemetry/shared_telemetry_utils.py @@ -30,42 +30,59 @@ class StringTable: if string in self.table: return self.table[string] else: result = self.current_index self.table[string] = result self.current_index += self.c_strlen(string) return result + def stringIndexes(self, strings): + """ Returns a list of indexes for the provided list of strings. + Adds the strings to the table if they are not in it yet. + :param strings: list of strings to put into the table. + """ + return [self.stringIndex(s) for s in strings] + def writeDefinition(self, f, name): """Writes the string table to a file as a C const char array. + + This writes out the string table as one single C char array for memory + size reasons, separating the individual strings with '\0' characters. + This way we can index directly into the string array and avoid the additional + storage costs for the pointers to them (and potential extra relocations for those). + :param f: the output stream. :param name: the name of the output array. """ entries = self.table.items() entries.sort(key=lambda x:x[1]) + # Avoid null-in-string warnings with GCC and potentially # overlong string constants; write everything out the long way. def explodeToCharArray(string): def toCChar(s): if s == "'": return "'\\''" else: return "'%s'" % s return ", ".join(map(toCChar, string)) + f.write("const char %s[] = {\n" % name) - for (string, offset) in entries[:-1]: + for (string, offset) in entries: + if "*/" in string: + raise ValueError, "String in string table contains unexpected sequence '*/': %s" % string + e = explodeToCharArray(string) if e: - f.write(" /* %5d */ %s, '\\0',\n" - % (offset, explodeToCharArray(string))) + f.write(" /* %5d - \"%s\" */ %s, '\\0',\n" + % (offset, string, explodeToCharArray(string))) else: - f.write(" /* %5d */ '\\0',\n" % offset) - f.write(" /* %5d */ %s, '\\0' };\n\n" - % (entries[-1][1], explodeToCharArray(entries[-1][0]))) + f.write(" /* %5d - \"%s\" */ '\\0',\n" % (offset, string)) + f.write("};\n\n") def static_assert(output, expression, message): """Writes a C++ compile-time assertion expression to a file. :param output: the output stream. :param expression: the expression to check. :param message: the string literal that will appear if the expression evaluates to false. """
--- a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js +++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js @@ -1,35 +1,67 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const INT_MAX = 0x7FFFFFFF; Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); -function test_expired_histogram() { - var histogram_id = "FOOBAR"; - var test_expired_id = "TELEMETRY_TEST_EXPIRED"; - var clone_id = "ExpiredClone"; - var dummy = Telemetry.newHistogram(histogram_id, "28.0a1", Telemetry.HISTOGRAM_EXPONENTIAL, 1, 2, 3); - var dummy_clone = Telemetry.histogramFrom(clone_id, test_expired_id); - var rh = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []); - Assert.ok(!!rh); +// Return an array of numbers from lower up to, excluding, upper +function numberRange(lower, upper) +{ + let a = []; + for (let i=lower; i<upper; ++i) { + a.push(i); + } + return a; +} - dummy.add(1); - dummy_clone.add(1); - - do_check_eq(Telemetry.histogramSnapshots["__expired__"], undefined); - do_check_eq(Telemetry.histogramSnapshots[histogram_id], undefined); - do_check_eq(Telemetry.histogramSnapshots[test_expired_id], undefined); - do_check_eq(Telemetry.histogramSnapshots[clone_id], undefined); - do_check_eq(rh[test_expired_id], undefined); +function expect_fail(f) { + let failed = false; + try { + f(); + failed = false; + } catch (e) { + failed = true; + } + do_check_true(failed); } -function test_histogram(histogram_type, name, min, max, bucket_count) { +function expect_success(f) { + let succeeded = false; + try { + f(); + succeeded = true; + } catch (e) { + succeeded = false; + } + do_check_true(succeeded); +} + +function compareHistograms(h1, h2) { + let s1 = h1.snapshot(); + let s2 = h2.snapshot(); + + do_check_eq(s1.histogram_type, s2.histogram_type); + do_check_eq(s1.min, s2.min); + do_check_eq(s1.max, s2.max); + do_check_eq(s1.sum, s2.sum); + + do_check_eq(s1.counts.length, s2.counts.length); + for (let i = 0; i < s1.counts.length; i++) + do_check_eq(s1.counts[i], s2.counts[i]); + + do_check_eq(s1.ranges.length, s2.ranges.length); + for (let i = 0; i < s1.ranges.length; i++) + do_check_eq(s1.ranges[i], s2.ranges[i]); +} + +function check_histogram(histogram_type, name, min, max, bucket_count) { var h = Telemetry.newHistogram(name, "never", histogram_type, min, max, bucket_count); var r = h.snapshot().ranges; var sum = 0; for(let i=0;i<r.length;i++) { var v = r[i]; sum += v; h.add(v); } @@ -65,40 +97,56 @@ function test_histogram(histogram_type, h.add(0); h.add(1); var c = h.snapshot().counts; do_check_eq(c[0], 1); do_check_eq(c[1], 1); } -function expect_fail(f) { - let failed = false; - try { - f(); - failed = false; - } catch (e) { - failed = true; - } - do_check_true(failed); -} +// This MUST be the very first test of this file. +add_task({ + skip_if: () => gIsAndroid +}, +function* test_instantiate() { + const ID = "TELEMETRY_TEST_COUNT"; + let h = Telemetry.getHistogramById(ID); + + // Instantiate the subsession histogram through |add| and make sure they match. + // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise + // |add| will not instantiate the histogram. + h.add(1); + let snapshot = h.snapshot(); + let subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.equal(snapshot.sum, subsession[ID].sum, + "Histogram and subsession histogram sum must match."); + // Clear the histogram, so we don't void the assumptions from the other tests. + h.clear(); +}); -function expect_success(f) { - let succeeded = false; - try { - f(); - succeeded = true; - } catch (e) { - succeeded = false; +add_task(function* test_parameterChecks() { + let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR] + for (let histogram_type of kinds) { + let [min, max, bucket_count] = [1, INT_MAX - 1, 10] + check_histogram(histogram_type, "test::"+histogram_type, min, max, bucket_count); + + const nh = Telemetry.newHistogram; + expect_fail(() => nh("test::min", "never", histogram_type, 0, max, bucket_count)); + expect_fail(() => nh("test::bucket_count", "never", histogram_type, min, max, 1)); } - do_check_true(succeeded); -} +}); -function test_boolean_histogram() -{ +add_task(function* test_noSerialization() { + // Instantiate the storage for this histogram and make sure it doesn't + // get reflected into JS, as it has no interesting data in it. + Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT"); + do_check_false("NEWTAB_PAGE_PINNED_SITES_COUNT" in Telemetry.histogramSnapshots); +}); + +add_task(function* test_boolean_histogram() { var h = Telemetry.newHistogram("test::boolean histogram", "never", Telemetry.HISTOGRAM_BOOLEAN); var r = h.snapshot().ranges; // boolean histograms ignore numeric parameters do_check_eq(uneval(r), uneval([0, 1, 2])) var sum = 0 for(var i=0;i<r.length;i++) { var v = r[i]; sum += v; @@ -107,20 +155,19 @@ function test_boolean_histogram() h.add(true); h.add(false); var s = h.snapshot(); do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_BOOLEAN); // last bucket should always be 0 since .add parameters are normalized to either 0 or 1 do_check_eq(s.counts[2], 0); do_check_eq(s.sum, 3); do_check_eq(s.counts[0], 2); -} +}); -function test_flag_histogram() -{ +add_task(function* test_flag_histogram() { var h = Telemetry.newHistogram("test::flag histogram", "never", Telemetry.HISTOGRAM_FLAG); var r = h.snapshot().ranges; // Flag histograms ignore numeric parameters. do_check_eq(uneval(r), uneval([0, 1, 2])); // Should already have a 0 counted. var c = h.snapshot().counts; var s = h.snapshot().sum; do_check_eq(uneval(c), uneval([1, 0, 0])); @@ -133,68 +180,78 @@ function test_flag_histogram() do_check_eq(s2, 1); // Should only switch counts once. h.add(1); var c3 = h.snapshot().counts; var s3 = h.snapshot().sum; do_check_eq(uneval(c3), uneval([0, 1, 0])); do_check_eq(s3, 1); do_check_eq(h.snapshot().histogram_type, Telemetry.HISTOGRAM_FLAG); -} +}); -function test_count_histogram() -{ +add_task(function* test_count_histogram() { let h = Telemetry.newHistogram("test::count histogram", "never", Telemetry.HISTOGRAM_COUNT, 1, 2, 3); let s = h.snapshot(); do_check_eq(uneval(s.ranges), uneval([0, 1, 2])); do_check_eq(uneval(s.counts), uneval([0, 0, 0])); do_check_eq(s.sum, 0); h.add(); s = h.snapshot(); do_check_eq(uneval(s.counts), uneval([1, 0, 0])); do_check_eq(s.sum, 1); h.add(); s = h.snapshot(); do_check_eq(uneval(s.counts), uneval([2, 0, 0])); do_check_eq(s.sum, 2); -} +}); + +add_task(function* test_categorical_histogram() +{ + let h1 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL"); + for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) { + h1.add(v); + } + for (let s of ["", "Label4", "1234"]) { + Assert.throws(() => h1.add(s)); + } -function test_getHistogramById() { + let snapshot = h1.snapshot(); + Assert.equal(snapshot.sum, 6); + Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3]); + Assert.deepEqual(snapshot.counts, [3, 2, 2, 0]); + + let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_OPTOUT"); + for (let v of ["CommonLabel", "CommonLabel", "Label4", "Label5", "Label6", 0, 1]) { + h2.add(v); + } + for (let s of ["", "Label3", "1234"]) { + Assert.throws(() => h2.add(s)); + } + + snapshot = h2.snapshot(); + Assert.equal(snapshot.sum, 7); + Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3, 4]); + Assert.deepEqual(snapshot.counts, [3, 2, 1, 1, 0]); +}); + +add_task(function* test_getHistogramById() { try { Telemetry.getHistogramById("nonexistent"); do_throw("This can't happen"); } catch (e) { } var h = Telemetry.getHistogramById("CYCLE_COLLECTOR"); var s = h.snapshot(); do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_EXPONENTIAL); do_check_eq(s.min, 1); do_check_eq(s.max, 10000); -} - -function compareHistograms(h1, h2) { - let s1 = h1.snapshot(); - let s2 = h2.snapshot(); - - do_check_eq(s1.histogram_type, s2.histogram_type); - do_check_eq(s1.min, s2.min); - do_check_eq(s1.max, s2.max); - do_check_eq(s1.sum, s2.sum); +}); - do_check_eq(s1.counts.length, s2.counts.length); - for (let i = 0; i < s1.counts.length; i++) - do_check_eq(s1.counts[i], s2.counts[i]); - - do_check_eq(s1.ranges.length, s2.ranges.length); - for (let i = 0; i < s1.ranges.length; i++) - do_check_eq(s1.ranges[i], s2.ranges[i]); -} - -function test_histogramFrom() { +add_task(function* test_histogramFrom() { // Test one histogram of each type. let names = [ "CYCLE_COLLECTOR", // EXPONENTIAL "GC_REASON_2", // LINEAR "GC_RESET", // BOOLEAN "TELEMETRY_TEST_FLAG", // FLAG "TELEMETRY_TEST_COUNT", // COUNT ]; @@ -211,31 +268,97 @@ function test_histogramFrom() { let testFlag = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG"); testFlag.add(1); let testCount = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT"); testCount.add(); let clone = Telemetry.histogramFrom("FlagClone", "TELEMETRY_TEST_FLAG"); compareHistograms(testFlag, clone); clone = Telemetry.histogramFrom("CountClone", "TELEMETRY_TEST_COUNT"); compareHistograms(testCount, clone); -} +}); -function test_getSlowSQL() { +add_task(function* test_getSlowSQL() { var slow = Telemetry.slowSQL; do_check_true(("mainThread" in slow) && ("otherThreads" in slow)); -} +}); -function test_getWebrtc() { +add_task(function* test_getWebrtc() { var webrtc = Telemetry.webrtcStats; do_check_true("IceCandidatesStats" in webrtc); var icestats = webrtc.IceCandidatesStats; do_check_true(("webrtc" in icestats) && ("loop" in icestats)); -} +}); + +// Check that telemetry doesn't record in private mode +add_task(function* test_privateMode() { + var h = Telemetry.newHistogram("test::private_mode_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN); + var orig = h.snapshot(); + Telemetry.canRecordExtended = false; + h.add(1); + do_check_eq(uneval(orig), uneval(h.snapshot())); + Telemetry.canRecordExtended = true; + h.add(1); + do_check_neq(uneval(orig), uneval(h.snapshot())); +}); + +// Check that telemetry records only when it is suppose to. +add_task(function* test_histogramRecording() { + // Check that no histogram is recorded if both base and extended recording are off. + Telemetry.canRecordBase = false; + Telemetry.canRecordExtended = false; + + let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); + h.clear(); + let orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum, h.snapshot().sum); + + // Check that only base histograms are recorded. + Telemetry.canRecordBase = true; + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Histogram value should have incremented by 1 due to recording."); -function test_addons() { + // Extended histograms should not be recorded. + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN"); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum, h.snapshot().sum, + "Histograms should be equal after recording."); + + // Runtime created histograms should not be recorded. + h = Telemetry.newHistogram("test::runtime_created_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum, h.snapshot().sum, + "Histograms should be equal after recording."); + + // Check that extended histograms are recorded when required. + Telemetry.canRecordExtended = true; + + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Runtime histogram value should have incremented by 1 due to recording."); + + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN"); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Histogram value should have incremented by 1 due to recording."); + + // Check that base histograms are still being recorded. + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); + h.clear(); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Histogram value should have incremented by 1 due to recording."); +}); + +add_task(function* test_addons() { var addon_id = "testing-addon"; var fake_addon_id = "fake-addon"; var name1 = "testing-histogram1"; var register = Telemetry.registerAddonHistogram; expect_success(() => register(addon_id, name1, Telemetry.HISTOGRAM_LINEAR, 1, 5, 6)); // Can't register the same histogram multiple times. expect_fail(() => @@ -311,96 +434,60 @@ function test_addons() { do_check_false(name2 in snapshots[flag_addon]); // Check that we can remove addon histograms. Telemetry.unregisterAddonHistograms(addon_id); snapshots = Telemetry.addonHistogramSnapshots; do_check_false(addon_id in snapshots); // Make sure other addons are unaffected. do_check_true(extra_addon in snapshots); -} - -// Check that telemetry doesn't record in private mode -function test_privateMode() { - var h = Telemetry.newHistogram("test::private_mode_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN); - var orig = h.snapshot(); - Telemetry.canRecordExtended = false; - h.add(1); - do_check_eq(uneval(orig), uneval(h.snapshot())); - Telemetry.canRecordExtended = true; - h.add(1); - do_check_neq(uneval(orig), uneval(h.snapshot())); -} +}); -// Check that telemetry records only when it is suppose to. -function test_histogramRecording() { - // Check that no histogram is recorded if both base and extended recording are off. - Telemetry.canRecordBase = false; - Telemetry.canRecordExtended = false; - - let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); - h.clear(); - let orig = h.snapshot(); - h.add(1); - Assert.equal(orig.sum, h.snapshot().sum); +add_task(function* test_expired_histogram() { + var histogram_id = "FOOBAR"; + var test_expired_id = "TELEMETRY_TEST_EXPIRED"; + var clone_id = "ExpiredClone"; + var dummy = Telemetry.newHistogram(histogram_id, "28.0a1", Telemetry.HISTOGRAM_EXPONENTIAL, 1, 2, 3); + var dummy_clone = Telemetry.histogramFrom(clone_id, test_expired_id); + var rh = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []); + Assert.ok(!!rh); - // Check that only base histograms are recorded. - Telemetry.canRecordBase = true; - h.add(1); - Assert.equal(orig.sum + 1, h.snapshot().sum, - "Histogram value should have incremented by 1 due to recording."); + dummy.add(1); + dummy_clone.add(1); - // Extended histograms should not be recorded. - h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN"); - orig = h.snapshot(); - h.add(1); - Assert.equal(orig.sum, h.snapshot().sum, - "Histograms should be equal after recording."); + do_check_eq(Telemetry.histogramSnapshots["__expired__"], undefined); + do_check_eq(Telemetry.histogramSnapshots[histogram_id], undefined); + do_check_eq(Telemetry.histogramSnapshots[test_expired_id], undefined); + do_check_eq(Telemetry.histogramSnapshots[clone_id], undefined); + do_check_eq(rh[test_expired_id], undefined); +}); - // Runtime created histograms should not be recorded. - h = Telemetry.newHistogram("test::runtime_created_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN); - orig = h.snapshot(); - h.add(1); - Assert.equal(orig.sum, h.snapshot().sum, - "Histograms should be equal after recording."); - - // Check that extended histograms are recorded when required. - Telemetry.canRecordExtended = true; +add_task(function* test_keyed_histogram() { + // Check that invalid names get rejected. - h.add(1); - Assert.equal(orig.sum + 1, h.snapshot().sum, - "Runtime histogram value should have incremented by 1 due to recording."); - - h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN"); - orig = h.snapshot(); - h.add(1); - Assert.equal(orig.sum + 1, h.snapshot().sum, - "Histogram value should have incremented by 1 due to recording."); + let threw = false; + try { + Telemetry.newKeyedHistogram("test::invalid # histogram", "never", Telemetry.HISTOGRAM_BOOLEAN); + } catch (e) { + // This should throw as we reject names with the # separator + threw = true; + } + Assert.ok(threw, "newKeyedHistogram should have thrown"); - // Check that base histograms are still being recorded. - h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); - h.clear(); - orig = h.snapshot(); - h.add(1); - Assert.equal(orig.sum + 1, h.snapshot().sum, - "Histogram value should have incremented by 1 due to recording."); -} + threw = false; + try { + Telemetry.getKeyedHistogramById("test::unknown histogram", "never", Telemetry.HISTOGRAM_BOOLEAN); + } catch (e) { + // This should throw as it is an unknown ID + threw = true; + } + Assert.ok(threw, "getKeyedHistogramById should have thrown"); +}); -// Return an array of numbers from lower up to, excluding, upper -function numberRange(lower, upper) -{ - let a = []; - for (let i=lower; i<upper; ++i) { - a.push(i); - } - return a; -} - -function test_keyed_boolean_histogram() -{ +add_task(function* test_keyed_boolean_histogram() { const KEYED_ID = "test::keyed::boolean"; let KEYS = numberRange(0, 2).map(i => "key" + (i + 1)); KEYS.push("漢語"); let histogramBase = { "min": 1, "max": 2, "histogram_type": 2, "sum": 1, @@ -436,20 +523,19 @@ function test_keyed_boolean_histogram() Assert.deepEqual(h.snapshot(), testSnapShot); let allSnapshots = Telemetry.keyedHistogramSnapshots; Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot); h.clear(); Assert.deepEqual(h.keys(), []); Assert.deepEqual(h.snapshot(), {}); -} +}); -function test_keyed_count_histogram() -{ +add_task(function* test_keyed_count_histogram() { const KEYED_ID = "test::keyed::count"; const KEYS = numberRange(0, 5).map(i => "key" + (i + 1)); let histogramBase = { "min": 1, "max": 2, "histogram_type": 4, "sum": 0, "ranges": [0, 1, 2], @@ -492,20 +578,19 @@ function test_keyed_count_histogram() Assert.deepEqual(h.snapshot(), testSnapShot); let allSnapshots = Telemetry.keyedHistogramSnapshots; Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot); h.clear(); Assert.deepEqual(h.keys(), []); Assert.deepEqual(h.snapshot(), {}); -} +}); -function test_keyed_flag_histogram() -{ +add_task(function* test_keyed_flag_histogram() { const KEYED_ID = "test::keyed::flag"; let h = Telemetry.newKeyedHistogram(KEYED_ID, "never", Telemetry.HISTOGRAM_FLAG); const KEY = "default"; h.add(KEY, true); let testSnapshot = {}; testSnapshot[KEY] = { @@ -521,19 +606,19 @@ function test_keyed_flag_histogram() Assert.deepEqual(h.snapshot(), testSnapshot); let allSnapshots = Telemetry.keyedHistogramSnapshots; Assert.deepEqual(allSnapshots[KEYED_ID], testSnapshot); h.clear(); Assert.deepEqual(h.keys(), []); Assert.deepEqual(h.snapshot(), {}); -} +}); -function test_keyed_histogram_recording() { +add_task(function* test_keyed_histogram_recording() { // Check that no histogram is recorded if both base and extended recording are off. Telemetry.canRecordBase = false; Telemetry.canRecordExtended = false; const TEST_KEY = "record_foo"; let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); h.clear(); h.add(TEST_KEY, 1); @@ -571,19 +656,19 @@ function test_keyed_histogram_recording( Assert.equal(h.snapshot(TEST_KEY).sum, 1, "The keyed histogram should record the correct value."); // Check that base histograms are still being recorded. h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); h.clear(); h.add(TEST_KEY, 1); Assert.equal(h.snapshot(TEST_KEY).sum, 1); -} +}); -function test_histogram_recording_enabled() { +add_task(function* test_histogram_recording_enabled() { Telemetry.canRecordBase = true; Telemetry.canRecordExtended = true; // Check that a "normal" histogram respects recording-enabled on/off var h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT"); var orig = h.snapshot(); h.add(1); @@ -620,20 +705,19 @@ function test_histogram_recording_enable Assert.equal(orig.sum + 1, h.snapshot().sum, "When recording is enabled add should record."); // Restore to disabled Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT_INIT_NO_RECORD", false); h.add(1); Assert.equal(orig.sum + 1, h.snapshot().sum, "When recording is disabled add should not record."); +}); -} - -function test_keyed_histogram_recording_enabled() { +add_task(function* test_keyed_histogram_recording_enabled() { Telemetry.canRecordBase = true; Telemetry.canRecordExtended = true; // Check RecordingEnabled for keyed histograms which are recording by default const TEST_KEY = "record_foo"; let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); h.clear(); @@ -665,49 +749,19 @@ function test_keyed_histogram_recording_ Assert.equal(h.snapshot(TEST_KEY).sum, 1, "Keyed histogram add should record when recording is enabled"); // Restore to disabled Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD", false); h.add(TEST_KEY, 1); Assert.equal(h.snapshot(TEST_KEY).sum, 1, "Keyed histogram add should not record when recording is disabled"); -} - -function test_keyed_histogram() { - // Check that invalid names get rejected. - - let threw = false; - try { - Telemetry.newKeyedHistogram("test::invalid # histogram", "never", Telemetry.HISTOGRAM_BOOLEAN); - } catch (e) { - // This should throw as we reject names with the # separator - threw = true; - } - Assert.ok(threw, "newKeyedHistogram should have thrown"); +}); - threw = false; - try { - Telemetry.getKeyedHistogramById("test::unknown histogram", "never", Telemetry.HISTOGRAM_BOOLEAN); - } catch (e) { - // This should throw as it is an unknown ID - threw = true; - } - Assert.ok(threw, "getKeyedHistogramById should have thrown"); - - // Check specific keyed histogram types working properly. - - test_keyed_boolean_histogram(); - test_keyed_count_histogram(); - test_keyed_flag_histogram(); - test_keyed_histogram_recording(); -} - -function test_datasets() -{ +add_task(function* test_datasets() { // Check that datasets work as expected. const RELEASE_CHANNEL_OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT; const RELEASE_CHANNEL_OPTIN = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN; // Histograms should default to the extended dataset let h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG"); Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN); @@ -740,45 +794,22 @@ function test_datasets() registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTIN, []); registered = new Set(registered); Assert.ok(registered.has("TELEMETRY_TEST_KEYED_FLAG")); Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT")); registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTOUT, []); registered = new Set(registered); Assert.ok(!registered.has("TELEMETRY_TEST_KEYED_FLAG")); Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT")); -} - -function test_instantiate() { - if (gIsAndroid) { - // We don't support subsessions yet on Android. - return; - } - - const ID = "TELEMETRY_TEST_COUNT"; - let h = Telemetry.getHistogramById(ID); +}); - // Instantiate the subsession histogram through |add| and make sure they match. - // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise - // |add| will not instantiate the histogram. - h.add(1); - let snapshot = h.snapshot(); - let subsession = Telemetry.snapshotSubsessionHistograms(); - Assert.equal(snapshot.sum, subsession[ID].sum, - "Histogram and subsession histogram sum must match."); - // Clear the histogram, so we don't void the assumptions from the other tests. - h.clear(); -} - -function test_subsession() { - if (gIsAndroid) { - // We don't support subsessions yet on Android. - return; - } - +add_task({ + skip_if: () => gIsAndroid +}, +function* test_subsession() { const ID = "TELEMETRY_TEST_COUNT"; const FLAG = "TELEMETRY_TEST_FLAG"; let h = Telemetry.getHistogramById(ID); let flag = Telemetry.getHistogramById(FLAG); // Both original and duplicate should start out the same. h.clear(); let snapshot = Telemetry.histogramSnapshots; @@ -849,24 +880,22 @@ function test_subsession() { Assert.ok(ID in snapshot); Assert.ok(ID in subsession); Assert.ok(FLAG in snapshot); Assert.ok(FLAG in subsession); Assert.equal(snapshot[ID].sum, 1); Assert.equal(subsession[ID].sum, 0); Assert.equal(snapshot[FLAG].sum, 1); Assert.equal(subsession[FLAG].sum, 0); -} +}); -function test_keyed_subsession() { - if (gIsAndroid) { - // We don't support subsessions yet on Android. - return; - } - +add_task({ + skip_if: () => gIsAndroid +}, +function* test_keyed_subsession() { let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG"); const KEY = "foo"; // Both original and subsession should start out the same. h.clear(); Assert.ok(!(KEY in h.snapshot())); Assert.ok(!(KEY in h.subsessionSnapshot())); Assert.equal(h.snapshot(KEY).sum, 0); @@ -899,54 +928,9 @@ function test_keyed_subsession() { Assert.ok(KEY in snapshot); Assert.ok(KEY in subsession); Assert.equal(snapshot[KEY].sum, 1); Assert.equal(subsession[KEY].sum, 1); subsession = h.subsessionSnapshot(); Assert.ok(!(KEY in subsession)); Assert.equal(h.subsessionSnapshot(KEY).sum, 0); -} - -function generateUUID() { - let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); - // strip {} - return str.substring(1, str.length - 1); -} - -function run_test() -{ - // This MUST be the very first test of this file. - test_instantiate(); - - let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR] - for (let histogram_type of kinds) { - let [min, max, bucket_count] = [1, INT_MAX - 1, 10] - test_histogram(histogram_type, "test::"+histogram_type, min, max, bucket_count); - - const nh = Telemetry.newHistogram; - expect_fail(() => nh("test::min", "never", histogram_type, 0, max, bucket_count)); - expect_fail(() => nh("test::bucket_count", "never", histogram_type, min, max, 1)); - } - - // Instantiate the storage for this histogram and make sure it doesn't - // get reflected into JS, as it has no interesting data in it. - let h = Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT"); - do_check_false("NEWTAB_PAGE_PINNED_SITES_COUNT" in Telemetry.histogramSnapshots); - - test_boolean_histogram(); - test_flag_histogram(); - test_count_histogram(); - test_getHistogramById(); - test_histogramFrom(); - test_getSlowSQL(); - test_getWebrtc(); - test_privateMode(); - test_histogramRecording(); - test_addons(); - test_expired_histogram(); - test_keyed_histogram(); - test_datasets(); - test_subsession(); - test_keyed_subsession(); - test_histogram_recording_enabled(); - test_keyed_histogram_recording_enabled(); -} +});