browser/modules/BrowserUITelemetry.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 // This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 // License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"];
michael@0 8
michael@0 9 const {interfaces: Ci, utils: Cu} = Components;
michael@0 10
michael@0 11 Cu.import("resource://gre/modules/Services.jsm");
michael@0 12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 13
michael@0 14 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
michael@0 15 "resource://gre/modules/UITelemetry.jsm");
michael@0 16 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
michael@0 17 "resource:///modules/RecentWindow.jsm");
michael@0 18 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
michael@0 19 "resource:///modules/CustomizableUI.jsm");
michael@0 20 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
michael@0 21 "resource:///modules/UITour.jsm");
michael@0 22 XPCOMUtils.defineLazyGetter(this, "Timer", function() {
michael@0 23 let timer = {};
michael@0 24 Cu.import("resource://gre/modules/Timer.jsm", timer);
michael@0 25 return timer;
michael@0 26 });
michael@0 27
michael@0 28 const MS_SECOND = 1000;
michael@0 29 const MS_MINUTE = MS_SECOND * 60;
michael@0 30 const MS_HOUR = MS_MINUTE * 60;
michael@0 31
michael@0 32 XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREA_PLACEMENTS", function() {
michael@0 33 let result = {
michael@0 34 "PanelUI-contents": [
michael@0 35 "edit-controls",
michael@0 36 "zoom-controls",
michael@0 37 "new-window-button",
michael@0 38 "privatebrowsing-button",
michael@0 39 "save-page-button",
michael@0 40 "print-button",
michael@0 41 "history-panelmenu",
michael@0 42 "fullscreen-button",
michael@0 43 "find-button",
michael@0 44 "preferences-button",
michael@0 45 "add-ons-button",
michael@0 46 "developer-button",
michael@0 47 ],
michael@0 48 "nav-bar": [
michael@0 49 "urlbar-container",
michael@0 50 "search-container",
michael@0 51 "webrtc-status-button",
michael@0 52 "bookmarks-menu-button",
michael@0 53 "downloads-button",
michael@0 54 "home-button",
michael@0 55 "social-share-button",
michael@0 56 ],
michael@0 57 // It's true that toolbar-menubar is not visible
michael@0 58 // on OS X, but the XUL node is definitely present
michael@0 59 // in the document.
michael@0 60 "toolbar-menubar": [
michael@0 61 "menubar-items",
michael@0 62 ],
michael@0 63 "TabsToolbar": [
michael@0 64 "tabbrowser-tabs",
michael@0 65 "new-tab-button",
michael@0 66 "alltabs-button",
michael@0 67 ],
michael@0 68 "PersonalToolbar": [
michael@0 69 "personal-bookmarks",
michael@0 70 ],
michael@0 71 };
michael@0 72
michael@0 73 let showCharacterEncoding = Services.prefs.getComplexValue(
michael@0 74 "browser.menu.showCharacterEncoding",
michael@0 75 Ci.nsIPrefLocalizedString
michael@0 76 ).data;
michael@0 77 if (showCharacterEncoding == "true") {
michael@0 78 result["PanelUI-contents"].push("characterencoding-button");
michael@0 79 }
michael@0 80
michael@0 81 if (Services.metro && Services.metro.supported) {
michael@0 82 result["PanelUI-contents"].push("switch-to-metro-button");
michael@0 83 }
michael@0 84
michael@0 85 return result;
michael@0 86 });
michael@0 87
michael@0 88 XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() {
michael@0 89 return Object.keys(DEFAULT_AREA_PLACEMENTS);
michael@0 90 });
michael@0 91
michael@0 92 XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() {
michael@0 93 let result = [
michael@0 94 "open-file-button",
michael@0 95 "developer-button",
michael@0 96 "feed-button",
michael@0 97 "email-link-button",
michael@0 98 "sync-button",
michael@0 99 "tabview-button",
michael@0 100 ];
michael@0 101
michael@0 102 let panelPlacements = DEFAULT_AREA_PLACEMENTS["PanelUI-contents"];
michael@0 103 if (panelPlacements.indexOf("characterencoding-button") == -1) {
michael@0 104 result.push("characterencoding-button");
michael@0 105 }
michael@0 106
michael@0 107 return result;
michael@0 108 });
michael@0 109
michael@0 110 XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() {
michael@0 111 let result = [];
michael@0 112 for (let [, buttons] of Iterator(DEFAULT_AREA_PLACEMENTS)) {
michael@0 113 result = result.concat(buttons);
michael@0 114 }
michael@0 115 return result;
michael@0 116 });
michael@0 117
michael@0 118 XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() {
michael@0 119 // These special cases are for click events on built-in items that are
michael@0 120 // contained within customizable items (like the navigation widget).
michael@0 121 const SPECIAL_CASES = [
michael@0 122 "back-button",
michael@0 123 "forward-button",
michael@0 124 "urlbar-stop-button",
michael@0 125 "urlbar-go-button",
michael@0 126 "urlbar-reload-button",
michael@0 127 "searchbar",
michael@0 128 "cut-button",
michael@0 129 "copy-button",
michael@0 130 "paste-button",
michael@0 131 "zoom-out-button",
michael@0 132 "zoom-reset-button",
michael@0 133 "zoom-in-button",
michael@0 134 "BMB_bookmarksPopup",
michael@0 135 "BMB_unsortedBookmarksPopup",
michael@0 136 "BMB_bookmarksToolbarPopup",
michael@0 137 ]
michael@0 138 return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
michael@0 139 .concat(SPECIAL_CASES);
michael@0 140 });
michael@0 141
michael@0 142 const OTHER_MOUSEUP_MONITORED_ITEMS = [
michael@0 143 "PlacesChevron",
michael@0 144 "PlacesToolbarItems",
michael@0 145 "menubar-items",
michael@0 146 ];
michael@0 147
michael@0 148 // Items that open arrow panels will often be overlapped by
michael@0 149 // the panel that they're opening by the time the mouseup
michael@0 150 // event is fired, so for these items, we monitor mousedown.
michael@0 151 const MOUSEDOWN_MONITORED_ITEMS = [
michael@0 152 "PanelUI-menu-button",
michael@0 153 ];
michael@0 154
michael@0 155 // Weakly maps browser windows to objects whose keys are relative
michael@0 156 // timestamps for when some kind of session started. For example,
michael@0 157 // when a customization session started. That way, when the window
michael@0 158 // exits customization mode, we can determine how long the session
michael@0 159 // lasted.
michael@0 160 const WINDOW_DURATION_MAP = new WeakMap();
michael@0 161
michael@0 162 // Default bucket name, when no other bucket is active.
michael@0 163 const BUCKET_DEFAULT = "__DEFAULT__";
michael@0 164 // Bucket prefix, for named buckets.
michael@0 165 const BUCKET_PREFIX = "bucket_";
michael@0 166 // Standard separator to use between different parts of a bucket name, such
michael@0 167 // as primary name and the time step string.
michael@0 168 const BUCKET_SEPARATOR = "|";
michael@0 169
michael@0 170 this.BrowserUITelemetry = {
michael@0 171 init: function() {
michael@0 172 UITelemetry.addSimpleMeasureFunction("toolbars",
michael@0 173 this.getToolbarMeasures.bind(this));
michael@0 174 // Ensure that UITour.jsm remains lazy-loaded, yet always registers its
michael@0 175 // simple measure function with UITelemetry.
michael@0 176 UITelemetry.addSimpleMeasureFunction("UITour",
michael@0 177 () => UITour.getTelemetry());
michael@0 178
michael@0 179 Services.obs.addObserver(this, "sessionstore-windows-restored", false);
michael@0 180 Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
michael@0 181 CustomizableUI.addListener(this);
michael@0 182 },
michael@0 183
michael@0 184 observe: function(aSubject, aTopic, aData) {
michael@0 185 switch(aTopic) {
michael@0 186 case "sessionstore-windows-restored":
michael@0 187 this._gatherFirstWindowMeasurements();
michael@0 188 break;
michael@0 189 case "browser-delayed-startup-finished":
michael@0 190 this._registerWindow(aSubject);
michael@0 191 break;
michael@0 192 }
michael@0 193 },
michael@0 194
michael@0 195 /**
michael@0 196 * For the _countableEvents object, constructs a chain of
michael@0 197 * Javascript Objects with the keys in aKeys, with the final
michael@0 198 * key getting the value in aEndWith. If the final key already
michael@0 199 * exists in the final object, its value is not set. In either
michael@0 200 * case, a reference to the second last object in the chain is
michael@0 201 * returned.
michael@0 202 *
michael@0 203 * Example - suppose I want to store:
michael@0 204 * _countableEvents: {
michael@0 205 * a: {
michael@0 206 * b: {
michael@0 207 * c: 0
michael@0 208 * }
michael@0 209 * }
michael@0 210 * }
michael@0 211 *
michael@0 212 * And then increment the "c" value by 1, you could call this
michael@0 213 * function like this:
michael@0 214 *
michael@0 215 * let example = this._ensureObjectChain([a, b, c], 0);
michael@0 216 * example["c"]++;
michael@0 217 *
michael@0 218 * Subsequent repetitions of these last two lines would
michael@0 219 * simply result in the c value being incremented again
michael@0 220 * and again.
michael@0 221 *
michael@0 222 * @param aKeys the Array of keys to chain Objects together with.
michael@0 223 * @param aEndWith the value to assign to the last key.
michael@0 224 * @returns a reference to the second last object in the chain -
michael@0 225 * so in our example, that'd be "b".
michael@0 226 */
michael@0 227 _ensureObjectChain: function(aKeys, aEndWith) {
michael@0 228 let current = this._countableEvents;
michael@0 229 let parent = null;
michael@0 230 aKeys.unshift(this._bucket);
michael@0 231 for (let [i, key] of Iterator(aKeys)) {
michael@0 232 if (!(key in current)) {
michael@0 233 if (i == aKeys.length - 1) {
michael@0 234 current[key] = aEndWith;
michael@0 235 } else {
michael@0 236 current[key] = {};
michael@0 237 }
michael@0 238 }
michael@0 239 parent = current;
michael@0 240 current = current[key];
michael@0 241 }
michael@0 242 return parent;
michael@0 243 },
michael@0 244
michael@0 245 _countableEvents: {},
michael@0 246 _countEvent: function(aKeyArray) {
michael@0 247 let countObject = this._ensureObjectChain(aKeyArray, 0);
michael@0 248 let lastItemKey = aKeyArray[aKeyArray.length - 1];
michael@0 249 countObject[lastItemKey]++;
michael@0 250 },
michael@0 251
michael@0 252 _countMouseUpEvent: function(aCategory, aAction, aButton) {
michael@0 253 const BUTTONS = ["left", "middle", "right"];
michael@0 254 let buttonKey = BUTTONS[aButton];
michael@0 255 if (buttonKey) {
michael@0 256 this._countEvent([aCategory, aAction, buttonKey]);
michael@0 257 }
michael@0 258 },
michael@0 259
michael@0 260 _firstWindowMeasurements: null,
michael@0 261 _gatherFirstWindowMeasurements: function() {
michael@0 262 // We'll gather measurements as soon as the session has restored.
michael@0 263 // We do this here instead of waiting for UITelemetry to ask for
michael@0 264 // our measurements because at that point all browser windows have
michael@0 265 // probably been closed, since the vast majority of saved-session
michael@0 266 // pings are gathered during shutdown.
michael@0 267 let win = RecentWindow.getMostRecentBrowserWindow({
michael@0 268 private: false,
michael@0 269 allowPopups: false,
michael@0 270 });
michael@0 271
michael@0 272 // If there are no such windows, we're out of luck. :(
michael@0 273 this._firstWindowMeasurements = win ? this._getWindowMeasurements(win)
michael@0 274 : {};
michael@0 275 },
michael@0 276
michael@0 277 _registerWindow: function(aWindow) {
michael@0 278 aWindow.addEventListener("unload", this);
michael@0 279 let document = aWindow.document;
michael@0 280
michael@0 281 for (let areaID of CustomizableUI.areas) {
michael@0 282 let areaNode = document.getElementById(areaID);
michael@0 283 if (areaNode) {
michael@0 284 (areaNode.customizationTarget || areaNode).addEventListener("mouseup", this);
michael@0 285 }
michael@0 286 }
michael@0 287
michael@0 288 for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
michael@0 289 let item = document.getElementById(itemID);
michael@0 290 if (item) {
michael@0 291 item.addEventListener("mouseup", this);
michael@0 292 }
michael@0 293 }
michael@0 294
michael@0 295 for (let itemID of MOUSEDOWN_MONITORED_ITEMS) {
michael@0 296 let item = document.getElementById(itemID);
michael@0 297 if (item) {
michael@0 298 item.addEventListener("mousedown", this);
michael@0 299 }
michael@0 300 }
michael@0 301
michael@0 302 WINDOW_DURATION_MAP.set(aWindow, {});
michael@0 303 },
michael@0 304
michael@0 305 _unregisterWindow: function(aWindow) {
michael@0 306 aWindow.removeEventListener("unload", this);
michael@0 307 let document = aWindow.document;
michael@0 308
michael@0 309 for (let areaID of CustomizableUI.areas) {
michael@0 310 let areaNode = document.getElementById(areaID);
michael@0 311 if (areaNode) {
michael@0 312 (areaNode.customizationTarget || areaNode).removeEventListener("mouseup", this);
michael@0 313 }
michael@0 314 }
michael@0 315
michael@0 316 for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
michael@0 317 let item = document.getElementById(itemID);
michael@0 318 if (item) {
michael@0 319 item.removeEventListener("mouseup", this);
michael@0 320 }
michael@0 321 }
michael@0 322
michael@0 323 for (let itemID of MOUSEDOWN_MONITORED_ITEMS) {
michael@0 324 let item = document.getElementById(itemID);
michael@0 325 if (item) {
michael@0 326 item.removeEventListener("mousedown", this);
michael@0 327 }
michael@0 328 }
michael@0 329 },
michael@0 330
michael@0 331 handleEvent: function(aEvent) {
michael@0 332 switch(aEvent.type) {
michael@0 333 case "unload":
michael@0 334 this._unregisterWindow(aEvent.currentTarget);
michael@0 335 break;
michael@0 336 case "mouseup":
michael@0 337 this._handleMouseUp(aEvent);
michael@0 338 break;
michael@0 339 case "mousedown":
michael@0 340 this._handleMouseDown(aEvent);
michael@0 341 break;
michael@0 342 }
michael@0 343 },
michael@0 344
michael@0 345 _handleMouseUp: function(aEvent) {
michael@0 346 let targetID = aEvent.currentTarget.id;
michael@0 347
michael@0 348 switch (targetID) {
michael@0 349 case "PlacesToolbarItems":
michael@0 350 this._PlacesToolbarItemsMouseUp(aEvent);
michael@0 351 break;
michael@0 352 case "PlacesChevron":
michael@0 353 this._PlacesChevronMouseUp(aEvent);
michael@0 354 break;
michael@0 355 case "menubar-items":
michael@0 356 this._menubarMouseUp(aEvent);
michael@0 357 break;
michael@0 358 default:
michael@0 359 this._checkForBuiltinItem(aEvent);
michael@0 360 }
michael@0 361 },
michael@0 362
michael@0 363 _handleMouseDown: function(aEvent) {
michael@0 364 if (aEvent.currentTarget.id == "PanelUI-menu-button") {
michael@0 365 // _countMouseUpEvent expects a detail for the second argument,
michael@0 366 // but we don't really have any details to give. Just passing in
michael@0 367 // "button" is probably simpler than trying to modify
michael@0 368 // _countMouseUpEvent for this particular case.
michael@0 369 this._countMouseUpEvent("click-menu-button", "button", aEvent.button);
michael@0 370 }
michael@0 371 },
michael@0 372
michael@0 373 _PlacesChevronMouseUp: function(aEvent) {
michael@0 374 let target = aEvent.originalTarget;
michael@0 375 let result = target.id == "PlacesChevron" ? "chevron" : "overflowed-item";
michael@0 376 this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
michael@0 377 },
michael@0 378
michael@0 379 _PlacesToolbarItemsMouseUp: function(aEvent) {
michael@0 380 let target = aEvent.originalTarget;
michael@0 381 // If this isn't a bookmark-item, we don't care about it.
michael@0 382 if (!target.classList.contains("bookmark-item")) {
michael@0 383 return;
michael@0 384 }
michael@0 385
michael@0 386 let result = target.hasAttribute("container") ? "container" : "item";
michael@0 387 this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
michael@0 388 },
michael@0 389
michael@0 390 _menubarMouseUp: function(aEvent) {
michael@0 391 let target = aEvent.originalTarget;
michael@0 392 let tag = target.localName
michael@0 393 let result = (tag == "menu" || tag == "menuitem") ? tag : "other";
michael@0 394 this._countMouseUpEvent("click-menubar", result, aEvent.button);
michael@0 395 },
michael@0 396
michael@0 397 _bookmarksMenuButtonMouseUp: function(aEvent) {
michael@0 398 let bookmarksWidget = CustomizableUI.getWidget("bookmarks-menu-button");
michael@0 399 if (bookmarksWidget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
michael@0 400 // In the menu panel, only the star is visible, and that opens up the
michael@0 401 // bookmarks subview.
michael@0 402 this._countMouseUpEvent("click-bookmarks-menu-button", "in-panel",
michael@0 403 aEvent.button);
michael@0 404 } else {
michael@0 405 let clickedItem = aEvent.originalTarget;
michael@0 406 // Did we click on the star, or the dropmarker? The star
michael@0 407 // has an anonid of "button". If we don't find that, we'll
michael@0 408 // assume we clicked on the dropmarker.
michael@0 409 let action = "menu";
michael@0 410 if (clickedItem.getAttribute("anonid") == "button") {
michael@0 411 // We clicked on the star - now we just need to record
michael@0 412 // whether or not we're adding a bookmark or editing an
michael@0 413 // existing one.
michael@0 414 let bookmarksMenuNode =
michael@0 415 bookmarksWidget.forWindow(aEvent.target.ownerGlobal).node;
michael@0 416 action = bookmarksMenuNode.hasAttribute("starred") ? "edit" : "add";
michael@0 417 }
michael@0 418 this._countMouseUpEvent("click-bookmarks-menu-button", action,
michael@0 419 aEvent.button);
michael@0 420 }
michael@0 421 },
michael@0 422
michael@0 423 _checkForBuiltinItem: function(aEvent) {
michael@0 424 let item = aEvent.originalTarget;
michael@0 425
michael@0 426 // We special-case the bookmarks-menu-button, since we want to
michael@0 427 // monitor more than just clicks on it.
michael@0 428 if (item.id == "bookmarks-menu-button" ||
michael@0 429 getIDBasedOnFirstIDedAncestor(item) == "bookmarks-menu-button") {
michael@0 430 this._bookmarksMenuButtonMouseUp(aEvent);
michael@0 431 return;
michael@0 432 }
michael@0 433
michael@0 434 // Perhaps we're seeing one of the default toolbar items
michael@0 435 // being clicked.
michael@0 436 if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) {
michael@0 437 // Base case - we clicked directly on one of our built-in items,
michael@0 438 // and we can go ahead and register that click.
michael@0 439 this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button);
michael@0 440 return;
michael@0 441 }
michael@0 442
michael@0 443 // If not, we need to check if one of the ancestors of the clicked
michael@0 444 // item is in our list of built-in items to check.
michael@0 445 let candidate = getIDBasedOnFirstIDedAncestor(item);
michael@0 446 if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) {
michael@0 447 this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button);
michael@0 448 }
michael@0 449 },
michael@0 450
michael@0 451 _getWindowMeasurements: function(aWindow) {
michael@0 452 let document = aWindow.document;
michael@0 453 let result = {};
michael@0 454
michael@0 455 // Determine if the window is in the maximized, normal or
michael@0 456 // fullscreen state.
michael@0 457 result.sizemode = document.documentElement.getAttribute("sizemode");
michael@0 458
michael@0 459 // Determine if the Bookmarks bar is currently visible
michael@0 460 let bookmarksBar = document.getElementById("PersonalToolbar");
michael@0 461 result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed;
michael@0 462
michael@0 463 // Determine if the menubar is currently visible. On OS X, the menubar
michael@0 464 // is never shown, despite not having the collapsed attribute set.
michael@0 465 let menuBar = document.getElementById("toolbar-menubar");
michael@0 466 result.menuBarEnabled =
michael@0 467 menuBar && Services.appinfo.OS != "Darwin"
michael@0 468 && menuBar.getAttribute("autohide") != "true";
michael@0 469
michael@0 470 // Determine if the titlebar is currently visible.
michael@0 471 result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar");
michael@0 472
michael@0 473 // Examine all customizable areas and see what default items
michael@0 474 // are present and missing.
michael@0 475 let defaultKept = [];
michael@0 476 let defaultMoved = [];
michael@0 477 let nondefaultAdded = [];
michael@0 478
michael@0 479 for (let areaID of CustomizableUI.areas) {
michael@0 480 let items = CustomizableUI.getWidgetIdsInArea(areaID);
michael@0 481 for (let item of items) {
michael@0 482 // Is this a default item?
michael@0 483 if (DEFAULT_ITEMS.indexOf(item) != -1) {
michael@0 484 // Ok, it's a default item - but is it in its default
michael@0 485 // toolbar? We use Array.isArray instead of checking for
michael@0 486 // toolbarID in DEFAULT_AREA_PLACEMENTS because an add-on might
michael@0 487 // be clever and give itself the id of "toString" or something.
michael@0 488 if (Array.isArray(DEFAULT_AREA_PLACEMENTS[areaID]) &&
michael@0 489 DEFAULT_AREA_PLACEMENTS[areaID].indexOf(item) != -1) {
michael@0 490 // The item is in its default toolbar
michael@0 491 defaultKept.push(item);
michael@0 492 } else {
michael@0 493 defaultMoved.push(item);
michael@0 494 }
michael@0 495 } else if (PALETTE_ITEMS.indexOf(item) != -1) {
michael@0 496 // It's a palette item that's been moved into a toolbar
michael@0 497 nondefaultAdded.push(item);
michael@0 498 }
michael@0 499 // else, it's provided by an add-on, and we won't record it.
michael@0 500 }
michael@0 501 }
michael@0 502
michael@0 503 // Now go through the items in the palette to see what default
michael@0 504 // items are in there.
michael@0 505 let paletteItems =
michael@0 506 CustomizableUI.getUnusedWidgets(aWindow.gNavToolbox.palette);
michael@0 507 let defaultRemoved = [item.id for (item of paletteItems)
michael@0 508 if (DEFAULT_ITEMS.indexOf(item.id) != -1)];
michael@0 509
michael@0 510 result.defaultKept = defaultKept;
michael@0 511 result.defaultMoved = defaultMoved;
michael@0 512 result.nondefaultAdded = nondefaultAdded;
michael@0 513 result.defaultRemoved = defaultRemoved;
michael@0 514
michael@0 515 // Next, determine how many add-on provided toolbars exist.
michael@0 516 let addonToolbars = 0;
michael@0 517 let toolbars = document.querySelectorAll("toolbar[customizable=true]");
michael@0 518 for (let toolbar of toolbars) {
michael@0 519 if (DEFAULT_AREAS.indexOf(toolbar.id) == -1) {
michael@0 520 addonToolbars++;
michael@0 521 }
michael@0 522 }
michael@0 523 result.addonToolbars = addonToolbars;
michael@0 524
michael@0 525 // Find out how many open tabs we have in each window
michael@0 526 let winEnumerator = Services.wm.getEnumerator("navigator:browser");
michael@0 527 let visibleTabs = [];
michael@0 528 let hiddenTabs = [];
michael@0 529 while (winEnumerator.hasMoreElements()) {
michael@0 530 let someWin = winEnumerator.getNext();
michael@0 531 if (someWin.gBrowser) {
michael@0 532 let visibleTabsNum = someWin.gBrowser.visibleTabs.length;
michael@0 533 visibleTabs.push(visibleTabsNum);
michael@0 534 hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum);
michael@0 535 }
michael@0 536 }
michael@0 537 result.visibleTabs = visibleTabs;
michael@0 538 result.hiddenTabs = hiddenTabs;
michael@0 539
michael@0 540 return result;
michael@0 541 },
michael@0 542
michael@0 543 getToolbarMeasures: function() {
michael@0 544 let result = this._firstWindowMeasurements || {};
michael@0 545 result.countableEvents = this._countableEvents;
michael@0 546 result.durations = this._durations;
michael@0 547 return result;
michael@0 548 },
michael@0 549
michael@0 550 countCustomizationEvent: function(aEventType) {
michael@0 551 this._countEvent(["customize", aEventType]);
michael@0 552 },
michael@0 553
michael@0 554 _durations: {
michael@0 555 customization: [],
michael@0 556 },
michael@0 557
michael@0 558 onCustomizeStart: function(aWindow) {
michael@0 559 this._countEvent(["customize", "start"]);
michael@0 560 let durationMap = WINDOW_DURATION_MAP.get(aWindow);
michael@0 561 if (!durationMap) {
michael@0 562 durationMap = {};
michael@0 563 WINDOW_DURATION_MAP.set(aWindow, durationMap);
michael@0 564 }
michael@0 565
michael@0 566 durationMap.customization = {
michael@0 567 start: aWindow.performance.now(),
michael@0 568 bucket: this._bucket,
michael@0 569 };
michael@0 570 },
michael@0 571
michael@0 572 onCustomizeEnd: function(aWindow) {
michael@0 573 let durationMap = WINDOW_DURATION_MAP.get(aWindow);
michael@0 574 if (durationMap && "customization" in durationMap) {
michael@0 575 let duration = aWindow.performance.now() - durationMap.customization.start;
michael@0 576 this._durations.customization.push({
michael@0 577 duration: duration,
michael@0 578 bucket: durationMap.customization.bucket,
michael@0 579 });
michael@0 580 delete durationMap.customization;
michael@0 581 }
michael@0 582 },
michael@0 583
michael@0 584 _bucket: BUCKET_DEFAULT,
michael@0 585 _bucketTimer: null,
michael@0 586
michael@0 587 /**
michael@0 588 * Default bucket name, when no other bucket is active.
michael@0 589 */
michael@0 590 get BUCKET_DEFAULT() BUCKET_DEFAULT,
michael@0 591
michael@0 592 /**
michael@0 593 * Bucket prefix, for named buckets.
michael@0 594 */
michael@0 595 get BUCKET_PREFIX() BUCKET_PREFIX,
michael@0 596
michael@0 597 /**
michael@0 598 * Standard separator to use between different parts of a bucket name, such
michael@0 599 * as primary name and the time step string.
michael@0 600 */
michael@0 601 get BUCKET_SEPARATOR() BUCKET_SEPARATOR,
michael@0 602
michael@0 603 get currentBucket() {
michael@0 604 return this._bucket;
michael@0 605 },
michael@0 606
michael@0 607 /**
michael@0 608 * Sets a named bucket for all countable events and select durections to be
michael@0 609 * put into.
michael@0 610 *
michael@0 611 * @param aName Name of bucket, or null for default bucket name (__DEFAULT__)
michael@0 612 */
michael@0 613 setBucket: function(aName) {
michael@0 614 if (this._bucketTimer) {
michael@0 615 Timer.clearTimeout(this._bucketTimer);
michael@0 616 this._bucketTimer = null;
michael@0 617 }
michael@0 618
michael@0 619 if (aName)
michael@0 620 this._bucket = BUCKET_PREFIX + aName;
michael@0 621 else
michael@0 622 this._bucket = BUCKET_DEFAULT;
michael@0 623 },
michael@0 624
michael@0 625 /**
michael@0 626 * Sets a bucket that expires at the rate of a given series of time steps.
michael@0 627 * Once the bucket expires, the current bucket will automatically revert to
michael@0 628 * the default bucket. While the bucket is expiring, it's name is postfixed
michael@0 629 * by '|' followed by a short string representation of the time step it's
michael@0 630 * currently in.
michael@0 631 * If any other bucket (expiring or normal) is set while an expiring bucket is
michael@0 632 * still expiring, the old expiring bucket stops expiring and the new bucket
michael@0 633 * immediately takes over.
michael@0 634 *
michael@0 635 * @param aName Name of bucket.
michael@0 636 * @param aTimeSteps An array of times in milliseconds to count up to before
michael@0 637 * reverting back to the default bucket. The array of times
michael@0 638 * is expected to be pre-sorted in ascending order.
michael@0 639 * For example, given a bucket name of 'bucket', the times:
michael@0 640 * [60000, 300000, 600000]
michael@0 641 * will result in the following buckets:
michael@0 642 * * bucket|1m - for the first 1 minute
michael@0 643 * * bucket|5m - for the following 4 minutes
michael@0 644 * (until 5 minutes after the start)
michael@0 645 * * bucket|10m - for the following 5 minutes
michael@0 646 * (until 10 minutes after the start)
michael@0 647 * * __DEFAULT__ - until a new bucket is set
michael@0 648 * @param aTimeOffset Time offset, in milliseconds, from which to start
michael@0 649 * counting. For example, if the first time step is 1000ms,
michael@0 650 * and the time offset is 300ms, then the next time step
michael@0 651 * will become active after 700ms. This affects all
michael@0 652 * following time steps also, meaning they will also all be
michael@0 653 * timed as though they started expiring 300ms before
michael@0 654 * setExpiringBucket was called.
michael@0 655 */
michael@0 656 setExpiringBucket: function(aName, aTimeSteps, aTimeOffset = 0) {
michael@0 657 if (aTimeSteps.length === 0) {
michael@0 658 this.setBucket(null);
michael@0 659 return;
michael@0 660 }
michael@0 661
michael@0 662 if (this._bucketTimer) {
michael@0 663 Timer.clearTimeout(this._bucketTimer);
michael@0 664 this._bucketTimer = null;
michael@0 665 }
michael@0 666
michael@0 667 // Make a copy of the time steps array, so we can safely modify it without
michael@0 668 // modifying the original array that external code has passed to us.
michael@0 669 let steps = [...aTimeSteps];
michael@0 670 let msec = steps.shift();
michael@0 671 let postfix = this._toTimeStr(msec);
michael@0 672 this.setBucket(aName + BUCKET_SEPARATOR + postfix);
michael@0 673
michael@0 674 this._bucketTimer = Timer.setTimeout(() => {
michael@0 675 this._bucketTimer = null;
michael@0 676 this.setExpiringBucket(aName, steps, aTimeOffset + msec);
michael@0 677 }, msec - aTimeOffset);
michael@0 678 },
michael@0 679
michael@0 680 /**
michael@0 681 * Formats a time interval, in milliseconds, to a minimal non-localized string
michael@0 682 * representation. Format is: 'h' for hours, 'm' for minutes, 's' for seconds,
michael@0 683 * 'ms' for milliseconds.
michael@0 684 * Examples:
michael@0 685 * 65 => 65ms
michael@0 686 * 1000 => 1s
michael@0 687 * 60000 => 1m
michael@0 688 * 61000 => 1m01s
michael@0 689 *
michael@0 690 * @param aTimeMS Time in milliseconds
michael@0 691 *
michael@0 692 * @return Minimal string representation.
michael@0 693 */
michael@0 694 _toTimeStr: function(aTimeMS) {
michael@0 695 let timeStr = "";
michael@0 696
michael@0 697 function reduce(aUnitLength, aSymbol) {
michael@0 698 if (aTimeMS >= aUnitLength) {
michael@0 699 let units = Math.floor(aTimeMS / aUnitLength);
michael@0 700 aTimeMS = aTimeMS - (units * aUnitLength)
michael@0 701 timeStr += units + aSymbol;
michael@0 702 }
michael@0 703 }
michael@0 704
michael@0 705 reduce(MS_HOUR, "h");
michael@0 706 reduce(MS_MINUTE, "m");
michael@0 707 reduce(MS_SECOND, "s");
michael@0 708 reduce(1, "ms");
michael@0 709
michael@0 710 return timeStr;
michael@0 711 },
michael@0 712 };
michael@0 713
michael@0 714 /**
michael@0 715 * Returns the id of the first ancestor of aNode that has an id. If aNode
michael@0 716 * has no parent, or no ancestor has an id, returns null.
michael@0 717 *
michael@0 718 * @param aNode the node to find the first ID'd ancestor of
michael@0 719 */
michael@0 720 function getIDBasedOnFirstIDedAncestor(aNode) {
michael@0 721 while (!aNode.id) {
michael@0 722 aNode = aNode.parentNode;
michael@0 723 if (!aNode) {
michael@0 724 return null;
michael@0 725 }
michael@0 726 }
michael@0 727
michael@0 728 return aNode.id;
michael@0 729 }

mercurial