mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 06:05:44 +00:00
724 lines
23 KiB
JavaScript
724 lines
23 KiB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"];
|
|
|
|
const {interfaces: Ci, utils: Cu} = Components;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
|
|
"resource://gre/modules/UITelemetry.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
|
|
"resource:///modules/RecentWindow.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
|
"resource:///modules/CustomizableUI.jsm");
|
|
XPCOMUtils.defineLazyGetter(this, "Timer", function() {
|
|
let timer = {};
|
|
Cu.import("resource://gre/modules/Timer.jsm", timer);
|
|
return timer;
|
|
});
|
|
|
|
const MS_SECOND = 1000;
|
|
const MS_MINUTE = MS_SECOND * 60;
|
|
const MS_HOUR = MS_MINUTE * 60;
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREA_PLACEMENTS", function() {
|
|
let result = {
|
|
"PanelUI-contents": [
|
|
"edit-controls",
|
|
"zoom-controls",
|
|
"new-window-button",
|
|
"privatebrowsing-button",
|
|
"save-page-button",
|
|
"print-button",
|
|
"history-panelmenu",
|
|
"fullscreen-button",
|
|
"find-button",
|
|
"preferences-button",
|
|
"add-ons-button",
|
|
"developer-button",
|
|
],
|
|
"nav-bar": [
|
|
"urlbar-container",
|
|
"search-container",
|
|
"webrtc-status-button",
|
|
"bookmarks-menu-button",
|
|
"downloads-button",
|
|
"home-button",
|
|
"social-share-button",
|
|
],
|
|
// It's true that toolbar-menubar is not visible
|
|
// on OS X, but the XUL node is definitely present
|
|
// in the document.
|
|
"toolbar-menubar": [
|
|
"menubar-items",
|
|
],
|
|
"TabsToolbar": [
|
|
"tabbrowser-tabs",
|
|
"new-tab-button",
|
|
"alltabs-button",
|
|
"tabs-closebutton",
|
|
],
|
|
"PersonalToolbar": [
|
|
"personal-bookmarks",
|
|
],
|
|
};
|
|
|
|
let showCharacterEncoding = Services.prefs.getComplexValue(
|
|
"browser.menu.showCharacterEncoding",
|
|
Ci.nsIPrefLocalizedString
|
|
).data;
|
|
if (showCharacterEncoding == "true") {
|
|
result["PanelUI-contents"].push("characterencoding-button");
|
|
}
|
|
|
|
if (Services.metro && Services.metro.supported) {
|
|
result["PanelUI-contents"].push("switch-to-metro-button");
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() {
|
|
return Object.keys(DEFAULT_AREA_PLACEMENTS);
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() {
|
|
let result = [
|
|
"open-file-button",
|
|
"developer-button",
|
|
"feed-button",
|
|
"email-link-button",
|
|
"sync-button",
|
|
"tabview-button",
|
|
];
|
|
|
|
let panelPlacements = DEFAULT_AREA_PLACEMENTS["PanelUI-contents"];
|
|
if (panelPlacements.indexOf("characterencoding-button") == -1) {
|
|
result.push("characterencoding-button");
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() {
|
|
let result = [];
|
|
for (let [, buttons] of Iterator(DEFAULT_AREA_PLACEMENTS)) {
|
|
result = result.concat(buttons);
|
|
}
|
|
return result;
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() {
|
|
// These special cases are for click events on built-in items that are
|
|
// contained within customizable items (like the navigation widget).
|
|
const SPECIAL_CASES = [
|
|
"back-button",
|
|
"forward-button",
|
|
"urlbar-stop-button",
|
|
"urlbar-go-button",
|
|
"urlbar-reload-button",
|
|
"searchbar",
|
|
"cut-button",
|
|
"copy-button",
|
|
"paste-button",
|
|
"zoom-out-button",
|
|
"zoom-reset-button",
|
|
"zoom-in-button",
|
|
"BMB_bookmarksPopup",
|
|
"BMB_unsortedBookmarksPopup",
|
|
"BMB_bookmarksToolbarPopup",
|
|
]
|
|
return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
|
|
.concat(SPECIAL_CASES);
|
|
});
|
|
|
|
const OTHER_MOUSEUP_MONITORED_ITEMS = [
|
|
"PlacesChevron",
|
|
"PlacesToolbarItems",
|
|
"menubar-items",
|
|
];
|
|
|
|
// Items that open arrow panels will often be overlapped by
|
|
// the panel that they're opening by the time the mouseup
|
|
// event is fired, so for these items, we monitor mousedown.
|
|
const MOUSEDOWN_MONITORED_ITEMS = [
|
|
"PanelUI-menu-button",
|
|
];
|
|
|
|
// Weakly maps browser windows to objects whose keys are relative
|
|
// timestamps for when some kind of session started. For example,
|
|
// when a customization session started. That way, when the window
|
|
// exits customization mode, we can determine how long the session
|
|
// lasted.
|
|
const WINDOW_DURATION_MAP = new WeakMap();
|
|
|
|
// Default bucket name, when no other bucket is active.
|
|
const BUCKET_DEFAULT = "__DEFAULT__";
|
|
// Bucket prefix, for named buckets.
|
|
const BUCKET_PREFIX = "bucket_";
|
|
// Standard separator to use between different parts of a bucket name, such
|
|
// as primary name and the time step string.
|
|
const BUCKET_SEPARATOR = "|";
|
|
|
|
this.BrowserUITelemetry = {
|
|
init: function() {
|
|
UITelemetry.addSimpleMeasureFunction("toolbars",
|
|
this.getToolbarMeasures.bind(this));
|
|
Services.obs.addObserver(this, "sessionstore-windows-restored", false);
|
|
Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
|
|
CustomizableUI.addListener(this);
|
|
},
|
|
|
|
observe: function(aSubject, aTopic, aData) {
|
|
switch(aTopic) {
|
|
case "sessionstore-windows-restored":
|
|
this._gatherFirstWindowMeasurements();
|
|
break;
|
|
case "browser-delayed-startup-finished":
|
|
this._registerWindow(aSubject);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* For the _countableEvents object, constructs a chain of
|
|
* Javascript Objects with the keys in aKeys, with the final
|
|
* key getting the value in aEndWith. If the final key already
|
|
* exists in the final object, its value is not set. In either
|
|
* case, a reference to the second last object in the chain is
|
|
* returned.
|
|
*
|
|
* Example - suppose I want to store:
|
|
* _countableEvents: {
|
|
* a: {
|
|
* b: {
|
|
* c: 0
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* And then increment the "c" value by 1, you could call this
|
|
* function like this:
|
|
*
|
|
* let example = this._ensureObjectChain([a, b, c], 0);
|
|
* example["c"]++;
|
|
*
|
|
* Subsequent repetitions of these last two lines would
|
|
* simply result in the c value being incremented again
|
|
* and again.
|
|
*
|
|
* @param aKeys the Array of keys to chain Objects together with.
|
|
* @param aEndWith the value to assign to the last key.
|
|
* @returns a reference to the second last object in the chain -
|
|
* so in our example, that'd be "b".
|
|
*/
|
|
_ensureObjectChain: function(aKeys, aEndWith) {
|
|
let current = this._countableEvents;
|
|
let parent = null;
|
|
aKeys.unshift(this._bucket);
|
|
for (let [i, key] of Iterator(aKeys)) {
|
|
if (!(key in current)) {
|
|
if (i == aKeys.length - 1) {
|
|
current[key] = aEndWith;
|
|
} else {
|
|
current[key] = {};
|
|
}
|
|
}
|
|
parent = current;
|
|
current = current[key];
|
|
}
|
|
return parent;
|
|
},
|
|
|
|
_countableEvents: {},
|
|
_countEvent: function(aKeyArray) {
|
|
let countObject = this._ensureObjectChain(aKeyArray, 0);
|
|
let lastItemKey = aKeyArray[aKeyArray.length - 1];
|
|
countObject[lastItemKey]++;
|
|
},
|
|
|
|
_countMouseUpEvent: function(aCategory, aAction, aButton) {
|
|
const BUTTONS = ["left", "middle", "right"];
|
|
let buttonKey = BUTTONS[aButton];
|
|
if (buttonKey) {
|
|
this._countEvent([aCategory, aAction, buttonKey]);
|
|
}
|
|
},
|
|
|
|
_firstWindowMeasurements: null,
|
|
_gatherFirstWindowMeasurements: function() {
|
|
// We'll gather measurements as soon as the session has restored.
|
|
// We do this here instead of waiting for UITelemetry to ask for
|
|
// our measurements because at that point all browser windows have
|
|
// probably been closed, since the vast majority of saved-session
|
|
// pings are gathered during shutdown.
|
|
let win = RecentWindow.getMostRecentBrowserWindow({
|
|
private: false,
|
|
allowPopups: false,
|
|
});
|
|
|
|
// If there are no such windows, we're out of luck. :(
|
|
this._firstWindowMeasurements = win ? this._getWindowMeasurements(win)
|
|
: {};
|
|
},
|
|
|
|
_registerWindow: function(aWindow) {
|
|
aWindow.addEventListener("unload", this);
|
|
let document = aWindow.document;
|
|
|
|
for (let areaID of CustomizableUI.areas) {
|
|
let areaNode = document.getElementById(areaID);
|
|
if (areaNode) {
|
|
(areaNode.customizationTarget || areaNode).addEventListener("mouseup", this);
|
|
}
|
|
}
|
|
|
|
for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
|
|
let item = document.getElementById(itemID);
|
|
if (item) {
|
|
item.addEventListener("mouseup", this);
|
|
}
|
|
}
|
|
|
|
for (let itemID of MOUSEDOWN_MONITORED_ITEMS) {
|
|
let item = document.getElementById(itemID);
|
|
if (item) {
|
|
item.addEventListener("mousedown", this);
|
|
}
|
|
}
|
|
|
|
WINDOW_DURATION_MAP.set(aWindow, {});
|
|
},
|
|
|
|
_unregisterWindow: function(aWindow) {
|
|
aWindow.removeEventListener("unload", this);
|
|
let document = aWindow.document;
|
|
|
|
for (let areaID of CustomizableUI.areas) {
|
|
let areaNode = document.getElementById(areaID);
|
|
if (areaNode) {
|
|
(areaNode.customizationTarget || areaNode).removeEventListener("mouseup", this);
|
|
}
|
|
}
|
|
|
|
for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
|
|
let item = document.getElementById(itemID);
|
|
if (item) {
|
|
item.removeEventListener("mouseup", this);
|
|
}
|
|
}
|
|
|
|
for (let itemID of MOUSEDOWN_MONITORED_ITEMS) {
|
|
let item = document.getElementById(itemID);
|
|
if (item) {
|
|
item.removeEventListener("mousedown", this);
|
|
}
|
|
}
|
|
},
|
|
|
|
handleEvent: function(aEvent) {
|
|
switch(aEvent.type) {
|
|
case "unload":
|
|
this._unregisterWindow(aEvent.currentTarget);
|
|
break;
|
|
case "mouseup":
|
|
this._handleMouseUp(aEvent);
|
|
break;
|
|
case "mousedown":
|
|
this._handleMouseDown(aEvent);
|
|
break;
|
|
}
|
|
},
|
|
|
|
_handleMouseUp: function(aEvent) {
|
|
let targetID = aEvent.currentTarget.id;
|
|
|
|
switch (targetID) {
|
|
case "PlacesToolbarItems":
|
|
this._PlacesToolbarItemsMouseUp(aEvent);
|
|
break;
|
|
case "PlacesChevron":
|
|
this._PlacesChevronMouseUp(aEvent);
|
|
break;
|
|
case "menubar-items":
|
|
this._menubarMouseUp(aEvent);
|
|
break;
|
|
default:
|
|
this._checkForBuiltinItem(aEvent);
|
|
}
|
|
},
|
|
|
|
_handleMouseDown: function(aEvent) {
|
|
if (aEvent.currentTarget.id == "PanelUI-menu-button") {
|
|
// _countMouseUpEvent expects a detail for the second argument,
|
|
// but we don't really have any details to give. Just passing in
|
|
// "button" is probably simpler than trying to modify
|
|
// _countMouseUpEvent for this particular case.
|
|
this._countMouseUpEvent("click-menu-button", "button", aEvent.button);
|
|
}
|
|
},
|
|
|
|
_PlacesChevronMouseUp: function(aEvent) {
|
|
let target = aEvent.originalTarget;
|
|
let result = target.id == "PlacesChevron" ? "chevron" : "overflowed-item";
|
|
this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
|
|
},
|
|
|
|
_PlacesToolbarItemsMouseUp: function(aEvent) {
|
|
let target = aEvent.originalTarget;
|
|
// If this isn't a bookmark-item, we don't care about it.
|
|
if (!target.classList.contains("bookmark-item")) {
|
|
return;
|
|
}
|
|
|
|
let result = target.hasAttribute("container") ? "container" : "item";
|
|
this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
|
|
},
|
|
|
|
_menubarMouseUp: function(aEvent) {
|
|
let target = aEvent.originalTarget;
|
|
let tag = target.localName
|
|
let result = (tag == "menu" || tag == "menuitem") ? tag : "other";
|
|
this._countMouseUpEvent("click-menubar", result, aEvent.button);
|
|
},
|
|
|
|
_bookmarksMenuButtonMouseUp: function(aEvent) {
|
|
let bookmarksWidget = CustomizableUI.getWidget("bookmarks-menu-button");
|
|
if (bookmarksWidget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
|
|
// In the menu panel, only the star is visible, and that opens up the
|
|
// bookmarks subview.
|
|
this._countMouseUpEvent("click-bookmarks-menu-button", "in-panel",
|
|
aEvent.button);
|
|
} else {
|
|
let clickedItem = aEvent.originalTarget;
|
|
// Did we click on the star, or the dropmarker? The star
|
|
// has an anonid of "button". If we don't find that, we'll
|
|
// assume we clicked on the dropmarker.
|
|
let action = "menu";
|
|
if (clickedItem.getAttribute("anonid") == "button") {
|
|
// We clicked on the star - now we just need to record
|
|
// whether or not we're adding a bookmark or editing an
|
|
// existing one.
|
|
let bookmarksMenuNode =
|
|
bookmarksWidget.forWindow(aEvent.target.ownerGlobal).node;
|
|
action = bookmarksMenuNode.hasAttribute("starred") ? "edit" : "add";
|
|
}
|
|
this._countMouseUpEvent("click-bookmarks-menu-button", action,
|
|
aEvent.button);
|
|
}
|
|
},
|
|
|
|
_checkForBuiltinItem: function(aEvent) {
|
|
let item = aEvent.originalTarget;
|
|
|
|
// We special-case the bookmarks-menu-button, since we want to
|
|
// monitor more than just clicks on it.
|
|
if (item.id == "bookmarks-menu-button" ||
|
|
getIDBasedOnFirstIDedAncestor(item) == "bookmarks-menu-button") {
|
|
this._bookmarksMenuButtonMouseUp(aEvent);
|
|
return;
|
|
}
|
|
|
|
// Perhaps we're seeing one of the default toolbar items
|
|
// being clicked.
|
|
if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) {
|
|
// Base case - we clicked directly on one of our built-in items,
|
|
// and we can go ahead and register that click.
|
|
this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button);
|
|
return;
|
|
}
|
|
|
|
// If not, we need to check if one of the ancestors of the clicked
|
|
// item is in our list of built-in items to check.
|
|
let candidate = getIDBasedOnFirstIDedAncestor(item);
|
|
if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) {
|
|
this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button);
|
|
}
|
|
},
|
|
|
|
_getWindowMeasurements: function(aWindow) {
|
|
let document = aWindow.document;
|
|
let result = {};
|
|
|
|
// Determine if the window is in the maximized, normal or
|
|
// fullscreen state.
|
|
result.sizemode = document.documentElement.getAttribute("sizemode");
|
|
|
|
// Determine if the Bookmarks bar is currently visible
|
|
let bookmarksBar = document.getElementById("PersonalToolbar");
|
|
result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed;
|
|
|
|
// Determine if the menubar is currently visible. On OS X, the menubar
|
|
// is never shown, despite not having the collapsed attribute set.
|
|
let menuBar = document.getElementById("toolbar-menubar");
|
|
result.menuBarEnabled =
|
|
menuBar && Services.appinfo.OS != "Darwin"
|
|
&& menuBar.getAttribute("autohide") != "true";
|
|
|
|
// Determine if the titlebar is currently visible.
|
|
result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar");
|
|
|
|
// Examine all customizable areas and see what default items
|
|
// are present and missing.
|
|
let defaultKept = [];
|
|
let defaultMoved = [];
|
|
let nondefaultAdded = [];
|
|
|
|
for (let areaID of CustomizableUI.areas) {
|
|
let items = CustomizableUI.getWidgetIdsInArea(areaID);
|
|
for (let item of items) {
|
|
// Is this a default item?
|
|
if (DEFAULT_ITEMS.indexOf(item) != -1) {
|
|
// Ok, it's a default item - but is it in its default
|
|
// toolbar? We use Array.isArray instead of checking for
|
|
// toolbarID in DEFAULT_AREA_PLACEMENTS because an add-on might
|
|
// be clever and give itself the id of "toString" or something.
|
|
if (Array.isArray(DEFAULT_AREA_PLACEMENTS[areaID]) &&
|
|
DEFAULT_AREA_PLACEMENTS[areaID].indexOf(item) != -1) {
|
|
// The item is in its default toolbar
|
|
defaultKept.push(item);
|
|
} else {
|
|
defaultMoved.push(item);
|
|
}
|
|
} else if (PALETTE_ITEMS.indexOf(item) != -1) {
|
|
// It's a palette item that's been moved into a toolbar
|
|
nondefaultAdded.push(item);
|
|
}
|
|
// else, it's provided by an add-on, and we won't record it.
|
|
}
|
|
}
|
|
|
|
// Now go through the items in the palette to see what default
|
|
// items are in there.
|
|
let paletteItems =
|
|
CustomizableUI.getUnusedWidgets(aWindow.gNavToolbox.palette);
|
|
let defaultRemoved = [item.id for (item of paletteItems)
|
|
if (DEFAULT_ITEMS.indexOf(item.id) != -1)];
|
|
|
|
result.defaultKept = defaultKept;
|
|
result.defaultMoved = defaultMoved;
|
|
result.nondefaultAdded = nondefaultAdded;
|
|
result.defaultRemoved = defaultRemoved;
|
|
|
|
// Next, determine how many add-on provided toolbars exist.
|
|
let addonToolbars = 0;
|
|
let toolbars = document.querySelectorAll("toolbar[customizable=true]");
|
|
for (let toolbar of toolbars) {
|
|
if (DEFAULT_AREAS.indexOf(toolbar.id) == -1) {
|
|
addonToolbars++;
|
|
}
|
|
}
|
|
result.addonToolbars = addonToolbars;
|
|
|
|
// Find out how many open tabs we have in each window
|
|
let winEnumerator = Services.wm.getEnumerator("navigator:browser");
|
|
let visibleTabs = [];
|
|
let hiddenTabs = [];
|
|
while (winEnumerator.hasMoreElements()) {
|
|
let someWin = winEnumerator.getNext();
|
|
if (someWin.gBrowser) {
|
|
let visibleTabsNum = someWin.gBrowser.visibleTabs.length;
|
|
visibleTabs.push(visibleTabsNum);
|
|
hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum);
|
|
}
|
|
}
|
|
result.visibleTabs = visibleTabs;
|
|
result.hiddenTabs = hiddenTabs;
|
|
|
|
return result;
|
|
},
|
|
|
|
getToolbarMeasures: function() {
|
|
let result = this._firstWindowMeasurements || {};
|
|
result.countableEvents = this._countableEvents;
|
|
result.durations = this._durations;
|
|
return result;
|
|
},
|
|
|
|
countCustomizationEvent: function(aEventType) {
|
|
this._countEvent(["customize", aEventType]);
|
|
},
|
|
|
|
_durations: {
|
|
customization: [],
|
|
},
|
|
|
|
onCustomizeStart: function(aWindow) {
|
|
this._countEvent(["customize", "start"]);
|
|
let durationMap = WINDOW_DURATION_MAP.get(aWindow);
|
|
if (!durationMap) {
|
|
durationMap = {};
|
|
WINDOW_DURATION_MAP.set(aWindow, durationMap);
|
|
}
|
|
|
|
durationMap.customization = {
|
|
start: aWindow.performance.now(),
|
|
bucket: this._bucket,
|
|
};
|
|
},
|
|
|
|
onCustomizeEnd: function(aWindow) {
|
|
let durationMap = WINDOW_DURATION_MAP.get(aWindow);
|
|
if (durationMap && "customization" in durationMap) {
|
|
let duration = aWindow.performance.now() - durationMap.customization.start;
|
|
this._durations.customization.push({
|
|
duration: duration,
|
|
bucket: durationMap.customization.bucket,
|
|
});
|
|
delete durationMap.customization;
|
|
}
|
|
},
|
|
|
|
_bucket: BUCKET_DEFAULT,
|
|
_bucketTimer: null,
|
|
|
|
/**
|
|
* Default bucket name, when no other bucket is active.
|
|
*/
|
|
get BUCKET_DEFAULT() BUCKET_DEFAULT,
|
|
|
|
/**
|
|
* Bucket prefix, for named buckets.
|
|
*/
|
|
get BUCKET_PREFIX() BUCKET_PREFIX,
|
|
|
|
/**
|
|
* Standard separator to use between different parts of a bucket name, such
|
|
* as primary name and the time step string.
|
|
*/
|
|
get BUCKET_SEPARATOR() BUCKET_SEPARATOR,
|
|
|
|
get currentBucket() {
|
|
return this._bucket;
|
|
},
|
|
|
|
/**
|
|
* Sets a named bucket for all countable events and select durections to be
|
|
* put into.
|
|
*
|
|
* @param aName Name of bucket, or null for default bucket name (__DEFAULT__)
|
|
*/
|
|
setBucket: function(aName) {
|
|
if (this._bucketTimer) {
|
|
Timer.clearTimeout(this._bucketTimer);
|
|
this._bucketTimer = null;
|
|
}
|
|
|
|
if (aName)
|
|
this._bucket = BUCKET_PREFIX + aName;
|
|
else
|
|
this._bucket = BUCKET_DEFAULT;
|
|
},
|
|
|
|
/**
|
|
* Sets a bucket that expires at the rate of a given series of time steps.
|
|
* Once the bucket expires, the current bucket will automatically revert to
|
|
* the default bucket. While the bucket is expiring, it's name is postfixed
|
|
* by '|' followed by a short string representation of the time step it's
|
|
* currently in.
|
|
* If any other bucket (expiring or normal) is set while an expiring bucket is
|
|
* still expiring, the old expiring bucket stops expiring and the new bucket
|
|
* immediately takes over.
|
|
*
|
|
* @param aName Name of bucket.
|
|
* @param aTimeSteps An array of times in milliseconds to count up to before
|
|
* reverting back to the default bucket. The array of times
|
|
* is expected to be pre-sorted in ascending order.
|
|
* For example, given a bucket name of 'bucket', the times:
|
|
* [60000, 300000, 600000]
|
|
* will result in the following buckets:
|
|
* * bucket|1m - for the first 1 minute
|
|
* * bucket|5m - for the following 4 minutes
|
|
* (until 5 minutes after the start)
|
|
* * bucket|10m - for the following 5 minutes
|
|
* (until 10 minutes after the start)
|
|
* * __DEFAULT__ - until a new bucket is set
|
|
* @param aTimeOffset Time offset, in milliseconds, from which to start
|
|
* counting. For example, if the first time step is 1000ms,
|
|
* and the time offset is 300ms, then the next time step
|
|
* will become active after 700ms. This affects all
|
|
* following time steps also, meaning they will also all be
|
|
* timed as though they started expiring 300ms before
|
|
* setExpiringBucket was called.
|
|
*/
|
|
setExpiringBucket: function(aName, aTimeSteps, aTimeOffset = 0) {
|
|
if (aTimeSteps.length === 0) {
|
|
this.setBucket(null);
|
|
return;
|
|
}
|
|
|
|
if (this._bucketTimer) {
|
|
Timer.clearTimeout(this._bucketTimer);
|
|
this._bucketTimer = null;
|
|
}
|
|
|
|
// Make a copy of the time steps array, so we can safely modify it without
|
|
// modifying the original array that external code has passed to us.
|
|
let steps = [...aTimeSteps];
|
|
let msec = steps.shift();
|
|
let postfix = this._toTimeStr(msec);
|
|
this.setBucket(aName + BUCKET_SEPARATOR + postfix);
|
|
|
|
this._bucketTimer = Timer.setTimeout(() => {
|
|
this._bucketTimer = null;
|
|
this.setExpiringBucket(aName, steps, aTimeOffset + msec);
|
|
}, msec - aTimeOffset);
|
|
},
|
|
|
|
/**
|
|
* Formats a time interval, in milliseconds, to a minimal non-localized string
|
|
* representation. Format is: 'h' for hours, 'm' for minutes, 's' for seconds,
|
|
* 'ms' for milliseconds.
|
|
* Examples:
|
|
* 65 => 65ms
|
|
* 1000 => 1s
|
|
* 60000 => 1m
|
|
* 61000 => 1m01s
|
|
*
|
|
* @param aTimeMS Time in milliseconds
|
|
*
|
|
* @return Minimal string representation.
|
|
*/
|
|
_toTimeStr: function(aTimeMS) {
|
|
let timeStr = "";
|
|
|
|
function reduce(aUnitLength, aSymbol) {
|
|
if (aTimeMS >= aUnitLength) {
|
|
let units = Math.floor(aTimeMS / aUnitLength);
|
|
aTimeMS = aTimeMS - (units * aUnitLength)
|
|
timeStr += units + aSymbol;
|
|
}
|
|
}
|
|
|
|
reduce(MS_HOUR, "h");
|
|
reduce(MS_MINUTE, "m");
|
|
reduce(MS_SECOND, "s");
|
|
reduce(1, "ms");
|
|
|
|
return timeStr;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Returns the id of the first ancestor of aNode that has an id. If aNode
|
|
* has no parent, or no ancestor has an id, returns null.
|
|
*
|
|
* @param aNode the node to find the first ID'd ancestor of
|
|
*/
|
|
function getIDBasedOnFirstIDedAncestor(aNode) {
|
|
while (!aNode.id) {
|
|
aNode = aNode.parentNode;
|
|
if (!aNode) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return aNode.id;
|
|
}
|