1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/modules/BrowserUITelemetry.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,729 @@ 1.4 +// This Source Code Form is subject to the terms of the Mozilla Public 1.5 +// License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +// file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"]; 1.11 + 1.12 +const {interfaces: Ci, utils: Cu} = Components; 1.13 + 1.14 +Cu.import("resource://gre/modules/Services.jsm"); 1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.16 + 1.17 +XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", 1.18 + "resource://gre/modules/UITelemetry.jsm"); 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", 1.20 + "resource:///modules/RecentWindow.jsm"); 1.21 +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", 1.22 + "resource:///modules/CustomizableUI.jsm"); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "UITour", 1.24 + "resource:///modules/UITour.jsm"); 1.25 +XPCOMUtils.defineLazyGetter(this, "Timer", function() { 1.26 + let timer = {}; 1.27 + Cu.import("resource://gre/modules/Timer.jsm", timer); 1.28 + return timer; 1.29 +}); 1.30 + 1.31 +const MS_SECOND = 1000; 1.32 +const MS_MINUTE = MS_SECOND * 60; 1.33 +const MS_HOUR = MS_MINUTE * 60; 1.34 + 1.35 +XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREA_PLACEMENTS", function() { 1.36 + let result = { 1.37 + "PanelUI-contents": [ 1.38 + "edit-controls", 1.39 + "zoom-controls", 1.40 + "new-window-button", 1.41 + "privatebrowsing-button", 1.42 + "save-page-button", 1.43 + "print-button", 1.44 + "history-panelmenu", 1.45 + "fullscreen-button", 1.46 + "find-button", 1.47 + "preferences-button", 1.48 + "add-ons-button", 1.49 + "developer-button", 1.50 + ], 1.51 + "nav-bar": [ 1.52 + "urlbar-container", 1.53 + "search-container", 1.54 + "webrtc-status-button", 1.55 + "bookmarks-menu-button", 1.56 + "downloads-button", 1.57 + "home-button", 1.58 + "social-share-button", 1.59 + ], 1.60 + // It's true that toolbar-menubar is not visible 1.61 + // on OS X, but the XUL node is definitely present 1.62 + // in the document. 1.63 + "toolbar-menubar": [ 1.64 + "menubar-items", 1.65 + ], 1.66 + "TabsToolbar": [ 1.67 + "tabbrowser-tabs", 1.68 + "new-tab-button", 1.69 + "alltabs-button", 1.70 + ], 1.71 + "PersonalToolbar": [ 1.72 + "personal-bookmarks", 1.73 + ], 1.74 + }; 1.75 + 1.76 + let showCharacterEncoding = Services.prefs.getComplexValue( 1.77 + "browser.menu.showCharacterEncoding", 1.78 + Ci.nsIPrefLocalizedString 1.79 + ).data; 1.80 + if (showCharacterEncoding == "true") { 1.81 + result["PanelUI-contents"].push("characterencoding-button"); 1.82 + } 1.83 + 1.84 + if (Services.metro && Services.metro.supported) { 1.85 + result["PanelUI-contents"].push("switch-to-metro-button"); 1.86 + } 1.87 + 1.88 + return result; 1.89 +}); 1.90 + 1.91 +XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() { 1.92 + return Object.keys(DEFAULT_AREA_PLACEMENTS); 1.93 +}); 1.94 + 1.95 +XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() { 1.96 + let result = [ 1.97 + "open-file-button", 1.98 + "developer-button", 1.99 + "feed-button", 1.100 + "email-link-button", 1.101 + "sync-button", 1.102 + "tabview-button", 1.103 + ]; 1.104 + 1.105 + let panelPlacements = DEFAULT_AREA_PLACEMENTS["PanelUI-contents"]; 1.106 + if (panelPlacements.indexOf("characterencoding-button") == -1) { 1.107 + result.push("characterencoding-button"); 1.108 + } 1.109 + 1.110 + return result; 1.111 +}); 1.112 + 1.113 +XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() { 1.114 + let result = []; 1.115 + for (let [, buttons] of Iterator(DEFAULT_AREA_PLACEMENTS)) { 1.116 + result = result.concat(buttons); 1.117 + } 1.118 + return result; 1.119 +}); 1.120 + 1.121 +XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() { 1.122 + // These special cases are for click events on built-in items that are 1.123 + // contained within customizable items (like the navigation widget). 1.124 + const SPECIAL_CASES = [ 1.125 + "back-button", 1.126 + "forward-button", 1.127 + "urlbar-stop-button", 1.128 + "urlbar-go-button", 1.129 + "urlbar-reload-button", 1.130 + "searchbar", 1.131 + "cut-button", 1.132 + "copy-button", 1.133 + "paste-button", 1.134 + "zoom-out-button", 1.135 + "zoom-reset-button", 1.136 + "zoom-in-button", 1.137 + "BMB_bookmarksPopup", 1.138 + "BMB_unsortedBookmarksPopup", 1.139 + "BMB_bookmarksToolbarPopup", 1.140 + ] 1.141 + return DEFAULT_ITEMS.concat(PALETTE_ITEMS) 1.142 + .concat(SPECIAL_CASES); 1.143 +}); 1.144 + 1.145 +const OTHER_MOUSEUP_MONITORED_ITEMS = [ 1.146 + "PlacesChevron", 1.147 + "PlacesToolbarItems", 1.148 + "menubar-items", 1.149 +]; 1.150 + 1.151 +// Items that open arrow panels will often be overlapped by 1.152 +// the panel that they're opening by the time the mouseup 1.153 +// event is fired, so for these items, we monitor mousedown. 1.154 +const MOUSEDOWN_MONITORED_ITEMS = [ 1.155 + "PanelUI-menu-button", 1.156 +]; 1.157 + 1.158 +// Weakly maps browser windows to objects whose keys are relative 1.159 +// timestamps for when some kind of session started. For example, 1.160 +// when a customization session started. That way, when the window 1.161 +// exits customization mode, we can determine how long the session 1.162 +// lasted. 1.163 +const WINDOW_DURATION_MAP = new WeakMap(); 1.164 + 1.165 +// Default bucket name, when no other bucket is active. 1.166 +const BUCKET_DEFAULT = "__DEFAULT__"; 1.167 +// Bucket prefix, for named buckets. 1.168 +const BUCKET_PREFIX = "bucket_"; 1.169 +// Standard separator to use between different parts of a bucket name, such 1.170 +// as primary name and the time step string. 1.171 +const BUCKET_SEPARATOR = "|"; 1.172 + 1.173 +this.BrowserUITelemetry = { 1.174 + init: function() { 1.175 + UITelemetry.addSimpleMeasureFunction("toolbars", 1.176 + this.getToolbarMeasures.bind(this)); 1.177 + // Ensure that UITour.jsm remains lazy-loaded, yet always registers its 1.178 + // simple measure function with UITelemetry. 1.179 + UITelemetry.addSimpleMeasureFunction("UITour", 1.180 + () => UITour.getTelemetry()); 1.181 + 1.182 + Services.obs.addObserver(this, "sessionstore-windows-restored", false); 1.183 + Services.obs.addObserver(this, "browser-delayed-startup-finished", false); 1.184 + CustomizableUI.addListener(this); 1.185 + }, 1.186 + 1.187 + observe: function(aSubject, aTopic, aData) { 1.188 + switch(aTopic) { 1.189 + case "sessionstore-windows-restored": 1.190 + this._gatherFirstWindowMeasurements(); 1.191 + break; 1.192 + case "browser-delayed-startup-finished": 1.193 + this._registerWindow(aSubject); 1.194 + break; 1.195 + } 1.196 + }, 1.197 + 1.198 + /** 1.199 + * For the _countableEvents object, constructs a chain of 1.200 + * Javascript Objects with the keys in aKeys, with the final 1.201 + * key getting the value in aEndWith. If the final key already 1.202 + * exists in the final object, its value is not set. In either 1.203 + * case, a reference to the second last object in the chain is 1.204 + * returned. 1.205 + * 1.206 + * Example - suppose I want to store: 1.207 + * _countableEvents: { 1.208 + * a: { 1.209 + * b: { 1.210 + * c: 0 1.211 + * } 1.212 + * } 1.213 + * } 1.214 + * 1.215 + * And then increment the "c" value by 1, you could call this 1.216 + * function like this: 1.217 + * 1.218 + * let example = this._ensureObjectChain([a, b, c], 0); 1.219 + * example["c"]++; 1.220 + * 1.221 + * Subsequent repetitions of these last two lines would 1.222 + * simply result in the c value being incremented again 1.223 + * and again. 1.224 + * 1.225 + * @param aKeys the Array of keys to chain Objects together with. 1.226 + * @param aEndWith the value to assign to the last key. 1.227 + * @returns a reference to the second last object in the chain - 1.228 + * so in our example, that'd be "b". 1.229 + */ 1.230 + _ensureObjectChain: function(aKeys, aEndWith) { 1.231 + let current = this._countableEvents; 1.232 + let parent = null; 1.233 + aKeys.unshift(this._bucket); 1.234 + for (let [i, key] of Iterator(aKeys)) { 1.235 + if (!(key in current)) { 1.236 + if (i == aKeys.length - 1) { 1.237 + current[key] = aEndWith; 1.238 + } else { 1.239 + current[key] = {}; 1.240 + } 1.241 + } 1.242 + parent = current; 1.243 + current = current[key]; 1.244 + } 1.245 + return parent; 1.246 + }, 1.247 + 1.248 + _countableEvents: {}, 1.249 + _countEvent: function(aKeyArray) { 1.250 + let countObject = this._ensureObjectChain(aKeyArray, 0); 1.251 + let lastItemKey = aKeyArray[aKeyArray.length - 1]; 1.252 + countObject[lastItemKey]++; 1.253 + }, 1.254 + 1.255 + _countMouseUpEvent: function(aCategory, aAction, aButton) { 1.256 + const BUTTONS = ["left", "middle", "right"]; 1.257 + let buttonKey = BUTTONS[aButton]; 1.258 + if (buttonKey) { 1.259 + this._countEvent([aCategory, aAction, buttonKey]); 1.260 + } 1.261 + }, 1.262 + 1.263 + _firstWindowMeasurements: null, 1.264 + _gatherFirstWindowMeasurements: function() { 1.265 + // We'll gather measurements as soon as the session has restored. 1.266 + // We do this here instead of waiting for UITelemetry to ask for 1.267 + // our measurements because at that point all browser windows have 1.268 + // probably been closed, since the vast majority of saved-session 1.269 + // pings are gathered during shutdown. 1.270 + let win = RecentWindow.getMostRecentBrowserWindow({ 1.271 + private: false, 1.272 + allowPopups: false, 1.273 + }); 1.274 + 1.275 + // If there are no such windows, we're out of luck. :( 1.276 + this._firstWindowMeasurements = win ? this._getWindowMeasurements(win) 1.277 + : {}; 1.278 + }, 1.279 + 1.280 + _registerWindow: function(aWindow) { 1.281 + aWindow.addEventListener("unload", this); 1.282 + let document = aWindow.document; 1.283 + 1.284 + for (let areaID of CustomizableUI.areas) { 1.285 + let areaNode = document.getElementById(areaID); 1.286 + if (areaNode) { 1.287 + (areaNode.customizationTarget || areaNode).addEventListener("mouseup", this); 1.288 + } 1.289 + } 1.290 + 1.291 + for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) { 1.292 + let item = document.getElementById(itemID); 1.293 + if (item) { 1.294 + item.addEventListener("mouseup", this); 1.295 + } 1.296 + } 1.297 + 1.298 + for (let itemID of MOUSEDOWN_MONITORED_ITEMS) { 1.299 + let item = document.getElementById(itemID); 1.300 + if (item) { 1.301 + item.addEventListener("mousedown", this); 1.302 + } 1.303 + } 1.304 + 1.305 + WINDOW_DURATION_MAP.set(aWindow, {}); 1.306 + }, 1.307 + 1.308 + _unregisterWindow: function(aWindow) { 1.309 + aWindow.removeEventListener("unload", this); 1.310 + let document = aWindow.document; 1.311 + 1.312 + for (let areaID of CustomizableUI.areas) { 1.313 + let areaNode = document.getElementById(areaID); 1.314 + if (areaNode) { 1.315 + (areaNode.customizationTarget || areaNode).removeEventListener("mouseup", this); 1.316 + } 1.317 + } 1.318 + 1.319 + for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) { 1.320 + let item = document.getElementById(itemID); 1.321 + if (item) { 1.322 + item.removeEventListener("mouseup", this); 1.323 + } 1.324 + } 1.325 + 1.326 + for (let itemID of MOUSEDOWN_MONITORED_ITEMS) { 1.327 + let item = document.getElementById(itemID); 1.328 + if (item) { 1.329 + item.removeEventListener("mousedown", this); 1.330 + } 1.331 + } 1.332 + }, 1.333 + 1.334 + handleEvent: function(aEvent) { 1.335 + switch(aEvent.type) { 1.336 + case "unload": 1.337 + this._unregisterWindow(aEvent.currentTarget); 1.338 + break; 1.339 + case "mouseup": 1.340 + this._handleMouseUp(aEvent); 1.341 + break; 1.342 + case "mousedown": 1.343 + this._handleMouseDown(aEvent); 1.344 + break; 1.345 + } 1.346 + }, 1.347 + 1.348 + _handleMouseUp: function(aEvent) { 1.349 + let targetID = aEvent.currentTarget.id; 1.350 + 1.351 + switch (targetID) { 1.352 + case "PlacesToolbarItems": 1.353 + this._PlacesToolbarItemsMouseUp(aEvent); 1.354 + break; 1.355 + case "PlacesChevron": 1.356 + this._PlacesChevronMouseUp(aEvent); 1.357 + break; 1.358 + case "menubar-items": 1.359 + this._menubarMouseUp(aEvent); 1.360 + break; 1.361 + default: 1.362 + this._checkForBuiltinItem(aEvent); 1.363 + } 1.364 + }, 1.365 + 1.366 + _handleMouseDown: function(aEvent) { 1.367 + if (aEvent.currentTarget.id == "PanelUI-menu-button") { 1.368 + // _countMouseUpEvent expects a detail for the second argument, 1.369 + // but we don't really have any details to give. Just passing in 1.370 + // "button" is probably simpler than trying to modify 1.371 + // _countMouseUpEvent for this particular case. 1.372 + this._countMouseUpEvent("click-menu-button", "button", aEvent.button); 1.373 + } 1.374 + }, 1.375 + 1.376 + _PlacesChevronMouseUp: function(aEvent) { 1.377 + let target = aEvent.originalTarget; 1.378 + let result = target.id == "PlacesChevron" ? "chevron" : "overflowed-item"; 1.379 + this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button); 1.380 + }, 1.381 + 1.382 + _PlacesToolbarItemsMouseUp: function(aEvent) { 1.383 + let target = aEvent.originalTarget; 1.384 + // If this isn't a bookmark-item, we don't care about it. 1.385 + if (!target.classList.contains("bookmark-item")) { 1.386 + return; 1.387 + } 1.388 + 1.389 + let result = target.hasAttribute("container") ? "container" : "item"; 1.390 + this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button); 1.391 + }, 1.392 + 1.393 + _menubarMouseUp: function(aEvent) { 1.394 + let target = aEvent.originalTarget; 1.395 + let tag = target.localName 1.396 + let result = (tag == "menu" || tag == "menuitem") ? tag : "other"; 1.397 + this._countMouseUpEvent("click-menubar", result, aEvent.button); 1.398 + }, 1.399 + 1.400 + _bookmarksMenuButtonMouseUp: function(aEvent) { 1.401 + let bookmarksWidget = CustomizableUI.getWidget("bookmarks-menu-button"); 1.402 + if (bookmarksWidget.areaType == CustomizableUI.TYPE_MENU_PANEL) { 1.403 + // In the menu panel, only the star is visible, and that opens up the 1.404 + // bookmarks subview. 1.405 + this._countMouseUpEvent("click-bookmarks-menu-button", "in-panel", 1.406 + aEvent.button); 1.407 + } else { 1.408 + let clickedItem = aEvent.originalTarget; 1.409 + // Did we click on the star, or the dropmarker? The star 1.410 + // has an anonid of "button". If we don't find that, we'll 1.411 + // assume we clicked on the dropmarker. 1.412 + let action = "menu"; 1.413 + if (clickedItem.getAttribute("anonid") == "button") { 1.414 + // We clicked on the star - now we just need to record 1.415 + // whether or not we're adding a bookmark or editing an 1.416 + // existing one. 1.417 + let bookmarksMenuNode = 1.418 + bookmarksWidget.forWindow(aEvent.target.ownerGlobal).node; 1.419 + action = bookmarksMenuNode.hasAttribute("starred") ? "edit" : "add"; 1.420 + } 1.421 + this._countMouseUpEvent("click-bookmarks-menu-button", action, 1.422 + aEvent.button); 1.423 + } 1.424 + }, 1.425 + 1.426 + _checkForBuiltinItem: function(aEvent) { 1.427 + let item = aEvent.originalTarget; 1.428 + 1.429 + // We special-case the bookmarks-menu-button, since we want to 1.430 + // monitor more than just clicks on it. 1.431 + if (item.id == "bookmarks-menu-button" || 1.432 + getIDBasedOnFirstIDedAncestor(item) == "bookmarks-menu-button") { 1.433 + this._bookmarksMenuButtonMouseUp(aEvent); 1.434 + return; 1.435 + } 1.436 + 1.437 + // Perhaps we're seeing one of the default toolbar items 1.438 + // being clicked. 1.439 + if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) { 1.440 + // Base case - we clicked directly on one of our built-in items, 1.441 + // and we can go ahead and register that click. 1.442 + this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button); 1.443 + return; 1.444 + } 1.445 + 1.446 + // If not, we need to check if one of the ancestors of the clicked 1.447 + // item is in our list of built-in items to check. 1.448 + let candidate = getIDBasedOnFirstIDedAncestor(item); 1.449 + if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) { 1.450 + this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button); 1.451 + } 1.452 + }, 1.453 + 1.454 + _getWindowMeasurements: function(aWindow) { 1.455 + let document = aWindow.document; 1.456 + let result = {}; 1.457 + 1.458 + // Determine if the window is in the maximized, normal or 1.459 + // fullscreen state. 1.460 + result.sizemode = document.documentElement.getAttribute("sizemode"); 1.461 + 1.462 + // Determine if the Bookmarks bar is currently visible 1.463 + let bookmarksBar = document.getElementById("PersonalToolbar"); 1.464 + result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed; 1.465 + 1.466 + // Determine if the menubar is currently visible. On OS X, the menubar 1.467 + // is never shown, despite not having the collapsed attribute set. 1.468 + let menuBar = document.getElementById("toolbar-menubar"); 1.469 + result.menuBarEnabled = 1.470 + menuBar && Services.appinfo.OS != "Darwin" 1.471 + && menuBar.getAttribute("autohide") != "true"; 1.472 + 1.473 + // Determine if the titlebar is currently visible. 1.474 + result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar"); 1.475 + 1.476 + // Examine all customizable areas and see what default items 1.477 + // are present and missing. 1.478 + let defaultKept = []; 1.479 + let defaultMoved = []; 1.480 + let nondefaultAdded = []; 1.481 + 1.482 + for (let areaID of CustomizableUI.areas) { 1.483 + let items = CustomizableUI.getWidgetIdsInArea(areaID); 1.484 + for (let item of items) { 1.485 + // Is this a default item? 1.486 + if (DEFAULT_ITEMS.indexOf(item) != -1) { 1.487 + // Ok, it's a default item - but is it in its default 1.488 + // toolbar? We use Array.isArray instead of checking for 1.489 + // toolbarID in DEFAULT_AREA_PLACEMENTS because an add-on might 1.490 + // be clever and give itself the id of "toString" or something. 1.491 + if (Array.isArray(DEFAULT_AREA_PLACEMENTS[areaID]) && 1.492 + DEFAULT_AREA_PLACEMENTS[areaID].indexOf(item) != -1) { 1.493 + // The item is in its default toolbar 1.494 + defaultKept.push(item); 1.495 + } else { 1.496 + defaultMoved.push(item); 1.497 + } 1.498 + } else if (PALETTE_ITEMS.indexOf(item) != -1) { 1.499 + // It's a palette item that's been moved into a toolbar 1.500 + nondefaultAdded.push(item); 1.501 + } 1.502 + // else, it's provided by an add-on, and we won't record it. 1.503 + } 1.504 + } 1.505 + 1.506 + // Now go through the items in the palette to see what default 1.507 + // items are in there. 1.508 + let paletteItems = 1.509 + CustomizableUI.getUnusedWidgets(aWindow.gNavToolbox.palette); 1.510 + let defaultRemoved = [item.id for (item of paletteItems) 1.511 + if (DEFAULT_ITEMS.indexOf(item.id) != -1)]; 1.512 + 1.513 + result.defaultKept = defaultKept; 1.514 + result.defaultMoved = defaultMoved; 1.515 + result.nondefaultAdded = nondefaultAdded; 1.516 + result.defaultRemoved = defaultRemoved; 1.517 + 1.518 + // Next, determine how many add-on provided toolbars exist. 1.519 + let addonToolbars = 0; 1.520 + let toolbars = document.querySelectorAll("toolbar[customizable=true]"); 1.521 + for (let toolbar of toolbars) { 1.522 + if (DEFAULT_AREAS.indexOf(toolbar.id) == -1) { 1.523 + addonToolbars++; 1.524 + } 1.525 + } 1.526 + result.addonToolbars = addonToolbars; 1.527 + 1.528 + // Find out how many open tabs we have in each window 1.529 + let winEnumerator = Services.wm.getEnumerator("navigator:browser"); 1.530 + let visibleTabs = []; 1.531 + let hiddenTabs = []; 1.532 + while (winEnumerator.hasMoreElements()) { 1.533 + let someWin = winEnumerator.getNext(); 1.534 + if (someWin.gBrowser) { 1.535 + let visibleTabsNum = someWin.gBrowser.visibleTabs.length; 1.536 + visibleTabs.push(visibleTabsNum); 1.537 + hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum); 1.538 + } 1.539 + } 1.540 + result.visibleTabs = visibleTabs; 1.541 + result.hiddenTabs = hiddenTabs; 1.542 + 1.543 + return result; 1.544 + }, 1.545 + 1.546 + getToolbarMeasures: function() { 1.547 + let result = this._firstWindowMeasurements || {}; 1.548 + result.countableEvents = this._countableEvents; 1.549 + result.durations = this._durations; 1.550 + return result; 1.551 + }, 1.552 + 1.553 + countCustomizationEvent: function(aEventType) { 1.554 + this._countEvent(["customize", aEventType]); 1.555 + }, 1.556 + 1.557 + _durations: { 1.558 + customization: [], 1.559 + }, 1.560 + 1.561 + onCustomizeStart: function(aWindow) { 1.562 + this._countEvent(["customize", "start"]); 1.563 + let durationMap = WINDOW_DURATION_MAP.get(aWindow); 1.564 + if (!durationMap) { 1.565 + durationMap = {}; 1.566 + WINDOW_DURATION_MAP.set(aWindow, durationMap); 1.567 + } 1.568 + 1.569 + durationMap.customization = { 1.570 + start: aWindow.performance.now(), 1.571 + bucket: this._bucket, 1.572 + }; 1.573 + }, 1.574 + 1.575 + onCustomizeEnd: function(aWindow) { 1.576 + let durationMap = WINDOW_DURATION_MAP.get(aWindow); 1.577 + if (durationMap && "customization" in durationMap) { 1.578 + let duration = aWindow.performance.now() - durationMap.customization.start; 1.579 + this._durations.customization.push({ 1.580 + duration: duration, 1.581 + bucket: durationMap.customization.bucket, 1.582 + }); 1.583 + delete durationMap.customization; 1.584 + } 1.585 + }, 1.586 + 1.587 + _bucket: BUCKET_DEFAULT, 1.588 + _bucketTimer: null, 1.589 + 1.590 + /** 1.591 + * Default bucket name, when no other bucket is active. 1.592 + */ 1.593 + get BUCKET_DEFAULT() BUCKET_DEFAULT, 1.594 + 1.595 + /** 1.596 + * Bucket prefix, for named buckets. 1.597 + */ 1.598 + get BUCKET_PREFIX() BUCKET_PREFIX, 1.599 + 1.600 + /** 1.601 + * Standard separator to use between different parts of a bucket name, such 1.602 + * as primary name and the time step string. 1.603 + */ 1.604 + get BUCKET_SEPARATOR() BUCKET_SEPARATOR, 1.605 + 1.606 + get currentBucket() { 1.607 + return this._bucket; 1.608 + }, 1.609 + 1.610 + /** 1.611 + * Sets a named bucket for all countable events and select durections to be 1.612 + * put into. 1.613 + * 1.614 + * @param aName Name of bucket, or null for default bucket name (__DEFAULT__) 1.615 + */ 1.616 + setBucket: function(aName) { 1.617 + if (this._bucketTimer) { 1.618 + Timer.clearTimeout(this._bucketTimer); 1.619 + this._bucketTimer = null; 1.620 + } 1.621 + 1.622 + if (aName) 1.623 + this._bucket = BUCKET_PREFIX + aName; 1.624 + else 1.625 + this._bucket = BUCKET_DEFAULT; 1.626 + }, 1.627 + 1.628 + /** 1.629 + * Sets a bucket that expires at the rate of a given series of time steps. 1.630 + * Once the bucket expires, the current bucket will automatically revert to 1.631 + * the default bucket. While the bucket is expiring, it's name is postfixed 1.632 + * by '|' followed by a short string representation of the time step it's 1.633 + * currently in. 1.634 + * If any other bucket (expiring or normal) is set while an expiring bucket is 1.635 + * still expiring, the old expiring bucket stops expiring and the new bucket 1.636 + * immediately takes over. 1.637 + * 1.638 + * @param aName Name of bucket. 1.639 + * @param aTimeSteps An array of times in milliseconds to count up to before 1.640 + * reverting back to the default bucket. The array of times 1.641 + * is expected to be pre-sorted in ascending order. 1.642 + * For example, given a bucket name of 'bucket', the times: 1.643 + * [60000, 300000, 600000] 1.644 + * will result in the following buckets: 1.645 + * * bucket|1m - for the first 1 minute 1.646 + * * bucket|5m - for the following 4 minutes 1.647 + * (until 5 minutes after the start) 1.648 + * * bucket|10m - for the following 5 minutes 1.649 + * (until 10 minutes after the start) 1.650 + * * __DEFAULT__ - until a new bucket is set 1.651 + * @param aTimeOffset Time offset, in milliseconds, from which to start 1.652 + * counting. For example, if the first time step is 1000ms, 1.653 + * and the time offset is 300ms, then the next time step 1.654 + * will become active after 700ms. This affects all 1.655 + * following time steps also, meaning they will also all be 1.656 + * timed as though they started expiring 300ms before 1.657 + * setExpiringBucket was called. 1.658 + */ 1.659 + setExpiringBucket: function(aName, aTimeSteps, aTimeOffset = 0) { 1.660 + if (aTimeSteps.length === 0) { 1.661 + this.setBucket(null); 1.662 + return; 1.663 + } 1.664 + 1.665 + if (this._bucketTimer) { 1.666 + Timer.clearTimeout(this._bucketTimer); 1.667 + this._bucketTimer = null; 1.668 + } 1.669 + 1.670 + // Make a copy of the time steps array, so we can safely modify it without 1.671 + // modifying the original array that external code has passed to us. 1.672 + let steps = [...aTimeSteps]; 1.673 + let msec = steps.shift(); 1.674 + let postfix = this._toTimeStr(msec); 1.675 + this.setBucket(aName + BUCKET_SEPARATOR + postfix); 1.676 + 1.677 + this._bucketTimer = Timer.setTimeout(() => { 1.678 + this._bucketTimer = null; 1.679 + this.setExpiringBucket(aName, steps, aTimeOffset + msec); 1.680 + }, msec - aTimeOffset); 1.681 + }, 1.682 + 1.683 + /** 1.684 + * Formats a time interval, in milliseconds, to a minimal non-localized string 1.685 + * representation. Format is: 'h' for hours, 'm' for minutes, 's' for seconds, 1.686 + * 'ms' for milliseconds. 1.687 + * Examples: 1.688 + * 65 => 65ms 1.689 + * 1000 => 1s 1.690 + * 60000 => 1m 1.691 + * 61000 => 1m01s 1.692 + * 1.693 + * @param aTimeMS Time in milliseconds 1.694 + * 1.695 + * @return Minimal string representation. 1.696 + */ 1.697 + _toTimeStr: function(aTimeMS) { 1.698 + let timeStr = ""; 1.699 + 1.700 + function reduce(aUnitLength, aSymbol) { 1.701 + if (aTimeMS >= aUnitLength) { 1.702 + let units = Math.floor(aTimeMS / aUnitLength); 1.703 + aTimeMS = aTimeMS - (units * aUnitLength) 1.704 + timeStr += units + aSymbol; 1.705 + } 1.706 + } 1.707 + 1.708 + reduce(MS_HOUR, "h"); 1.709 + reduce(MS_MINUTE, "m"); 1.710 + reduce(MS_SECOND, "s"); 1.711 + reduce(1, "ms"); 1.712 + 1.713 + return timeStr; 1.714 + }, 1.715 +}; 1.716 + 1.717 +/** 1.718 + * Returns the id of the first ancestor of aNode that has an id. If aNode 1.719 + * has no parent, or no ancestor has an id, returns null. 1.720 + * 1.721 + * @param aNode the node to find the first ID'd ancestor of 1.722 + */ 1.723 +function getIDBasedOnFirstIDedAncestor(aNode) { 1.724 + while (!aNode.id) { 1.725 + aNode = aNode.parentNode; 1.726 + if (!aNode) { 1.727 + return null; 1.728 + } 1.729 + } 1.730 + 1.731 + return aNode.id; 1.732 +}