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