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

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

mercurial