browser/modules/BrowserUITelemetry.jsm

changeset 0
6474c204b198
     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 +}

mercurial