|
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/. |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"]; |
|
8 |
|
9 const {interfaces: Ci, utils: Cu} = Components; |
|
10 |
|
11 Cu.import("resource://gre/modules/Services.jsm"); |
|
12 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
13 |
|
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 }); |
|
27 |
|
28 const MS_SECOND = 1000; |
|
29 const MS_MINUTE = MS_SECOND * 60; |
|
30 const MS_HOUR = MS_MINUTE * 60; |
|
31 |
|
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 }; |
|
72 |
|
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 } |
|
80 |
|
81 if (Services.metro && Services.metro.supported) { |
|
82 result["PanelUI-contents"].push("switch-to-metro-button"); |
|
83 } |
|
84 |
|
85 return result; |
|
86 }); |
|
87 |
|
88 XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() { |
|
89 return Object.keys(DEFAULT_AREA_PLACEMENTS); |
|
90 }); |
|
91 |
|
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 ]; |
|
101 |
|
102 let panelPlacements = DEFAULT_AREA_PLACEMENTS["PanelUI-contents"]; |
|
103 if (panelPlacements.indexOf("characterencoding-button") == -1) { |
|
104 result.push("characterencoding-button"); |
|
105 } |
|
106 |
|
107 return result; |
|
108 }); |
|
109 |
|
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 }); |
|
117 |
|
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 }); |
|
141 |
|
142 const OTHER_MOUSEUP_MONITORED_ITEMS = [ |
|
143 "PlacesChevron", |
|
144 "PlacesToolbarItems", |
|
145 "menubar-items", |
|
146 ]; |
|
147 |
|
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 ]; |
|
154 |
|
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(); |
|
161 |
|
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 = "|"; |
|
169 |
|
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()); |
|
178 |
|
179 Services.obs.addObserver(this, "sessionstore-windows-restored", false); |
|
180 Services.obs.addObserver(this, "browser-delayed-startup-finished", false); |
|
181 CustomizableUI.addListener(this); |
|
182 }, |
|
183 |
|
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 }, |
|
194 |
|
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 }, |
|
244 |
|
245 _countableEvents: {}, |
|
246 _countEvent: function(aKeyArray) { |
|
247 let countObject = this._ensureObjectChain(aKeyArray, 0); |
|
248 let lastItemKey = aKeyArray[aKeyArray.length - 1]; |
|
249 countObject[lastItemKey]++; |
|
250 }, |
|
251 |
|
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 }, |
|
259 |
|
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 }); |
|
271 |
|
272 // If there are no such windows, we're out of luck. :( |
|
273 this._firstWindowMeasurements = win ? this._getWindowMeasurements(win) |
|
274 : {}; |
|
275 }, |
|
276 |
|
277 _registerWindow: function(aWindow) { |
|
278 aWindow.addEventListener("unload", this); |
|
279 let document = aWindow.document; |
|
280 |
|
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 } |
|
287 |
|
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 } |
|
294 |
|
295 for (let itemID of MOUSEDOWN_MONITORED_ITEMS) { |
|
296 let item = document.getElementById(itemID); |
|
297 if (item) { |
|
298 item.addEventListener("mousedown", this); |
|
299 } |
|
300 } |
|
301 |
|
302 WINDOW_DURATION_MAP.set(aWindow, {}); |
|
303 }, |
|
304 |
|
305 _unregisterWindow: function(aWindow) { |
|
306 aWindow.removeEventListener("unload", this); |
|
307 let document = aWindow.document; |
|
308 |
|
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 } |
|
315 |
|
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 } |
|
322 |
|
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 }, |
|
330 |
|
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 }, |
|
344 |
|
345 _handleMouseUp: function(aEvent) { |
|
346 let targetID = aEvent.currentTarget.id; |
|
347 |
|
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 }, |
|
362 |
|
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 }, |
|
372 |
|
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 }, |
|
378 |
|
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 } |
|
385 |
|
386 let result = target.hasAttribute("container") ? "container" : "item"; |
|
387 this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button); |
|
388 }, |
|
389 |
|
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 }, |
|
396 |
|
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 }, |
|
422 |
|
423 _checkForBuiltinItem: function(aEvent) { |
|
424 let item = aEvent.originalTarget; |
|
425 |
|
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 } |
|
433 |
|
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 } |
|
442 |
|
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 }, |
|
450 |
|
451 _getWindowMeasurements: function(aWindow) { |
|
452 let document = aWindow.document; |
|
453 let result = {}; |
|
454 |
|
455 // Determine if the window is in the maximized, normal or |
|
456 // fullscreen state. |
|
457 result.sizemode = document.documentElement.getAttribute("sizemode"); |
|
458 |
|
459 // Determine if the Bookmarks bar is currently visible |
|
460 let bookmarksBar = document.getElementById("PersonalToolbar"); |
|
461 result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed; |
|
462 |
|
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"; |
|
469 |
|
470 // Determine if the titlebar is currently visible. |
|
471 result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar"); |
|
472 |
|
473 // Examine all customizable areas and see what default items |
|
474 // are present and missing. |
|
475 let defaultKept = []; |
|
476 let defaultMoved = []; |
|
477 let nondefaultAdded = []; |
|
478 |
|
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 } |
|
502 |
|
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)]; |
|
509 |
|
510 result.defaultKept = defaultKept; |
|
511 result.defaultMoved = defaultMoved; |
|
512 result.nondefaultAdded = nondefaultAdded; |
|
513 result.defaultRemoved = defaultRemoved; |
|
514 |
|
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; |
|
524 |
|
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; |
|
539 |
|
540 return result; |
|
541 }, |
|
542 |
|
543 getToolbarMeasures: function() { |
|
544 let result = this._firstWindowMeasurements || {}; |
|
545 result.countableEvents = this._countableEvents; |
|
546 result.durations = this._durations; |
|
547 return result; |
|
548 }, |
|
549 |
|
550 countCustomizationEvent: function(aEventType) { |
|
551 this._countEvent(["customize", aEventType]); |
|
552 }, |
|
553 |
|
554 _durations: { |
|
555 customization: [], |
|
556 }, |
|
557 |
|
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 } |
|
565 |
|
566 durationMap.customization = { |
|
567 start: aWindow.performance.now(), |
|
568 bucket: this._bucket, |
|
569 }; |
|
570 }, |
|
571 |
|
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 }, |
|
583 |
|
584 _bucket: BUCKET_DEFAULT, |
|
585 _bucketTimer: null, |
|
586 |
|
587 /** |
|
588 * Default bucket name, when no other bucket is active. |
|
589 */ |
|
590 get BUCKET_DEFAULT() BUCKET_DEFAULT, |
|
591 |
|
592 /** |
|
593 * Bucket prefix, for named buckets. |
|
594 */ |
|
595 get BUCKET_PREFIX() BUCKET_PREFIX, |
|
596 |
|
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, |
|
602 |
|
603 get currentBucket() { |
|
604 return this._bucket; |
|
605 }, |
|
606 |
|
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 } |
|
618 |
|
619 if (aName) |
|
620 this._bucket = BUCKET_PREFIX + aName; |
|
621 else |
|
622 this._bucket = BUCKET_DEFAULT; |
|
623 }, |
|
624 |
|
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 } |
|
661 |
|
662 if (this._bucketTimer) { |
|
663 Timer.clearTimeout(this._bucketTimer); |
|
664 this._bucketTimer = null; |
|
665 } |
|
666 |
|
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); |
|
673 |
|
674 this._bucketTimer = Timer.setTimeout(() => { |
|
675 this._bucketTimer = null; |
|
676 this.setExpiringBucket(aName, steps, aTimeOffset + msec); |
|
677 }, msec - aTimeOffset); |
|
678 }, |
|
679 |
|
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 = ""; |
|
696 |
|
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 } |
|
704 |
|
705 reduce(MS_HOUR, "h"); |
|
706 reduce(MS_MINUTE, "m"); |
|
707 reduce(MS_SECOND, "s"); |
|
708 reduce(1, "ms"); |
|
709 |
|
710 return timeStr; |
|
711 }, |
|
712 }; |
|
713 |
|
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 } |
|
727 |
|
728 return aNode.id; |
|
729 } |