From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- toolkit/components/telemetry/EventInfo.h | 52 + toolkit/components/telemetry/Events.yaml | 68 + toolkit/components/telemetry/GCTelemetry.jsm | 216 + toolkit/components/telemetry/Histograms.json | 11002 +++++++++++++++++++ toolkit/components/telemetry/Makefile.in | 17 + toolkit/components/telemetry/ProcessedStack.h | 63 + toolkit/components/telemetry/ScalarInfo.h | 27 + toolkit/components/telemetry/Scalars.yaml | 298 + toolkit/components/telemetry/Telemetry.cpp | 3076 ++++++ toolkit/components/telemetry/Telemetry.h | 436 + toolkit/components/telemetry/TelemetryArchive.jsm | 125 + toolkit/components/telemetry/TelemetryCommon.cpp | 105 + toolkit/components/telemetry/TelemetryCommon.h | 75 + toolkit/components/telemetry/TelemetryComms.h | 84 + .../components/telemetry/TelemetryController.jsm | 954 ++ .../components/telemetry/TelemetryEnvironment.jsm | 1459 +++ toolkit/components/telemetry/TelemetryEvent.cpp | 687 ++ toolkit/components/telemetry/TelemetryEvent.h | 39 + .../components/telemetry/TelemetryHistogram.cpp | 2725 +++++ toolkit/components/telemetry/TelemetryHistogram.h | 104 + toolkit/components/telemetry/TelemetryLog.jsm | 35 + .../telemetry/TelemetryReportingPolicy.jsm | 496 + toolkit/components/telemetry/TelemetryScalar.cpp | 1896 ++++ toolkit/components/telemetry/TelemetryScalar.h | 64 + toolkit/components/telemetry/TelemetrySend.jsm | 1114 ++ toolkit/components/telemetry/TelemetrySession.jsm | 2124 ++++ toolkit/components/telemetry/TelemetryStartup.js | 49 + .../components/telemetry/TelemetryStartup.manifest | 4 + .../components/telemetry/TelemetryStopwatch.jsm | 335 + toolkit/components/telemetry/TelemetryStorage.jsm | 1882 ++++ .../components/telemetry/TelemetryTimestamps.jsm | 54 + toolkit/components/telemetry/TelemetryUtils.jsm | 152 + .../components/telemetry/ThirdPartyCookieProbe.jsm | 181 + toolkit/components/telemetry/ThreadHangStats.h | 230 + toolkit/components/telemetry/UITelemetry.jsm | 235 + toolkit/components/telemetry/WebrtcTelemetry.cpp | 112 + toolkit/components/telemetry/WebrtcTelemetry.h | 43 + .../components/telemetry/datareporting-prefs.js | 12 + .../telemetry/docs/collection/custom-pings.rst | 74 + .../telemetry/docs/collection/histograms.rst | 5 + .../components/telemetry/docs/collection/index.rst | 35 + .../telemetry/docs/collection/measuring-time.rst | 74 + .../telemetry/docs/collection/scalars.rst | 140 + .../telemetry/docs/concepts/archiving.rst | 12 + .../components/telemetry/docs/concepts/crashes.rst | 23 + .../components/telemetry/docs/concepts/index.rst | 23 + .../components/telemetry/docs/concepts/pings.rst | 32 + .../telemetry/docs/concepts/sessions.rst | 40 + .../telemetry/docs/concepts/submission.rst | 34 + .../docs/concepts/subsession_triggers.png | Bin 0 -> 1219295 bytes .../telemetry/docs/data/addons-malware-ping.rst | 42 + .../components/telemetry/docs/data/common-ping.rst | 42 + .../components/telemetry/docs/data/core-ping.rst | 191 + .../components/telemetry/docs/data/crash-ping.rst | 144 + .../telemetry/docs/data/deletion-ping.rst | 19 + .../components/telemetry/docs/data/environment.rst | 373 + .../telemetry/docs/data/heartbeat-ping.rst | 63 + toolkit/components/telemetry/docs/data/index.rst | 18 + .../components/telemetry/docs/data/main-ping.rst | 609 + .../components/telemetry/docs/data/sync-ping.rst | 182 + .../components/telemetry/docs/data/uitour-ping.rst | 26 + .../components/telemetry/docs/fhr/architecture.rst | 226 + .../components/telemetry/docs/fhr/dataformat.rst | 1997 ++++ .../components/telemetry/docs/fhr/identifiers.rst | 83 + toolkit/components/telemetry/docs/fhr/index.rst | 34 + toolkit/components/telemetry/docs/index.rst | 25 + .../components/telemetry/docs/internals/index.rst | 9 + .../telemetry/docs/internals/preferences.rst | 119 + toolkit/components/telemetry/gen-event-data.py | 142 + toolkit/components/telemetry/gen-event-enum.py | 73 + .../telemetry/gen-histogram-bucket-ranges.py | 52 + toolkit/components/telemetry/gen-histogram-data.py | 178 + toolkit/components/telemetry/gen-histogram-enum.py | 107 + toolkit/components/telemetry/gen-scalar-data.py | 90 + toolkit/components/telemetry/gen-scalar-enum.py | 56 + toolkit/components/telemetry/healthreport-prefs.js | 10 + .../components/telemetry/histogram-whitelists.json | 1990 ++++ toolkit/components/telemetry/histogram_tools.py | 513 + toolkit/components/telemetry/moz.build | 130 + toolkit/components/telemetry/nsITelemetry.idl | 469 + toolkit/components/telemetry/parse_events.py | 271 + toolkit/components/telemetry/parse_scalars.py | 262 + .../components/telemetry/schemas/core.schema.json | 41 + .../components/telemetry/shared_telemetry_utils.py | 103 + .../telemetry/tests/addons/dictionary/install.rdf | 25 + .../telemetry/tests/addons/experiment/install.rdf | 16 + .../telemetry/tests/addons/extension-2/install.rdf | 16 + .../telemetry/tests/addons/extension/install.rdf | 16 + .../telemetry/tests/addons/long-fields/install.rdf | 24 + .../telemetry/tests/addons/restartless/install.rdf | 24 + .../tests/addons/signed/META-INF/manifest.mf | 7 + .../tests/addons/signed/META-INF/mozilla.rsa | Bin 0 -> 4190 bytes .../tests/addons/signed/META-INF/mozilla.sf | 4 + .../telemetry/tests/addons/signed/install.rdf | 24 + .../telemetry/tests/addons/system/install.rdf | 24 + .../telemetry/tests/addons/theme/install.rdf | 16 + .../components/telemetry/tests/browser/browser.ini | 5 + .../telemetry/tests/browser/browser_TelemetryGC.js | 193 + .../telemetry/tests/search/chrome.manifest | 3 + .../telemetry/tests/search/searchTest.jar | Bin 0 -> 867 bytes .../components/telemetry/tests/unit/.eslintrc.js | 7 + .../tests/unit/TelemetryArchiveTesting.jsm | 86 + toolkit/components/telemetry/tests/unit/engine.xml | 7 + toolkit/components/telemetry/tests/unit/head.js | 319 + .../telemetry/tests/unit/test_ChildHistograms.js | 107 + .../telemetry/tests/unit/test_PingAPI.js | 502 + .../tests/unit/test_SubsessionChaining.js | 236 + .../tests/unit/test_TelemetryController.js | 507 + .../tests/unit/test_TelemetryControllerBuildID.js | 70 + .../tests/unit/test_TelemetryControllerShutdown.js | 70 + .../tests/unit/test_TelemetryController_idle.js | 73 + .../tests/unit/test_TelemetryEnvironment.js | 1528 +++ .../telemetry/tests/unit/test_TelemetryEvents.js | 249 + .../tests/unit/test_TelemetryFlagClear.js | 14 + .../tests/unit/test_TelemetryLateWrites.js | 127 + .../tests/unit/test_TelemetryLockCount.js | 53 + .../telemetry/tests/unit/test_TelemetryLog.js | 51 + .../tests/unit/test_TelemetryReportingPolicy.js | 268 + .../telemetry/tests/unit/test_TelemetryScalars.js | 574 + .../telemetry/tests/unit/test_TelemetrySend.js | 427 + .../tests/unit/test_TelemetrySendOldPings.js | 547 + .../telemetry/tests/unit/test_TelemetrySession.js | 2029 ++++ .../tests/unit/test_TelemetryStopwatch.js | 156 + .../tests/unit/test_TelemetryTimestamps.js | 77 + .../telemetry/tests/unit/test_ThreadHangStats.js | 102 + .../telemetry/tests/unit/test_nsITelemetry.js | 883 ++ .../components/telemetry/tests/unit/xpcshell.ini | 63 + 127 files changed, 49340 insertions(+) create mode 100644 toolkit/components/telemetry/EventInfo.h create mode 100644 toolkit/components/telemetry/Events.yaml create mode 100644 toolkit/components/telemetry/GCTelemetry.jsm create mode 100644 toolkit/components/telemetry/Histograms.json create mode 100644 toolkit/components/telemetry/Makefile.in create mode 100644 toolkit/components/telemetry/ProcessedStack.h create mode 100644 toolkit/components/telemetry/ScalarInfo.h create mode 100644 toolkit/components/telemetry/Scalars.yaml create mode 100644 toolkit/components/telemetry/Telemetry.cpp create mode 100644 toolkit/components/telemetry/Telemetry.h create mode 100644 toolkit/components/telemetry/TelemetryArchive.jsm create mode 100644 toolkit/components/telemetry/TelemetryCommon.cpp create mode 100644 toolkit/components/telemetry/TelemetryCommon.h create mode 100644 toolkit/components/telemetry/TelemetryComms.h create mode 100644 toolkit/components/telemetry/TelemetryController.jsm create mode 100644 toolkit/components/telemetry/TelemetryEnvironment.jsm create mode 100644 toolkit/components/telemetry/TelemetryEvent.cpp create mode 100644 toolkit/components/telemetry/TelemetryEvent.h create mode 100644 toolkit/components/telemetry/TelemetryHistogram.cpp create mode 100644 toolkit/components/telemetry/TelemetryHistogram.h create mode 100644 toolkit/components/telemetry/TelemetryLog.jsm create mode 100644 toolkit/components/telemetry/TelemetryReportingPolicy.jsm create mode 100644 toolkit/components/telemetry/TelemetryScalar.cpp create mode 100644 toolkit/components/telemetry/TelemetryScalar.h create mode 100644 toolkit/components/telemetry/TelemetrySend.jsm create mode 100644 toolkit/components/telemetry/TelemetrySession.jsm create mode 100644 toolkit/components/telemetry/TelemetryStartup.js create mode 100644 toolkit/components/telemetry/TelemetryStartup.manifest create mode 100644 toolkit/components/telemetry/TelemetryStopwatch.jsm create mode 100644 toolkit/components/telemetry/TelemetryStorage.jsm create mode 100644 toolkit/components/telemetry/TelemetryTimestamps.jsm create mode 100644 toolkit/components/telemetry/TelemetryUtils.jsm create mode 100644 toolkit/components/telemetry/ThirdPartyCookieProbe.jsm create mode 100644 toolkit/components/telemetry/ThreadHangStats.h create mode 100644 toolkit/components/telemetry/UITelemetry.jsm create mode 100644 toolkit/components/telemetry/WebrtcTelemetry.cpp create mode 100644 toolkit/components/telemetry/WebrtcTelemetry.h create mode 100644 toolkit/components/telemetry/datareporting-prefs.js create mode 100644 toolkit/components/telemetry/docs/collection/custom-pings.rst create mode 100644 toolkit/components/telemetry/docs/collection/histograms.rst create mode 100644 toolkit/components/telemetry/docs/collection/index.rst create mode 100644 toolkit/components/telemetry/docs/collection/measuring-time.rst create mode 100644 toolkit/components/telemetry/docs/collection/scalars.rst create mode 100644 toolkit/components/telemetry/docs/concepts/archiving.rst create mode 100644 toolkit/components/telemetry/docs/concepts/crashes.rst create mode 100644 toolkit/components/telemetry/docs/concepts/index.rst create mode 100644 toolkit/components/telemetry/docs/concepts/pings.rst create mode 100644 toolkit/components/telemetry/docs/concepts/sessions.rst create mode 100644 toolkit/components/telemetry/docs/concepts/submission.rst create mode 100644 toolkit/components/telemetry/docs/concepts/subsession_triggers.png create mode 100644 toolkit/components/telemetry/docs/data/addons-malware-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/common-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/core-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/crash-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/deletion-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/environment.rst create mode 100644 toolkit/components/telemetry/docs/data/heartbeat-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/index.rst create mode 100644 toolkit/components/telemetry/docs/data/main-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/sync-ping.rst create mode 100644 toolkit/components/telemetry/docs/data/uitour-ping.rst create mode 100644 toolkit/components/telemetry/docs/fhr/architecture.rst create mode 100644 toolkit/components/telemetry/docs/fhr/dataformat.rst create mode 100644 toolkit/components/telemetry/docs/fhr/identifiers.rst create mode 100644 toolkit/components/telemetry/docs/fhr/index.rst create mode 100644 toolkit/components/telemetry/docs/index.rst create mode 100644 toolkit/components/telemetry/docs/internals/index.rst create mode 100644 toolkit/components/telemetry/docs/internals/preferences.rst create mode 100644 toolkit/components/telemetry/gen-event-data.py create mode 100644 toolkit/components/telemetry/gen-event-enum.py create mode 100644 toolkit/components/telemetry/gen-histogram-bucket-ranges.py create mode 100644 toolkit/components/telemetry/gen-histogram-data.py create mode 100644 toolkit/components/telemetry/gen-histogram-enum.py create mode 100644 toolkit/components/telemetry/gen-scalar-data.py create mode 100644 toolkit/components/telemetry/gen-scalar-enum.py create mode 100644 toolkit/components/telemetry/healthreport-prefs.js create mode 100644 toolkit/components/telemetry/histogram-whitelists.json create mode 100644 toolkit/components/telemetry/histogram_tools.py create mode 100644 toolkit/components/telemetry/moz.build create mode 100644 toolkit/components/telemetry/nsITelemetry.idl create mode 100644 toolkit/components/telemetry/parse_events.py create mode 100644 toolkit/components/telemetry/parse_scalars.py create mode 100644 toolkit/components/telemetry/schemas/core.schema.json create mode 100644 toolkit/components/telemetry/shared_telemetry_utils.py create mode 100644 toolkit/components/telemetry/tests/addons/dictionary/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/experiment/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/extension-2/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/extension/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/long-fields/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/restartless/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/signed/META-INF/manifest.mf create mode 100644 toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.rsa create mode 100644 toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.sf create mode 100644 toolkit/components/telemetry/tests/addons/signed/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/system/install.rdf create mode 100644 toolkit/components/telemetry/tests/addons/theme/install.rdf create mode 100644 toolkit/components/telemetry/tests/browser/browser.ini create mode 100644 toolkit/components/telemetry/tests/browser/browser_TelemetryGC.js create mode 100644 toolkit/components/telemetry/tests/search/chrome.manifest create mode 100644 toolkit/components/telemetry/tests/search/searchTest.jar create mode 100644 toolkit/components/telemetry/tests/unit/.eslintrc.js create mode 100644 toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm create mode 100644 toolkit/components/telemetry/tests/unit/engine.xml create mode 100644 toolkit/components/telemetry/tests/unit/head.js create mode 100644 toolkit/components/telemetry/tests/unit/test_ChildHistograms.js create mode 100644 toolkit/components/telemetry/tests/unit/test_PingAPI.js create mode 100644 toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryController.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryLog.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetrySend.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetrySession.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js create mode 100644 toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js create mode 100644 toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js create mode 100644 toolkit/components/telemetry/tests/unit/test_nsITelemetry.js create mode 100644 toolkit/components/telemetry/tests/unit/xpcshell.ini (limited to 'toolkit/components/telemetry') diff --git a/toolkit/components/telemetry/EventInfo.h b/toolkit/components/telemetry/EventInfo.h new file mode 100644 index 000000000..b8934e2c4 --- /dev/null +++ b/toolkit/components/telemetry/EventInfo.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef TelemetryEventInfo_h__ +#define TelemetryEventInfo_h__ + +// This module is internal to Telemetry. The structures here hold data that +// describe events. +// It should only be used by TelemetryEventData.h and TelemetryEvent.cpp. +// +// For the public interface to Telemetry functionality, see Telemetry.h. + +namespace { + +struct CommonEventInfo { + // Indices for the category and expiration strings. + uint32_t category_offset; + uint32_t expiration_version_offset; + + // The index and count for the extra key offsets in the extra table. + uint32_t extra_index; + uint32_t extra_count; + + // The day since UNIX epoch that this probe expires on. + uint32_t expiration_day; + + // The dataset this event is recorded in. + uint32_t dataset; + + // Convenience functions for accessing event strings. + const char* expiration_version() const; + const char* category() const; + const char* extra_key(uint32_t index) const; +}; + +struct EventInfo { + // The corresponding CommonEventInfo. + const CommonEventInfo& common_info; + + // Indices for the method & object strings. + uint32_t method_offset; + uint32_t object_offset; + + const char* method() const; + const char* object() const; +}; + +} // namespace + +#endif // TelemetryEventInfo_h__ diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml new file mode 100644 index 000000000..750a13914 --- /dev/null +++ b/toolkit/components/telemetry/Events.yaml @@ -0,0 +1,68 @@ +navigation: +- methods: ["search"] + objects: ["about_home", "about_newtab", "contextmenu", "oneoff", + "suggestion", "alias", "enter", "searchbar", "urlbar"] + release_channel_collection: opt-in + description: > + This is recorded on each search navigation. + The value field records the action used to trigger the search: + "enter", "oneoff", "suggestion", "alias", null (for contextmenu) + bug_numbers: [1316281] + notification_emails: ["past@mozilla.com"] + expiry_version: "58.0" + extra_keys: + engine: The id of the search engine used. + +# This category contains event entries used for Telemetry tests. +# They will not be sent out with any pings. +telemetry.test: +- methods: ["test1", "test2"] + objects: ["object1", "object2"] + bug_numbers: [1286606] + notification_emails: ["telemetry-client-dev@mozilla.com"] + description: This is a test entry for Telemetry. + expiry_date: never + extra_keys: + key1: This is just a test description. + key2: This is another test description. +- methods: ["optout"] + objects: ["object1", "object2"] + bug_numbers: [1286606] + notification_emails: ["telemetry-client-dev@mozilla.com"] + description: This is an opt-out test entry. + expiry_date: never + release_channel_collection: opt-out + extra_keys: + key1: This is just a test description. +- methods: ["expired_version"] + objects: ["object1", "object2"] + bug_numbers: [1286606] + notification_emails: ["telemetry-client-dev@mozilla.com"] + description: This is a test entry with an expired version. + expiry_version: "3.6" +- methods: ["expired_date"] + objects: ["object1", "object2"] + bug_numbers: [1286606] + notification_emails: ["telemetry-client-dev@mozilla.com"] + description: This is a test entry with an expired date. + expiry_date: 2014-01-28 +- methods: ["not_expired_optout"] + objects: ["object1"] + bug_numbers: [1286606] + notification_emails: ["telemetry-client-dev@mozilla.com"] + description: This is an opt-out test entry with unexpired date and version. + release_channel_collection: opt-out + expiry_date: 2099-01-01 + expiry_version: "999.0" + +# This is a secondary category used for Telemetry tests. +# The events here will not be sent out with any pings. +telemetry.test.second: +- methods: ["test"] + objects: ["object1", "object2", "object3"] + bug_numbers: [1286606] + notification_emails: ["telemetry-client-dev@mozilla.com"] + description: This is a test entry for Telemetry. + expiry_date: never + extra_keys: + key1: This is just a test description. diff --git a/toolkit/components/telemetry/GCTelemetry.jsm b/toolkit/components/telemetry/GCTelemetry.jsm new file mode 100644 index 000000000..43a4ea9ca --- /dev/null +++ b/toolkit/components/telemetry/GCTelemetry.jsm @@ -0,0 +1,216 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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 module records detailed timing information about selected + * GCs. The data is sent back in the telemetry session ping. To avoid + * bloating the ping, only a few GCs are included. There are two + * selection strategies. We always save the five GCs with the worst + * max_pause time. Additionally, five collections are selected at + * random. If a GC runs for C milliseconds and the total time for all + * GCs since the session began is T milliseconds, then the GC has a + * 5*C/T probablility of being selected (the factor of 5 is because we + * save 5 of them). + * + * GCs from both the main process and all content processes are + * recorded. The data is cleared for each new subsession. + */ + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm", this); + +this.EXPORTED_SYMBOLS = ["GCTelemetry"]; + +// Names of processes where we record GCs. +const PROCESS_NAMES = ["main", "content"]; + +// Should be the time we started up in milliseconds since the epoch. +const BASE_TIME = Date.now() - Services.telemetry.msSinceProcessStart(); + +// Records selected GCs. There is one instance per process type. +class GCData { + constructor(kind) { + let numRandom = {main: 0, content: 2}; + let numWorst = {main: 2, content: 2}; + + this.totalGCTime = 0; + this.randomlySelected = Array(numRandom[kind]).fill(null); + this.worst = Array(numWorst[kind]).fill(null); + } + + // Turn absolute timestamps (in microseconds since the epoch) into + // milliseconds since startup. + rebaseTimes(data) { + function fixup(t) { + return t / 1000.0 - BASE_TIME; + } + + data.timestamp = fixup(data.timestamp); + + for (let i = 0; i < data.slices.length; i++) { + let slice = data.slices[i]; + slice.start_timestamp = fixup(slice.start_timestamp); + slice.end_timestamp = fixup(slice.end_timestamp); + } + } + + // Records a GC (represented by |data|) in the randomlySelected or + // worst batches depending on the criteria above. + record(data) { + this.rebaseTimes(data); + + let time = data.total_time; + this.totalGCTime += time; + + // Probability that we will replace any one of our + // current randomlySelected GCs with |data|. + let prob = time / this.totalGCTime; + + // Note that we may replace multiple GCs in + // randomlySelected. It's easier to reason about the + // probabilities this way, and it's unlikely to have any effect in + // practice. + for (let i = 0; i < this.randomlySelected.length; i++) { + let r = Math.random(); + if (r <= prob) { + this.randomlySelected[i] = data; + } + } + + // Save the 5 worst GCs based on max_pause. A GC may appear in + // both worst and randomlySelected. + for (let i = 0; i < this.worst.length; i++) { + if (!this.worst[i]) { + this.worst[i] = data; + break; + } + + if (this.worst[i].max_pause < data.max_pause) { + this.worst.splice(i, 0, data); + this.worst.length--; + break; + } + } + } + + entries() { + return { + random: this.randomlySelected.filter(e => e !== null), + worst: this.worst.filter(e => e !== null), + }; + } +} + +// If you adjust any of the constants here (slice limit, number of keys, etc.) +// make sure to update the JSON schema at: +// https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/telemetry/main.schema.json +// You should also adjust browser_TelemetryGC.js. +const MAX_GC_KEYS = 25; +const MAX_SLICES = 4; +const MAX_SLICE_KEYS = 15; +const MAX_PHASES = 65; + +function limitProperties(obj, count) { + // If there are too many properties, just delete them all. We don't + // expect this ever to happen. + if (Object.keys(obj).length > count) { + for (let key of Object.keys(obj)) { + delete obj[key]; + } + } +} + +function limitSize(data) { + // Store the number of slices so we know if we lost any at the end. + data.num_slices = data.slices.length; + + data.slices.sort((a, b) => b.pause - a.pause); + + if (data.slices.length > MAX_SLICES) { + // Make sure we always keep the first slice since it has the + // reason the GC was started. + let firstSliceIndex = data.slices.findIndex(s => s.slice == 0); + if (firstSliceIndex >= MAX_SLICES) { + data.slices[MAX_SLICES - 1] = data.slices[firstSliceIndex]; + } + + data.slices.length = MAX_SLICES; + } + + data.slices.sort((a, b) => a.slice - b.slice); + + limitProperties(data, MAX_GC_KEYS); + + for (let slice of data.slices) { + limitProperties(slice, MAX_SLICE_KEYS); + limitProperties(slice.times, MAX_PHASES); + } + + limitProperties(data.totals, MAX_PHASES); +} + +let processData = new Map(); +for (let name of PROCESS_NAMES) { + processData.set(name, new GCData(name)); +} + +var GCTelemetry = { + initialized: false, + + init() { + if (this.initialized) { + return false; + } + + this.initialized = true; + Services.obs.addObserver(this, "garbage-collection-statistics", false); + + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + Services.ppmm.addMessageListener("Telemetry:GCStatistics", this); + } + + return true; + }, + + shutdown() { + if (!this.initialized) { + return; + } + + Services.obs.removeObserver(this, "garbage-collection-statistics"); + + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + Services.ppmm.removeMessageListener("Telemetry:GCStatistics", this); + } + this.initialized = false; + }, + + observe(subject, topic, arg) { + let data = JSON.parse(arg); + + limitSize(data); + + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + processData.get("main").record(data); + } else { + Services.cpmm.sendAsyncMessage("Telemetry:GCStatistics", data); + } + }, + + receiveMessage(msg) { + processData.get("content").record(msg.data); + }, + + entries(kind, clear) { + let result = processData.get(kind).entries(); + if (clear) { + processData.set(kind, new GCData(kind)); + } + return result; + }, +}; diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json new file mode 100644 index 000000000..aa66fbe14 --- /dev/null +++ b/toolkit/components/telemetry/Histograms.json @@ -0,0 +1,11002 @@ + +{ + "A11Y_INSTANTIATED_FLAG": { + "expires_in_version": "never", + "kind": "flag", + "description": "has accessibility support been instantiated" + }, + "A11Y_CONSUMERS": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 11, + "description": "Accessibility client by enum id" + }, + "A11Y_ISIMPLEDOM_USAGE_FLAG": { + "expires_in_version": "default", + "kind": "flag", + "description": "have the ISimpleDOM* accessibility interfaces been used" + }, + "A11Y_IATABLE_USAGE_FLAG": { + "expires_in_version": "default", + "kind": "flag", + "description": "has the IAccessibleTable accessibility interface been used" + }, + "A11Y_UPDATE_TIME": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "time spent updating accessibility (ms)" + }, + "ADDON_MANAGER_UPGRADE_UI_SHOWN": { + "expires_in_version": "53", + "kind": "count", + "description": "Recorded when the addon manager shows the modal upgrade UI. Should only be recorded once per upgrade.", + "releaseChannelCollection": "opt-out", + "bug_numbers": [1268548], + "alert_emails": ["kev@mozilla.com"] + }, + "ADDON_SHIM_USAGE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 15, + "keyed": true, + "description": "Reasons why add-on shims were used, keyed by add-on ID." + }, + "ADDON_FORBIDDEN_CPOW_USAGE": { + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "Counts the number of times a given add-on used CPOWs when it was marked as e10s compatible.", + "bug_numbers": [1214824], + "alert_emails": ["wmccloskey@mozilla.com"] + }, + "BROWSER_SHIM_USAGE_BLOCKED": { + "expires_in_version": "never", + "kind": "count", + "description": "Counts the number of times a CPOW shim was blocked from being created by browser code.", + "releaseChannelCollection": "opt-out", + "bug_numbers": [1245901], + "alert_emails": ["benjamin@smedbergs.us"] + }, + "APPLICATION_REPUTATION_SHOULD_BLOCK": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Overall (local or remote) application reputation verdict (shouldBlock=false is OK)." + }, + "APPLICATION_REPUTATION_LOCAL": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "Application reputation local results (0=ALLOW, 1=BLOCK, 2=NONE)" + }, + "APPLICATION_REPUTATION_SERVER": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "Status of the application reputation remote lookup (0=OK, 1=failed, 2=invalid protobuf response)" + }, + "APPLICATION_REPUTATION_SERVER_VERDICT": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "56", + "releaseChannelCollection": "opt-out", + "bug_numbers": [1272788], + "kind": "enumerated", + "n_values": 8, + "description": "Application reputation remote verdict (0=SAFE, 1=DANGEROUS, 2=UNCOMMON, 3=POTENTIALLY_UNWANTED, 4=DANGEROUS_HOST, 5=UNKNOWN)" + }, + "APPLICATION_REPUTATION_COUNT": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Application reputation query count (both local and remote)" + }, + "APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "56", + "kind": "boolean", + "bug_numbers": [1172689], + "description": "Recorded when application reputation remote lookup is performed, `true` is recorded if the lookup times out." + }, + "AUDIOSTREAM_FIRST_OPEN_MS": { + "expires_in_version": "50", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "The length of time (in milliseconds) for the first open of AudioStream." + }, + "AUDIOSTREAM_LATER_OPEN_MS": { + "expires_in_version": "50", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "The length of time (in milliseconds) for the subsequent opens of AudioStream." + }, + "AUDIOSTREAM_BACKEND_USED": { + "alert_emails": ["padenot@mozilla.com", "kinetik@flim.org"], + "bug_numbers": [1280630], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 16, + "description": "The operating system audio back-end used when successfully opening an audio stream, or whether the failure occurred on the first try or not ", + "releaseChannelCollection": "opt-out" + }, + "AUSHELPER_CPU_ERROR_CODE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "bug_numbers": [1296630], + "expires_in_version": "60", + "kind": "enumerated", + "n_values": 16, + "releaseChannelCollection": "opt-out", + "description": "The error code from the aushelper system add-on when querying the registry for CPU information for bug 1296630 (see browser/extensions/aushelper/bootstrap.js)." + }, + "AUSHELPER_CPU_RESULT_CODE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "bug_numbers": [1296630], + "expires_in_version": "60", + "kind": "enumerated", + "n_values": 5, + "releaseChannelCollection": "opt-out", + "description": "Whether the system is affected by bug 1296630 (1=No, 2=Yes, 3=Error, and 4=Unknown)." + }, + "AUSHELPER_WEBSENSE_ERROR_CODE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "bug_numbers": [1305847], + "expires_in_version": "60", + "kind": "enumerated", + "n_values": 8, + "releaseChannelCollection": "opt-out", + "description": "The error code from the aushelper system add-on when gathering information on Websense (see browser/extensions/aushelper/bootstrap.js)." + }, + "AUSHELPER_WEBSENSE_REG_EXISTS": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "bug_numbers": [1305847], + "expires_in_version": "60", + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "Whether the system has a Websense InstallVersion registry value (see browser/extensions/aushelper/bootstrap.js)." + }, + "BACKGROUNDFILESAVER_THREAD_COUNT": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 21, + "description": "Maximum number of concurrent threads reached during a given download session" + }, + "BLOCKLIST_SYNC_FILE_LOAD": { + "alert_emails": ["rvitillo@mozilla.com"], + "expires_in_version": "35", + "kind": "boolean", + "description": "blocklist.xml has been loaded synchronously *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "CHECKERBOARD_DURATION": { + "alert_emails": ["kgupta@mozilla.com"], + "bug_numbers": [1238040], + "expires_in_version": "55", + "kind": "exponential", + "high": 100000, + "n_buckets": 50, + "description": "Duration of a checkerboard event in milliseconds" + }, + "CHECKERBOARD_PEAK": { + "alert_emails": ["kgupta@mozilla.com"], + "bug_numbers": [1238040], + "expires_in_version": "55", + "kind": "exponential", + "high": 66355200, + "n_buckets": 50, + "description": "Peak number of CSS pixels checkerboarded during a checkerboard event (the high value is the size of a 4k display with max APZ zooming)" + }, + "CHECKERBOARD_POTENTIAL_DURATION": { + "alert_emails": ["kgupta@mozilla.com"], + "bug_numbers": [1238040], + "expires_in_version": "55", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "Duration of a chunk of time (in ms) that could reasonably have had checkerboarding" + }, + "CHECKERBOARD_SEVERITY": { + "alert_emails": ["kgupta@mozilla.com"], + "bug_numbers": [1238040], + "expires_in_version": "55", + "kind": "exponential", + "high": 1073741824, + "n_buckets": 50, + "description": "Opaque measure of the severity of a checkerboard event" + }, + "COMPOSITE_TIME" : { + "expires_in_version": "never", + "description": "Composite times in milliseconds", + "kind": "exponential", + "high": 1000, + "n_buckets": 50 + }, + "COMPOSITE_FRAME_ROUNDTRIP_TIME" : { + "expires_in_version": "never", + "description": "Time from vsync to finishing a composite in milliseconds.", + "kind": "exponential", + "high": 1000, + "n_buckets": 50 + }, + "CONTENT_RESPONSE_DURATION" : { + "alert_emails": ["kgupta@mozilla.com"], + "bug_numbers": [1261373], + "expires_in_version": "55", + "description": "Main thread response times for APZ notifications about input events (ms)", + "kind" : "exponential", + "high": 60000, + "n_buckets": 50 + }, + "CREATE_EVENT_BEFOREUNLOADEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"beforeunloadevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_COMMANDEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"commandevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_COMMANDEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"commandevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_COMPOSITIONEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"compositionevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_CUSTOMEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"customevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_DATACONTAINEREVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"datacontainerevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_DATACONTAINEREVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"datacontainerevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_DEVICEMOTIONEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"devicemotionevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_DEVICEORIENTATIONEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"deviceorientationevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_DRAGEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"dragevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_DRAGEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"dragevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_EVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"event\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_EVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"events\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_HASHCHANGEEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"hashchangeevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_HTMLEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"htmlevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_KEYBOARDEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"keyboardevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_KEYEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"keyevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_MESSAGEEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"messageevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_MOUSEEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"mouseevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_MOUSEEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"mouseevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_MOUSESCROLLEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"mousescrollevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_MUTATIONEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"mutationevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_MUTATIONEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"mutationevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_NOTIFYPAINTEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"notifypaintevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_PAGETRANSITION" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"pagetransition\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_POPSTATEEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"popstateevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_POPUPEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"popupevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_SCROLLAREAEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"scrollareaevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_SIMPLEGESTUREEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"simplegestureevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_STORAGEEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"storageevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_SVGEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"svgevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_SVGEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"svgevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_SVGZOOMEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"svgzoomevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_SVGZOOMEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"svgzoomevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_TEXTEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"textevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_TEXTEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"textevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_TIMEEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"timeevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_TIMEEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"timeevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_TOUCHEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"touchevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_UIEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"uievent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_UIEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"uievents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_XULCOMMANDEVENT" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"xulcommandevent\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CREATE_EVENT_XULCOMMANDEVENTS" : { + "alert_emails": ["ayg@aryeh.name"], + "description": "Was document.createEvent(\"xulcommandevents\") ever called", + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1295588, 1251198] + }, + "CYCLE_COLLECTOR": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent on one cycle collection (ms)" + }, + "CYCLE_COLLECTOR_WORKER": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent on one cycle collection in a worker (ms)" + }, + "CYCLE_COLLECTOR_FULL": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Full pause time for one cycle collection, including preparation (ms)" + }, + "CYCLE_COLLECTOR_MAX_PAUSE": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Longest pause for an individual slice of one cycle collection, including preparation (ms)" + }, + "CYCLE_COLLECTOR_FINISH_IGC": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Cycle collection finished an incremental GC" + }, + "CYCLE_COLLECTOR_SYNC_SKIPPABLE": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Cycle collection synchronously ran forget skippable" + }, + "CYCLE_COLLECTOR_VISITED_REF_COUNTED": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 50, + "description": "Number of ref counted objects visited by the cycle collector" + }, + "CYCLE_COLLECTOR_WORKER_VISITED_REF_COUNTED": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 50, + "description": "Number of ref counted objects visited by the cycle collector in a worker" + }, + "CYCLE_COLLECTOR_VISITED_GCED": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 50, + "description": "Number of JS objects visited by the cycle collector" + }, + "CYCLE_COLLECTOR_WORKER_VISITED_GCED": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 50, + "description": "Number of JS objects visited by the cycle collector in a worker" + }, + "CYCLE_COLLECTOR_COLLECTED": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 50, + "description": "Number of objects collected by the cycle collector" + }, + "CYCLE_COLLECTOR_WORKER_COLLECTED": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 50, + "description": "Number of objects collected by the cycle collector in a worker" + }, + "CYCLE_COLLECTOR_NEED_GC": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Needed garbage collection before cycle collection." + }, + "CYCLE_COLLECTOR_WORKER_NEED_GC": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Needed garbage collection before cycle collection in a worker." + }, + "CYCLE_COLLECTOR_TIME_BETWEEN": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 120, + "n_buckets": 50, + "description": "Time spent in between cycle collections (seconds)" + }, + "CYCLE_COLLECTOR_OOM": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if the cycle collector ran out of memory at some point" + }, + "CYCLE_COLLECTOR_WORKER_OOM": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if the cycle collector in a worker ran out of memory at some point" + }, + "CYCLE_COLLECTOR_ASYNC_SNOW_WHITE_FREEING": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent on one asynchronous SnowWhite freeing (ms)" + }, + "DEFERRED_FINALIZE_ASYNC": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Pause time for asynchronous deferred finalization (ms)" + }, + "DEVICE_RESET_REASON": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "GPU Device Reset Reason (ok, hung, removed, reset, internal error, invalid call, out of memory)" + }, + "FAMILY_SAFETY": { + "alert_emails": ["seceng@mozilla.org"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 16, + "bug_numbers": [1239166], + "description": "Status of Family Safety detection and remediation. See nsNSSComponent.cpp." + }, + "FETCH_IS_MAINTHREAD": { + "expires_in_version": "50", + "kind": "boolean", + "description": "Was Fetch request initiated from the main thread?" + }, + "FORCED_DEVICE_RESET_REASON": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "releaseChannelCollection": "opt-out", + "description": "GPU Forced Device Reset Reason (OpenSharedHandle)" + }, + "FORGET_SKIPPABLE_MAX": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Max time spent on one forget skippable (ms)" + }, + "FULLSCREEN_TRANSITION_BLACK_MS": { + "alert_emails": ["xquan@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 100, + "high": 5000, + "n_buckets": 50, + "bug_numbers": [1271160], + "description": "The time spent in the fully-black screen in fullscreen transition" + }, + "FULLSCREEN_CHANGE_MS": { + "alert_emails": ["xquan@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 100, + "high": 5000, + "n_buckets": 50, + "bug_numbers": [1271160], + "description": "The time content uses to enter/exit fullscreen regardless of fullscreen transition timeout" + }, + "GC_REASON_2": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "description": "Reason (enum value) for initiating a GC" + }, + "GC_IS_COMPARTMENTAL": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Is it a zone GC?" + }, + "GC_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent running JS GC (ms)" + }, + "GC_BUDGET_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 10, + "description": "Requested GC slice budget (ms)" + }, + "GC_ANIMATION_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent running JS GC when animating (ms)" + }, + "GC_MAX_PAUSE_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 1000, + "n_buckets": 50, + "description": "Longest GC slice in a GC (ms)" + }, + "GC_MARK_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent running JS GC mark phase (ms)" + }, + "GC_SWEEP_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent running JS GC sweep phase (ms)" + }, + "GC_COMPACT_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent running JS GC compact phase (ms)" + }, + "GC_MARK_ROOTS_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 200, + "n_buckets": 50, + "description": "Time spent marking GC roots (ms)" + }, + "GC_MARK_GRAY_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 200, + "n_buckets": 50, + "description": "Time spent marking gray GC objects (ms)" + }, + "GC_SLICE_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent running a JS GC slice (ms)" + }, + "GC_SLOW_PHASE": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 75, + "description": "The longest phase in any slice that goes over 2x the budget" + }, + "GC_MMU_50": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 20, + "description": "Minimum percentage of time spent outside GC over any 50ms window" + }, + "GC_RESET": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Was an incremental GC canceled?" + }, + "GC_RESET_REASON": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "description": "Reason for cancelling an ongoing GC (see js::gc::AbortReason)", + "bug_numbers": [1308116] + }, + "GC_INCREMENTAL_DISABLED": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Is incremental GC permanently disabled?" + }, + "GC_NON_INCREMENTAL": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Was the GC non-incremental?" + }, + "GC_NON_INCREMENTAL_REASON": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "description": "Reason for performing a non-incremental GC (see js::gc::AbortReason)", + "bug_numbers": [1308116] + }, + "GC_SCC_SWEEP_TOTAL_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 500, + "n_buckets": 50, + "description": "Time spent sweeping compartment SCCs (ms)" + }, + "GC_SCC_SWEEP_MAX_PAUSE_MS": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 500, + "n_buckets": 50, + "description": "Time spent sweeping slowest compartment SCC (ms)" + }, + "GC_MINOR_REASON": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "description": "Reason (enum value) for initiating a minor GC" + }, + "GC_MINOR_REASON_LONG": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "description": "Reason (enum value) that caused a long (>1ms) minor GC" + }, + "GC_MINOR_US": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 100, + "description": "Time spent running JS minor GC (us)" + }, + "GC_NURSERY_BYTES": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 16777216, + "n_buckets": 16, + "bug_numbers": [1259347], + "description": "Size of the GC nursery (bytes)" + }, + "GC_PRETENURE_COUNT": { + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "kind": "enumerated", + "n_values": 32, + "bug_numbers": [1293262], + "description": "How many objects groups were selected for pretenuring by a minor GC" + }, + "GEOLOCATION_ACCURACY_EXPONENTIAL": { + "expires_in_version": "default", + "kind": "exponential", + "high": 100000, + "n_buckets": 50, + "description": "Location accuracy" + }, + "GEOLOCATION_ERROR": { + "expires_in_version": "never", + "kind": "flag", + "description": "Has seen location error" + }, + "GEOLOCATION_GETCURRENTPOSITION_SECURE_ORIGIN" : { + "expires_in_version" : "55", + "kind": "enumerated", + "n_values": 10, + "bug_numbers": [1230209], + "description" : "Number of navigator.geolocation.getCurrentPosition() calls (0=other, 1=http, 2=https)" + }, + "GEOLOCATION_REQUEST_GRANTED": { + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 20, + "bug_numbers": [1230209], + "description": "Geolocation requests either granted or denied (0=denied/other, 1=denied/http, 2=denied/https, ..., 10=granted/other, 11=granted/http, 12=granted/https)" + }, + "GEOLOCATION_WATCHPOSITION_SECURE_ORIGIN" : { + "expires_in_version" : "55", + "kind": "enumerated", + "n_values": 10, + "bug_numbers": [1230209], + "description" : "Number of navigator.geolocation.watchPosition() calls (0=other, 1=http, 2=https)" + }, + "GEOLOCATION_WIN8_SOURCE_IS_MLS": { + "expires_in_version": "default", + "kind": "boolean", + "description": "Geolocation on Win8 is either MLS or native" + }, + "GEOLOCATION_OSX_SOURCE_IS_MLS": { + "expires_in_version": "default", + "kind": "boolean", + "description": "Geolocation on OS X is either MLS or CoreLocation" + }, + "GEOLOCATION_GETCURRENTPOSITION_VISIBLE": { + "alert_emails": ["michelangelo@mozilla.com"], + "expires_in_version": "55", + "kind": "boolean", + "bug_numbers": [1255198], + "description": "This metric is recorded every time a navigator.geolocation.getCurrentPosition() request gets allowed/fulfilled. A false value is recorded if the owner is not visible according to document.isVisible." + }, + "GEOLOCATION_WATCHPOSITION_VISIBLE": { + "alert_emails": ["michelangelo@mozilla.com"], + "expires_in_version": "55", + "kind": "boolean", + "bug_numbers": [1255198], + "description": "This metric is recorded every time a navigator.geolocation.watchPosition() request gets allowed/fulfilled. A false value is recorded if the owner is not visible according to document.isVisible." + }, + "GPU_PROCESS_LAUNCH_TIME_MS" : { + "alert_emails": ["george@mozilla.com", "danderson@mozilla.com"], + "expires_in_version": "never", + "bug_numbers": [1297790], + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "releaseChannelCollection": "opt-out", + "description": "GPU process launch time in milliseconds" + }, + "JS_DEPRECATED_LANGUAGE_EXTENSIONS_IN_CONTENT": { + "alert_emails": ["jdemooij@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "Use of SpiderMonkey's deprecated language extensions in web content: ForEach=0, DestructuringForIn=1 (obsolete), LegacyGenerator=2, ExpressionClosure=3, LetBlock=4 (obsolete), LetExpression=5 (obsolete), NoSuchMethod=6 (obsolete), FlagsArgument=7 (obsolete), RegExpSourceProp=8 (obsolete), RestoredRegExpStatics=9 (obsolete), BlockScopeFunRedecl=10" + }, + "JS_DEPRECATED_LANGUAGE_EXTENSIONS_IN_ADDONS": { + "alert_emails": ["jdemooij@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "Use of SpiderMonkey's deprecated language extensions in add-ons: ForEach=0, DestructuringForIn=1 (obsolete), LegacyGenerator=2, ExpressionClosure=3, LetBlock=4 (obsolete), LetExpression=5 (obsolete), NoSuchMethod=6 (obsolete), FlagsArgument=7 (obsolete), RegExpSourceProp=8 (obsolete), RestoredRegExpStatics=9 (obsolete), BlockScopeFunRedecl=10" + }, + "XUL_CACHE_DISABLED": { + "expires_in_version": "default", + "kind": "flag", + "description": "XUL cache was disabled" + }, + "MEMORY_RESIDENT_FAST": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 32768, + "high": 16777216, + "n_buckets": 100, + "bug_numbers": [1226196], + "description": "Resident memory size (KB)" + }, + "MEMORY_TOTAL": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "bug_numbers": [1198209], + "expires_in_version": "never", + "kind": "exponential", + "low": 32768, + "high": 16777216, + "n_buckets": 100, + "description": "Total Memory Across All Processes (KB)" + }, + "MEMORY_UNIQUE": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "bug_numbers": [1198209], + "expires_in_version": "never", + "kind": "exponential", + "low": 32768, + "high": 16777216, + "n_buckets": 100, + "description": "Unique Set Size (KB)" + }, + "MEMORY_VSIZE": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 32768, + "high": 16777216, + "n_buckets": 100, + "description": "Virtual memory size (KB)" + }, + "MEMORY_VSIZE_MAX_CONTIGUOUS": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 32768, + "high": 16777216, + "n_buckets": 100, + "description": "Maximum-sized block of contiguous virtual memory (KB)" + }, + "MEMORY_JS_COMPARTMENTS_SYSTEM": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "Total JavaScript compartments used for add-ons and internals." + }, + "MEMORY_JS_COMPARTMENTS_USER": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "Total JavaScript compartments used for web pages" + }, + "MEMORY_JS_GC_HEAP": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 1024, + "high": 16777216, + "n_buckets": 200, + "description": "Memory used by the garbage-collected JavaScript heap (KB)" + }, + "MEMORY_STORAGE_SQLITE": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 1024, + "high": 524288, + "n_buckets": 50, + "description": "Memory used by SQLite (KB)" + }, + "MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 1024, + "high": 1048576, + "n_buckets": 50, + "description": "Memory used for uncompressed, in-use content images (KB)" + }, + "MEMORY_HEAP_ALLOCATED": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 1024, + "high": 16777216, + "n_buckets": 200, + "description": "Heap memory allocated (KB)" + }, + "MEMORY_HEAP_COMMITTED_UNUSED": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 1024, + "high": 524288, + "n_buckets": 50, + "description": "Committed, unused heap memory (KB)" + }, + "MEMORY_HEAP_OVERHEAD_FRACTION": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "bug_numbers": [1252375], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 25, + "description": "Fraction of committed heap memory that is overhead (percentage)." + }, + "GHOST_WINDOWS": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 128, + "n_buckets": 32, + "description": "Number of ghost windows" + }, + "MEMORY_FREE_PURGED_PAGES_MS": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1024, + "n_buckets": 10, + "description": "Time(ms) to purge dirty heap pages." + }, + "LOW_MEMORY_EVENTS_VIRTUAL": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1024, + "n_buckets": 21, + "description": "Number of low-virtual-memory events fired since last ping", + "cpp_guard": "XP_WIN" + }, + "LOW_MEMORY_EVENTS_PHYSICAL": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1024, + "n_buckets": 21, + "description": "Number of low-physical-memory events fired since last ping", + "cpp_guard": "XP_WIN" + }, + "LOW_MEMORY_EVENTS_COMMIT_SPACE": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1024, + "n_buckets": 21, + "description": "Number of low-commit-space events fired since last ping", + "cpp_guard": "XP_WIN" + }, + "PAGE_FAULTS_HARD": { + "expires_in_version": "default", + "kind": "exponential", + "low": 8, + "high": 65536, + "n_buckets": 13, + "description": "Hard page faults (since last telemetry ping)", + "cpp_guard": "XP_UNIX" + }, + "FONTLIST_INITOTHERFAMILYNAMES": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "Time(ms) spent on reading other family names from all fonts" + }, + "FONTLIST_INITFACENAMELISTS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "Time(ms) spent on reading family names from all fonts" + }, + "DWRITEFONT_DELAYEDINITFONTLIST_TOTAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "gfxDWriteFontList::DelayedInitFontList Total (ms)", + "cpp_guard": "XP_WIN" + }, + "DWRITEFONT_DELAYEDINITFONTLIST_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "gfxDWriteFontList::DelayedInitFontList Font Family Count", + "cpp_guard": "XP_WIN" + }, + "DWRITEFONT_DELAYEDINITFONTLIST_COLLECT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "gfxDWriteFontList::DelayedInitFontList GetSystemFontCollection (ms)", + "cpp_guard": "XP_WIN" + }, + "DWRITEFONT_INIT_PROBLEM": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "DirectWrite system fontlist initialization problem (1=GDI interop, 2=system font collection, 3=no fonts)", + "cpp_guard": "XP_WIN" + }, + "GDI_INITFONTLIST_TOTAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "gfxGDIFontList::InitFontList Total (ms)", + "cpp_guard": "XP_WIN" + }, + "MAC_INITFONTLIST_TOTAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "gfxMacPlatformFontList::InitFontList Total (ms)", + "cpp_guard": "XP_DARWIN" + }, + "SYSTEM_FONT_FALLBACK": { + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 50, + "description": "System font fallback (us)" + }, + "SYSTEM_FONT_FALLBACK_FIRST": { + "expires_in_version": "never", + "kind": "exponential", + "high": 40000, + "n_buckets": 20, + "description": "System font fallback, first call (ms)" + }, + "SYSTEM_FONT_FALLBACK_SCRIPT": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 110, + "description": "System font fallback script" + }, + "GRADIENT_DURATION": { + "expires_in_version": "default", + "kind": "exponential", + "high": 50000000, + "n_buckets": 20, + "description": "Gradient generation time (us)" + }, + "GRADIENT_RETENTION_TIME": { + "expires_in_version": "never", + "kind": "linear", + "high": 10000, + "n_buckets": 20, + "description": "Maximum retention time for the gradient cache. (ms)" + }, + "STARTUP_CACHE_AGE_HOURS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 20, + "description": "Startup cache age (hours)" + }, + "STARTUP_CACHE_INVALID": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "flag", + "description": "Was the disk startup cache file detected as invalid" + }, + "WORD_CACHE_HITS_CONTENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 30, + "description": "Word cache hits, content text (chars)" + }, + "WORD_CACHE_HITS_CHROME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 30, + "description": "Word cache hits, chrome text (chars)" + }, + "WORD_CACHE_MISSES_CONTENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 30, + "description": "Word cache misses, content text (chars)" + }, + "WORD_CACHE_MISSES_CHROME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 30, + "description": "Word cache misses, chrome text (chars)" + }, + "FONT_CACHE_HIT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "font cache hit" + }, + "BAD_FALLBACK_FONT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "system fallback font can't be used" + }, + "SHUTDOWN_OK": { + "expires_in_version": "default", + "kind": "boolean", + "description": "Did the browser start after a successful shutdown" + }, + "IMAGE_DECODE_LATENCY_US": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 5000000, + "n_buckets": 100, + "description": "Time spent decoding an image chunk (us)" + }, + "IMAGE_DECODE_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 50000000, + "n_buckets": 100, + "description": "Time spent decoding an image (us)" + }, + "IMAGE_DECODE_ON_DRAW_LATENCY": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 50000000, + "n_buckets": 100, + "description": "Time from starting a decode to it showing up on the screen (us)" + }, + "IMAGE_DECODE_CHUNKS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 500, + "n_buckets": 50, + "description": "Number of chunks per decode attempt" + }, + "IMAGE_DECODE_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 500, + "n_buckets": 50, + "description": "Decode count" + }, + "IMAGE_DECODE_OPAQUE_BGRA": { + "alert_emails": ["aosmond@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "description": "Opaque images are BGRA", + "bug_numbers": [1311779] + }, + "IMAGE_DECODE_SPEED_JPEG": { + "expires_in_version": "never", + "kind": "exponential", + "low": 500, + "high": 50000000, + "n_buckets": 50, + "description": "JPEG image decode speed (Kbytes/sec)" + }, + "IMAGE_DECODE_SPEED_GIF": { + "expires_in_version": "never", + "kind": "exponential", + "low": 500, + "high": 50000000, + "n_buckets": 50, + "description": "GIF image decode speed (Kbytes/sec)" + }, + "IMAGE_DECODE_SPEED_PNG": { + "expires_in_version": "never", + "kind": "exponential", + "low": 500, + "high": 50000000, + "n_buckets": 50, + "description": "PNG image decode speed (Kbytes/sec)" + }, + "CANVAS_2D_USED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "2D canvas used" + }, + "CANVAS_WEBGL_ACCL_FAILURE_ID": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bgirard@mozilla.com","msreckovic@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "Track the failure IDs that lead us to reject attempting to create an accelerated context. CANVAS_WEBGL_FAILURE_ID reports the overall WebGL status with the attempt to fallback.", + "bug_numbers": [1272808] + }, + "CANVAS_WEBGL_FAILURE_ID": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bgirard@mozilla.com","msreckovic@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "WebGL runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.", + "bug_numbers": [1272808] + }, + "CANVAS_WEBGL_SUCCESS": { + "alert_emails": ["jmuizelaar@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "WebGL1 creation success", + "bug_numbers": [1247327] + }, + "CANVAS_WEBGL_USED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "WebGL canvas used" + }, + "CANVAS_WEBGL2_SUCCESS": { + "alert_emails": ["jmuizelaar@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "WebGL2 creation success", + "bug_numbers": [1247327] + }, + "TOTAL_CONTENT_PAGE_LOAD_TIME": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 100, + "high": 30000, + "n_buckets": 100, + "description": "HTTP: Total page load time (ms)" + }, + "HTTP_SUBITEM_OPEN_LATENCY_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Page start -> subitem open() (ms)" + }, + "HTTP_SUBITEM_FIRST_BYTE_LATENCY_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Page start -> first byte received for subitem reply (ms)" + }, + "HTTP_REQUEST_PER_PAGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "HTTP: Requests per page (count)" + }, + "HTTP_REQUEST_PER_PAGE_FROM_CACHE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 101, + "description": "HTTP: Requests serviced from cache (%)" + }, + "HTTP_REQUEST_PER_CONN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "HTTP: requests per connection" + }, + "HTTP_KBREAD_PER_CONN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 50, + "description": "HTTP: KB read per connection" + }, + "HTTP_PAGE_DNS_ISSUE_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: open() -> DNS request issued (ms)" + }, + "HTTP_PAGE_DNS_LOOKUP_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: DNS lookup time (ms)" + }, + "HTTP_PAGE_TCP_CONNECTION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: TCP connection setup (ms)" + }, + "HTTP_PAGE_OPEN_TO_FIRST_SENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Open -> first byte of request sent (ms)" + }, + "HTTP_PAGE_FIRST_SENT_TO_LAST_RECEIVED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: First byte of request sent -> last byte of response received (ms)" + }, + "HTTP_PAGE_OPEN_TO_FIRST_RECEIVED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Open -> first byte of reply received (ms)" + }, + "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Open -> cache read start (ms)" + }, + "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Open -> cache read start (ms), [cache2]" + }, + "HTTP_PAGE_CACHE_READ_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Cache read time (ms)" + }, + "HTTP_PAGE_CACHE_READ_TIME_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Cache read time (ms) [cache2]" + }, + "HTTP_PAGE_REVALIDATION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Positive cache validation time (ms)" + }, + "HTTP_PAGE_COMPLETE_LOAD": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Overall load time - all (ms)" + }, + "HTTP_PAGE_COMPLETE_LOAD_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Overall load time - all (ms) [cache2]" + }, + "HTTP_PAGE_COMPLETE_LOAD_CACHED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Overall load time - cache hits (ms)" + }, + "HTTP_PAGE_COMPLETE_LOAD_CACHED_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Overall load time - cache hits (ms) [cache2]" + }, + "HTTP_PAGE_COMPLETE_LOAD_NET": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Overall load time - network (ms)" + }, + "HTTP_PAGE_COMPLETE_LOAD_NET_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page: Overall load time - network (ms) [cache2]" + }, + "HTTP_SUB_DNS_ISSUE_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: open() -> DNS request issued (ms)" + }, + "HTTP_SUB_DNS_LOOKUP_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: DNS lookup time (ms)" + }, + "HTTP_SUB_TCP_CONNECTION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: TCP connection setup (ms)" + }, + "HTTP_SUB_OPEN_TO_FIRST_SENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Open -> first byte of request sent (ms)" + }, + "HTTP_SUB_FIRST_SENT_TO_LAST_RECEIVED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: First byte of request sent -> last byte of response received (ms)" + }, + "HTTP_SUB_OPEN_TO_FIRST_RECEIVED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Open -> first byte of reply received (ms)" + }, + "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Open -> cache read start (ms)" + }, + "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Open -> cache read start (ms) [cache2]" + }, + "HTTP_SUB_CACHE_READ_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Cache read time (ms)" + }, + "HTTP_SUB_CACHE_READ_TIME_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Cache read time (ms) [cache2]" + }, + "HTTP_SUB_REVALIDATION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Positive cache validation time (ms)" + }, + "HTTP_SUB_COMPLETE_LOAD": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Overall load time - all (ms)" + }, + "HTTP_SUB_COMPLETE_LOAD_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Overall load time - all (ms) [cache2]" + }, + "HTTP_SUB_COMPLETE_LOAD_CACHED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Overall load time - cache hits (ms)" + }, + "HTTP_SUB_COMPLETE_LOAD_CACHED_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Overall load time - cache hits (ms) [cache2]" + }, + "HTTP_SUB_COMPLETE_LOAD_NET": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Overall load time - network (ms)" + }, + "HTTP_SUB_COMPLETE_LOAD_NET_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP subitem: Overall load time - network (ms) [cache2]" + }, + "HTTP_PROXY_TYPE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "HTTP Proxy Type (none, http, socks)" + }, + "HTTP_TRANSACTION_IS_SSL": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a HTTP transaction was over SSL or not." + }, + "HTTP_PAGELOAD_IS_SSL": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a HTTP base page load was over SSL or not." + }, + "HTTP_TRANSACTION_USE_ALTSVC": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a HTTP transaction was routed via Alt-Svc or not." + }, + "HTTP_TRANSACTION_USE_ALTSVC_OE": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a HTTP transaction routed via Alt-Svc was scheme=http" + }, + "HTTP_SCHEME_UPGRADE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "Was the URL upgraded to HTTPS? (0=already HTTPS, 1=no reason to upgrade, 2=STS upgrade blocked by pref, 3=upgraded with STS, 4=upgraded with CSP)" + }, + "HTTP_RESPONSE_STATUS_CODE": { + "alert_emails": ["ckerschbaumer@mozilla.com"], + "bug_numbers": [1272345, 1296287], + "expires_in_version": "56", + "kind": "enumerated", + "n_values": 12, + "description": "Whether the URL gets redirected? (0=200, 1=301, 2=302, 3=304, 4=307, 5=308, 6=400, 7=401, 8=403, 9=404, 10=500, 11=other)" + }, + "HTTP_NET_VS_CACHE_ONSTART_ISIMG": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for images. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_NOTIMG": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for non-images. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_ISIMG": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for images. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_NOTIMG": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for non-images. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_QSMALL_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and small queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_QMED_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and medium queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_QBIG_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and large queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_QSMALL_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and small queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_QMED_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and medium queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_QBIG_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and large queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_QSMALL_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and small queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_QMED_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and medium queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_QBIG_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and large queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_QSMALL_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and small queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_QMED_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and medium queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_QBIG_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and large queue. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_SMALL_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a small size (<32K) and normal priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_MED_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a medium size (<256K) and normal priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_LARGE_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a large size (>256K) and normal priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_SMALL_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a small size (<32K) and high priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_MED_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a medium size (<256K) and high priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_LARGE_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a large size (>256K) and high priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_SMALL_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a small size (<32K) and normal priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_MED_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a medium size (<256K) and normal priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_LARGE_NORMALPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a large size (>256K) and normal priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_SMALL_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a small size (<32K) and high priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_MED_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a medium size (<256K) and high priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_LARGE_HIGHPRI": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a large size (>256K) and high priority. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_REVALIDATED": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) revalidated cache entries. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTART_NOTREVALIDATED": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStartRequest) difference (ms) not revalidated cache entries. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_REVALIDATED": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) revalidated cache entries. Offset by 500 ms." + }, + "HTTP_NET_VS_CACHE_ONSTOP_NOTREVALIDATED": { + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1313095], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "Network vs cache time load (OnStopRequest) difference (ms) not revalidated cache entries. Offset by 500 ms." + }, + "HTTP_AUTH_DIALOG_STATS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "Stats about what kind of resource requested http authentication. (0=top-level doc, 1=same origin subresources, 2=cross-origin subresources, 3=xhr)" + }, + "HTTP_AUTH_TYPE_STATS": { + "alert_emails": ["rbarnes@mozilla.com"], + "bug_numbers": [1266571], + "expires_in_version": "52", + "kind": "enumerated", + "n_values": 8, + "releaseChannelCollection": "opt-out", + "description": "Recorded once for each HTTP 401 response. The value records the type of authentication and the TLS-enabled status. (0=basic/clear, 1=basic/tls, 2=digest/clear, 3=digest/tls, 4=ntlm/clear, 5=ntlm/tls, 6=negotiate/clear, 7=negotiate/tls)" + }, + "TLS_EARLY_DATA_NEGOTIATED": { + "expires_in_version": "58", + "kind": "enumerated", + "n_values": 3, + "description": "Sending TLS early data was possible: 0 - not possible, 1 - possible but not used, 2 - possible and used.", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1296288] + }, + "TLS_EARLY_DATA_ACCEPTED": { + "expires_in_version": "58", + "kind": "boolean", + "description": "TLS early data was used and it was accepted (true) or rejected (false) by the remote host.", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1296288] + }, + "TLS_EARLY_DATA_BYTES_WRITTEN": { + "expires_in_version": "58", + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "description": "Amount of bytes sent using TLS early data at the start of a TLS connection for a given channel.", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1296288] + }, + "SSL_HANDSHAKE_VERSION": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "bug_numbers": [1250568], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "SSL Version (1=tls1, 2=tls1.1, 3=tls1.2, 4=tls1.3)" + }, + "SSL_HANDSHAKE_RESULT": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "bug_numbers": [1331280], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 672, + "releaseChannelCollection": "opt-out", + "description": "SSL handshake result, 0=success, 1-255=NSS error offset, 256-511=SEC error offset + 256, 512-639=NSPR error offset + 512, 640-670=PKIX error, 671=unknown err" + }, + "SSL_TIME_UNTIL_READY": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 200, + "description": "ms of SSL wait time including TCP and proxy tunneling" + }, + "SSL_TIME_UNTIL_HANDSHAKE_FINISHED": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 200, + "description": "ms of SSL wait time for full handshake including TCP and proxy tunneling" + }, + "SSL_BYTES_BEFORE_CERT_CALLBACK": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 32000, + "n_buckets": 64, + "description": "plaintext bytes read before a server certificate authenticated" + }, + "SSL_NPN_TYPE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "NPN Results (0=none, 1=negotiated, 2=no-overlap, 3=selected(alpn))" + }, + "SSL_RESUMED_SESSION": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "complete TLS connect that used TLS Sesison Resumption" + }, + "CERT_VALIDATION_HTTP_REQUEST_RESULT": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "HTTP result of OCSP, etc.. (0=canceled, 1=OK, 2=FAILED, 3=internal-error)" + }, + "CERT_VALIDATION_HTTP_REQUEST_CANCELED_TIME": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 200, + "description": "ms elapsed time of OCSP etc.. that was canceled" + }, + "CERT_VALIDATION_HTTP_REQUEST_SUCCEEDED_TIME": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 200, + "description": "ms elapsed time of OCSP etc.. that succeeded" + }, + "CERT_VALIDATION_HTTP_REQUEST_FAILED_TIME": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 200, + "description": "ms elapsed time of OCSP etc.. that failed" + }, + "SSL_KEY_EXCHANGE_ALGORITHM_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "SSL Handshake Key Exchange Algorithm for full handshake (null=0, rsa=1, dh=2, fortezza=3, ecdh=4)" + }, + "SSL_KEY_EXCHANGE_ALGORITHM_RESUMED": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "SSL Handshake Key Exchange Algorithm for resumed handshake (null=0, rsa=1, dh=2, fortezza=3, ecdh=4)" + }, + "SSL_OBSERVED_END_ENTITY_CERTIFICATE_LIFETIME": { + "expires_in_version": "55", + "alert_emails": ["seceng-telemetry@mozilla.com"], + "kind": "enumerated", + "n_values": 125, + "releaseChannelCollection": "opt-out", + "description": "The lifetime of accepted HTTPS server certificates, in weeks, up to 2 years. Bucket 105 is all end-entity HTTPS server certificates with a lifetime > 2 years." + }, + "KEYGEN_GENERATED_KEY_TYPE": { + "expires_in_version": "55", + "alert_emails": ["seceng-telemetry@mozilla.com"], + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "bug_numbers": [1191414,1284945], + "description": "The number of times we generate a key via keygen, keyed on algorithm and keysize. Keys include RSA with key size (512, 1024, 2048, possibly others), secp384r1, secp256r1, and 'other_ec'." + }, + "WEBSOCKETS_HANDSHAKE_TYPE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "Websockets Handshake Results (ws-ok-plain, ws-ok-proxy, ws-failed-plain, ws-failed-proxy, wss-ok-plain, wss-ok-proxy, wss-failed-plain, wss-failed-proxy)" + }, + "SPDY_VERSION2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 48, + "description": "SPDY: Protocol Version Used" + }, + "HTTP_RESPONSE_VERSION": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 48, + "description": "HTTP: Protocol Version Used on Response from nsHttp.h" + }, + "HTTP_09_INFO": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "HTTP 09 Response Breakdown: lowbit subresource, high bit nonstd port", + "bug_numbers": [1262572], + "alert_emails": ["necko@mozilla.com"] + }, + "SPDY_PARALLEL_STREAMS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "SPDY: Streams concurrent active per connection" + }, + "SPDY_REQUEST_PER_CONN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "SPDY: Streams created per connection" + }, + "SPDY_SERVER_INITIATED_STREAMS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 250, + "description": "SPDY: Streams recevied per connection" + }, + "SPDY_CHUNK_RECVD": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 100, + "description": "SPDY: Recvd Chunk Size (rounded to KB)" + }, + "SPDY_SYN_SIZE": { + "expires_in_version": "never", + "kind": "exponential", + "low": 20, + "high": 20000, + "n_buckets": 50, + "description": "SPDY: SYN Frame Header Size" + }, + "SPDY_SYN_RATIO": { + "expires_in_version": "never", + "kind": "linear", + "high": 99, + "n_buckets": 20, + "description": "SPDY: SYN Frame Header Ratio (lower better)" + }, + "SPDY_SYN_REPLY_SIZE": { + "expires_in_version": "never", + "kind": "exponential", + "low": 16, + "high": 20000, + "n_buckets": 50, + "description": "SPDY: SYN Reply Header Size" + }, + "SPDY_SYN_REPLY_RATIO": { + "expires_in_version": "never", + "kind": "linear", + "high": 99, + "n_buckets": 20, + "description": "SPDY: SYN Reply Header Ratio (lower better)" + }, + "SPDY_NPN_CONNECT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "SPDY: NPN Negotiated" + }, + "SPDY_NPN_JOIN": { + "expires_in_version": "never", + "kind": "boolean", + "description": "SPDY: Coalesce Succeeded" + }, + "SPDY_KBREAD_PER_CONN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 50, + "description": "SPDY: KB read per connection" + }, + "SPDY_SETTINGS_UL_BW": { + "expires_in_version": "42", + "kind": "exponential", + "high": 10000, + "n_buckets": 100, + "description": "SPDY: Settings Upload Bandwidth" + }, + "SPDY_SETTINGS_DL_BW": { + "expires_in_version": "42", + "kind": "exponential", + "high": 10000, + "n_buckets": 100, + "description": "SPDY: Settings Download Bandwidth" + }, + "SPDY_SETTINGS_RTT": { + "expires_in_version": "42", + "kind": "exponential", + "high": 1000, + "n_buckets": 100, + "description": "SPDY: Settings RTT" + }, + "SPDY_SETTINGS_MAX_STREAMS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 100, + "description": "H2: Settings Max Streams parameter" + }, + "SPDY_SETTINGS_CWND": { + "expires_in_version": "42", + "kind": "exponential", + "high": 500, + "n_buckets": 50, + "description": "SPDY: Settings CWND (packets)" + }, + "SPDY_SETTINGS_RETRANS": { + "expires_in_version": "42", + "kind": "exponential", + "high": 100, + "n_buckets": 50, + "description": "SPDY: Retransmission Rate" + }, + "SPDY_SETTINGS_IW": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "H2: Settings Initial Window (rounded to KB)" + }, + "SPDY_GOAWAY_LOCAL": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 32, + "description": "H2: goaway reason client sent from rfc 7540. 31 is none sent." + }, + "SPDY_GOAWAY_PEER": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 32, + "description": "H2: goaway reason from peer from rfc 7540. 31 is none received." + }, + "HPACK_ELEMENTS_EVICTED_DECOMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 50, + "description": "HPACK: Number of items removed from dynamic table to make room for 1 new item", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_BYTES_EVICTED_DECOMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 8192, + "n_buckets": 50, + "description": "HPACK: Number of bytes removed from dynamic table to make room for 1 new item", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_BYTES_EVICTED_RATIO_DECOMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 50, + "description": "HPACK: Ratio of bytes evicted to bytes added (* 100)", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_PEAK_COUNT_DECOMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1024, + "n_buckets": 50, + "description": "HPACK: peak number of items in the dynamic table", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_PEAK_SIZE_DECOMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 16384, + "n_buckets": 100, + "description": "HPACK: peak size in bytes of the table", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_ELEMENTS_EVICTED_COMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 50, + "description": "HPACK: Number of items removed from dynamic table to make room for 1 new item", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_BYTES_EVICTED_COMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 8192, + "n_buckets": 50, + "description": "HPACK: Number of bytes removed from dynamic table to make room for 1 new item", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_BYTES_EVICTED_RATIO_COMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 256, + "n_buckets": 50, + "description": "HPACK: Ratio of bytes evicted to bytes added (* 100)", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_PEAK_COUNT_COMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1024, + "n_buckets": 50, + "description": "HPACK: peak number of items in the dynamic table", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HPACK_PEAK_SIZE_COMPRESSOR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 16384, + "n_buckets": 100, + "description": "HPACK: peak size in bytes of the table", + "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"], + "bug_numbers": [1296280] + }, + "HTTP_CHANNEL_DISPOSITION" : { + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1341128], + "expires_in_version": "60", + "kind": "enumerated", + "n_values": 16, + "releaseChannelCollection": "opt-out", + "description": "Channel Disposition: 0=Cancel, 1=Disk, 2=NetOK, 3=NetEarlyFail, 4=NetlateFail, +8 for HTTPS" + }, + "HTTP_CONNECTION_ENTRY_CACHE_HIT_1" : { + "expires_in_version": "never", + "kind": "boolean", + "description": "Fraction of sockets that used a nsConnectionEntry with history - size 300." + }, + "HTTP_CACHE_DISPOSITION_2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "HTTP Cache Hit, Reval, Failed-Reval, Miss" + }, + "HTTP_CACHE_DISPOSITION_2_V2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "HTTP Cache v2 Hit, Reval, Failed-Reval, Miss" + }, + "HTTP_DISK_CACHE_DISPOSITION_2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "HTTP Disk Cache Hit, Reval, Failed-Reval, Miss" + }, + "HTTP_CACHE_MISS_HALFLIFE_EXPERIMENT_2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "HTTP Cache v2 Miss by half-life value (5 min, 15 min, 1 hour, 6 hours)" + }, + "HTTP_CACHE_ENTRY_RELOAD_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 900000, + "n_buckets": 50, + "description": "Time before we reload an HTTP cache entry again to memory" + }, + "HTTP_CACHE_ENTRY_ALIVE_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 7200000, + "n_buckets": 50, + "description": "Time for which an HTTP cache entry is kept warmed in memory" + }, + "HTTP_CACHE_ENTRY_REUSE_COUNT": { + "expires_in_version": "never", + "kind": "linear", + "high": 20, + "n_buckets": 19, + "description": "Reuse count of an HTTP cache entry warmed in memory" + }, + "HTTP_MEMORY_CACHE_DISPOSITION_2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "HTTP Memory Cache Hit, Reval, Failed-Reval, Miss" + }, + "HTTP_OFFLINE_CACHE_DISPOSITION_2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "HTTP Offline Cache Hit, Reval, Failed-Reval, Miss" + }, + "HTTP_OFFLINE_CACHE_DOCUMENT_LOAD": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Rate of page load from offline cache" + }, + "HTTP_CACHE_IO_QUEUE_2_OPEN_PRIORITY": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_READ_PRIORITY": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_MANAGEMENT": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_OPEN": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_READ": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_WRITE": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_WRITE_PRIORITY": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_INDEX": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "HTTP_CACHE_IO_QUEUE_2_EVICT": { + "alert_emails": ["hbambas@mozilla.com"], + "bug_numbers": [1294183], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "HTTP Cache IO queue length" + }, + "CACHE_DEVICE_SEARCH_2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time to search cache (ms)" + }, + "CACHE_MEMORY_SEARCH_2": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time to search memory cache (ms)" + }, + "CACHE_DISK_SEARCH_2": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time to search disk cache (ms)" + }, + "CACHE_OFFLINE_SEARCH_2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time to search offline cache (ms)" + }, + "TRANSACTION_WAIT_TIME_HTTP": { + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 100, + "description": "Time from submission to dispatch of HTTP transaction (ms)" + }, + "TRANSACTION_WAIT_TIME_HTTP_PIPELINES": { + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 100, + "description": "Time from submission to dispatch of HTTP with pipelines transaction (ms)" + }, + "TRANSACTION_WAIT_TIME_SPDY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 100, + "description": "Time from submission to dispatch of SPDY transaction (ms)" + }, + "HTTP_SAW_QUIC_ALT_PROTOCOL": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Fraction of responses with a quic alt-protocol advertisement." + }, + "HTTP_CONTENT_ENCODING": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 6, + "description": "encoding removed: 0=unknown, 1=gzip, 2=deflate, 3=brotli" + }, + "HTTP_DISK_CACHE_OVERHEAD": { + "expires_in_version": "default", + "kind": "exponential", + "high": 32000000, + "n_buckets": 100, + "description": "HTTP Disk cache memory overhead (bytes)" + }, + "CACHE_LM_INCONSISTENT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Cache discovered inconsistent last-modified entry" + }, + "CACHE_SERVICE_LOCK_WAIT_2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms)" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock on the main thread (ms)" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSSETDISKSMARTSIZECALLBACK_NOTIFY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSSETDISKSMARTSIZECALLBACK_NOTIFY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSPROCESSREQUESTEVENT_RUN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSPROCESSREQUESTEVENT_RUN" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_LAZYINIT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSOUTPUTSTREAMWRAPPER_LAZYINIT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_CLOSEINTERNAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSOUTPUTSTREAMWRAPPER_CLOSEINTERNAL" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_RELEASE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSOUTPUTSTREAMWRAPPER_RELEASE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCOMPRESSOUTPUTSTREAMWRAPPER_RELEASE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCOMPRESSOUTPUTSTREAMWRAPPER_RELEASE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_LAZYINIT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSINPUTSTREAMWRAPPER_LAZYINIT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_CLOSEINTERNAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSINPUTSTREAMWRAPPER_CLOSEINTERNAL" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_RELEASE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSINPUTSTREAMWRAPPER_RELEASE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSDECOMPRESSINPUTSTREAMWRAPPER_RELEASE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSDECOMPRESSINPUTSTREAMWRAPPER_RELEASE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SHUTDOWN" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHEENABLED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETOFFLINECACHEENABLED" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHECAPACITY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETOFFLINECACHECAPACITY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETMEMORYCACHE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKSMARTSIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKSMARTSIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEMAXENTRYSIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKCACHEMAXENTRYSIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHEMAXENTRYSIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETMEMORYCACHEMAXENTRYSIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEENABLED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKCACHEENABLED" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHECAPACITY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKCACHECAPACITY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_OPENCACHEENTRY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_OPENCACHEENTRY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILESHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_ONPROFILESHUTDOWN" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILECHANGED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_ONPROFILECHANGED" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ISSTORAGEENABLEDFORPOLICY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_ISSTORAGEENABLEDFORPOLICY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_GETCACHEIOTARGET": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_GETCACHEIOTARGET" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_EVICTENTRIESFORCLIENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_EVICTENTRIESFORCLIENT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_DISKDEVICEHEAPSIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_DISKDEVICEHEAPSIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_CLOSEALLSTREAMS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_CLOSEALLSTREAMS" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOM": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_DOOM" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETPREDICTEDDATASIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETPREDICTEDDATASIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDATASIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETDATASIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEDATASIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETSTORAGEDATASIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_REQUESTDATASIZECHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_REQUESTDATASIZECHANGE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETDATASIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETDATASIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENINPUTSTREAM": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_OPENINPUTSTREAM" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENOUTPUTSTREAM": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_OPENOUTPUTSTREAM" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCACHEELEMENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETCACHEELEMENT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETCACHEELEMENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETCACHEELEMENT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEPOLICY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETSTORAGEPOLICY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSTORAGEPOLICY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETSTORAGEPOLICY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFILE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETFILE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSECURITYINFO": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETSECURITYINFO" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSECURITYINFO": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETSECURITYINFO" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOMANDFAILPENDINGREQUESTS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_DOOMANDFAILPENDINGREQUESTS" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_MARKVALID": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_MARKVALID" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_CLOSE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_CLOSE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETMETADATAELEMENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETMETADATAELEMENT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETMETADATAELEMENT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETMETADATAELEMENT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_VISITMETADATA": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_VISITMETADATA" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETEXPIRATIONTIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETEXPIRATIONTIME" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_ISSTREAMBASED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_ISSTREAMBASED" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTMODIFIED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETLASTMODIFIED" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETEXPIRATIONTIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETEXPIRATIONTIME" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETKEY": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETKEY" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFETCHCOUNT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETFETCHCOUNT" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDEVICEID": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETDEVICEID" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_PROCESSREQUEST": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_PROCESSREQUEST" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_VISITENTRIES": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_VISITENTRIES" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETPREDICTEDDATASIZE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETPREDICTEDDATASIZE" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTFETCHED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETLASTFETCHED" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCLIENTID": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETCLIENTID" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSBLOCKONCACHETHREADEVENT_RUN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSBLOCKONCACHETHREADEVENT_RUN" + }, + "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSASYNCDOOMEVENT_RUN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSASYNCDOOMEVENT_RUN" + }, + "DNT_USAGE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "I want to be tracked, I do NOT want to be tracked, DNT unset" + }, + "DNS_LOOKUP_METHOD2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "DNS Lookup Type (hit, renewal, negative-hit, literal, overflow, network-first, network-shared)" + }, + "DNS_CLEANUP_AGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1440, + "n_buckets": 50, + "description": "DNS Cache Entry Age at Removal Time (minutes)" + }, + "DNS_LOOKUP_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time for a successful DNS OS resolution (msec)" + }, + "DNS_RENEWAL_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time for a renewed DNS OS resolution (msec)" + }, + "DNS_RENEWAL_TIME_FOR_TTL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time for a DNS OS resolution (msec) used to get TTL" + }, + "DNS_FAILED_LOOKUP_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time for an unsuccessful DNS OS resolution (msec)" + }, + "DNS_BLACKLIST_COUNT": { + "expires_in_version": "never", + "kind": "linear", + "high": 21, + "n_buckets": 20, + "description": "The number of unusable addresses reported for each record" + }, + "REFRESH_DRIVER_TICK" : { + "expires_in_version": "never", + "description": "Total time spent ticking the refresh driver in milliseconds", + "kind": "exponential", + "high": 1000, + "n_buckets": 50 + }, + "PAINT_BUILD_DISPLAYLIST_TIME" : { + "expires_in_version": "never", + "description": "Time spent in building displaylists in milliseconds", + "kind": "exponential", + "high": 1000, + "n_buckets": 50 + }, + "PAINT_RASTERIZE_TIME" : { + "expires_in_version": "never", + "description": "Time spent rasterizing each frame in milliseconds", + "kind": "exponential", + "high": 1000, + "n_buckets": 50 + }, + "PREDICTOR_PREDICT_ATTEMPTS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "Number of times nsINetworkPredictor::Predict is called and attempts to predict" + }, + "PREDICTOR_LEARN_ATTEMPTS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "Number of times nsINetworkPredictor::Learn is called and attempts to learn" + }, + "PREDICTOR_PREDICT_FULL_QUEUE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Number of times nsINetworkPredictor::Predict doesn't continue because the queue is full" + }, + "PREDICTOR_LEARN_FULL_QUEUE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Number of times nsINetworkPredictor::Learn doesn't continue because the queue is full" + }, + "PREDICTOR_WAIT_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Amount of time a predictor event waits in the queue (ms)" + }, + "PREDICTOR_PREDICT_WORK_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Amount of time spent doing the work for predict (ms)" + }, + "PREDICTOR_LEARN_WORK_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Amount of time spent doing the work for learn (ms)" + }, + "PREDICTOR_TOTAL_PREDICTIONS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many actual predictions (preresolves, preconnects, ...) happen" + }, + "PREDICTOR_TOTAL_PREFETCHES": { + "expires_in_version": "never", + "alert_emails": [], + "bug_numbers": [1016628], + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many actual prefetches happen" + }, + "PREDICTOR_TOTAL_PREFETCHES_USED": { + "expires_in_version": "never", + "alert_emails": [], + "bug_numbers": [1016628], + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many prefetches are actually used by a channel" + }, + "PREDICTOR_PREFETCH_TIME": { + "expires_in_version": "never", + "alert_emails": [], + "bug_numbers": [1016628], + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "How long it takes from OnStartRequest to OnStopRequest for a prefetch" + }, + "PREDICTOR_TOTAL_PRECONNECTS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many actual preconnects happen" + }, + "PREDICTOR_TOTAL_PRECONNECTS_CREATED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many preconnects actually created a speculative socket" + }, + "PREDICTOR_TOTAL_PRECONNECTS_USED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many preconnects actually created a used speculative socket" + }, + "PREDICTOR_TOTAL_PRECONNECTS_UNUSED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many preconnects needlessly created a speculative socket" + }, + "PREDICTOR_TOTAL_PRERESOLVES": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many actual preresolves happen" + }, + "PREDICTOR_PREDICTIONS_CALCULATED": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many prediction calculations are performed" + }, + "PREDICTOR_GLOBAL_DEGRADATION": { + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 50, + "description": "The global degradation calculated" + }, + "PREDICTOR_SUBRESOURCE_DEGRADATION": { + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 50, + "description": "The degradation calculated for a subresource" + }, + "PREDICTOR_BASE_CONFIDENCE": { + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 50, + "description": "The base confidence calculated for a subresource" + }, + "PREDICTOR_CONFIDENCE": { + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 50, + "description": "The final confidence calculated for a subresource" + }, + "PREDICTOR_PREDICT_TIME_TO_ACTION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "How long it takes from the time Predict() is called to the time we take action" + }, + "PREDICTOR_PREDICT_TIME_TO_INACTION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "How long it takes from the time Predict() is called to the time we figure out there's nothing to do" + }, + "HTTPCONNMGR_TOTAL_SPECULATIVE_CONN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many speculative http connections are created" + }, + "HTTPCONNMGR_USED_SPECULATIVE_CONN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many speculative http connections are actually used" + }, + "HTTPCONNMGR_UNUSED_SPECULATIVE_CONN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 50, + "description": "How many speculative connections are made needlessly" + }, + "TAP_TO_LOAD_IMAGE_SIZE": { + "expires_in_version": "50", + "kind": "exponential", + "high": 32768, + "n_buckets": 50, + "description": "The size of the image being shown, when using tap-to-load images. (kilobytes)", + "bug_numbers": [1208167] + }, + "STS_POLL_AND_EVENTS_CYCLE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "The duraion of a socketThread cycle, including polls and pending events. (ms)" + }, + "STS_NUMBER_OF_PENDING_EVENTS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "description": "Number of pending events per SocketThread cycle." + }, + "STS_POLL_CYCLE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "The duration of poll. (ms)" + }, + "STS_POLL_AND_EVENT_THE_LAST_CYCLE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "The duraion of the socketThread cycle during shutdown, including polls and pending events. (ms)" + }, + "STS_NUMBER_OF_PENDING_EVENTS_IN_THE_LAST_CYCLE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "description": "Number of pending events per SocketThread cycle during shutdown." + }, + "STS_NUMBER_OF_ONSOCKETREADY_CALLS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "description": "Number of OnSocketReady calls during a single poll." + }, + "STS_POLL_BLOCK_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked on poll (ms)." + }, + "PRCONNECT_BLOCKING_TIME_NORMAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Connect when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)." + }, + "PRCONNECT_BLOCKING_TIME_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Connect during a shutdown (ms)." + }, + "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Connect when there has been the connectiviy change in the last 60s (ms)." + }, + "PRCONNECT_BLOCKING_TIME_LINK_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Connect when there has been a link change in the last 60s (ms)." + }, + "PRCONNECT_BLOCKING_TIME_OFFLINE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Connect when the offline state has changed in the last 60s (ms)." + }, + "PRCONNECT_FAIL_BLOCKING_TIME_NORMAL": { + "bug_numbers": [1257809], + "alert_emails": ["ddamjanovic@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "description": "Time spent blocked in a failed PR_Connect when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)." + }, + "PRCONNECT_FAIL_BLOCKING_TIME_SHUTDOWN": { + "bug_numbers": [1257809], + "alert_emails": ["ddamjanovic@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "description": "Time spent blocked in a failed PR_Connect during a shutdown (ms)." + }, + "PRCONNECT_FAIL_BLOCKING_TIME_CONNECTIVITY_CHANGE": { + "bug_numbers": [1257809], + "alert_emails": ["ddamjanovic@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "description": "Time spent blocked in a failed PR_Connect when there has been the connectiviy change in the last 60s (ms)." + }, + "PRCONNECT_FAIL_BLOCKING_TIME_LINK_CHANGE": { + "bug_numbers": [1257809], + "alert_emails": ["ddamjanovic@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "description": "Time spent blocked in a failed PR_Connect when there has been a link change in the last 60s (ms)." + }, + "PRCONNECT_FAIL_BLOCKING_TIME_OFFLINE": { + "bug_numbers": [1257809], + "alert_emails": ["ddamjanovic@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "description": "Time spent blocked in a failed PR_Connect when the offline state has changed in the last 60s (ms)." + }, + "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_ConnectContinue when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)." + }, + "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_ConnectContinue during a shutdown (ms)." + }, + "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_ConnectContinue when there has been the connectivity change in the last 60s (ms)." + }, + "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_ConnectContinue when there has been a link change in the last 60s (ms)." + }, + "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_ConnectContinue when the offline state has changed in the last 60s (ms)." + }, + "PRCLOSE_TCP_BLOCKING_TIME_NORMAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)." + }, + "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close during a shutdown (ms)." + }, + "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when there has been the connectivity change in the last 60s (ms)." + }, + "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when there has been a link change in the last 60s (ms)." + }, + "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when the offline state has changed in the last 60s (ms)." + }, + "PRCLOSE_UDP_BLOCKING_TIME_NORMAL": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)." + }, + "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close during a shutdown (ms)." + }, + "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when there has been the connectivity change in the last 60s (ms)." + }, + "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when there has been a link change in the last 60s (ms)." + }, + "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "Time spent blocked in PR_Close when the offline state has changed in the last 60s (ms)." + }, + "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "Count the number of 0) successful connections to an ipv4 address, 1) failed connection an ipv4 address, 2) successful connection to an ipv6 address and 3) failed connections to an ipv6 address." + }, + "NETWORK_SESSION_AT_900FD": { + "expires_in_version": "never", + "kind": "boolean", + "description": "session reached 900 fd limit sockets", + "bug_numbers": [1260218], + "alert_emails": ["necko@mozilla.com"] + }, + "NETWORK_PROBE_MAXCOUNT": { + "expires_in_version": "never", + "kind": "linear", + "low": 50, + "high": 1000, + "n_buckets": 10, + "description": "Result of nsSocketTransportService::ProbeMaxCount()", + "bug_numbers": [1260218], + "alert_emails": ["necko@mozilla.com"] + }, + "FIND_PLUGINS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent scanning filesystem for plugins (ms)" + }, + "CHECK_JAVA_ENABLED": { + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent checking if Java is enabled (ms)" + }, + "PLUGIN_HANG_UI_USER_RESPONSE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "User response to Plugin Hang UI" + }, + "PLUGIN_HANG_UI_DONT_ASK": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether the user has requested not to see the Plugin Hang UI again" + }, + "PLUGIN_HANG_UI_RESPONSE_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 20, + "description": "Time spent in Plugin Hang UI (ms)" + }, + "PLUGIN_HANG_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 20, + "description": "Value of dom.ipc.plugins.hangUITimeoutSecs plus time spent in Plugin Hang UI (ms)" + }, + "PLUGIN_LOAD_METADATA": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 20, + "description": "Time spent loading plugin DLL and obtaining metadata (ms)" + }, + "PLUGIN_SHUTDOWN_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 20, + "description": "Time spent shutting down plugins (ms)" + }, + "PLUGIN_CALLED_DIRECTLY": { + "expires_in_version": "never", + "kind": "flag", + "description": "A plugin object was successfully invoked as a function" + }, + "FLASH_PLUGIN_STATES": { + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 20, + "description": "A flash object's initialization state" + }, + "FLASH_PLUGIN_AREA": { + "expires_in_version": "50", + "kind": "exponential", + "low": 256, + "high": 16777216, + "n_buckets": 50, + "description": "Flash object area (width * height)" + }, + "FLASH_PLUGIN_WIDTH": { + "expires_in_version": "50", + "kind": "linear", + "low": 1, + "high": 2000, + "n_buckets": 50, + "description": "Flash object width" + }, + "FLASH_PLUGIN_HEIGHT": { + "expires_in_version": "50", + "kind": "linear", + "low": 1, + "high": 2000, + "n_buckets": 50, + "description": "Flash object height" + }, + "FLASH_PLUGIN_INSTANCES_ON_PAGE": { + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 30, + "description": "Flash object instances count on page" + }, + "MOZ_SQLITE_OPEN_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite open() (ms)" + }, + "MOZ_SQLITE_OPEN_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite open() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_TRUNCATE_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite truncate() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_TRUNCATE_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite truncate() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_OTHER_READ_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_OTHER_READ_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_PLACES_READ_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_PLACES_READ_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on cookie DB open with readahead (ms)" + }, + "MOZ_SQLITE_COOKIES_READ_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_COOKIES_READ_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_WEBAPPS_READ_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_WEBAPPS_READ_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_OTHER_WRITE_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_OTHER_WRITE_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_PLACES_WRITE_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms)" + }, + "MOZ_SQLITE_PLACES_WRITE_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_COOKIES_WRITE_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_COOKIES_WRITE_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_WEBAPPS_WRITE_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_WEBAPPS_WRITE_MAIN_THREAD_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_OTHER_SYNC_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_OTHER_SYNC_MAIN_THREAD_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_PLACES_SYNC_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_PLACES_SYNC_MAIN_THREAD_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_COOKIES_SYNC_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_COOKIES_SYNC_MAIN_THREAD_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_WEBAPPS_SYNC_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_WEBAPPS_SYNC_MAIN_THREAD_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time spent on SQLite fsync() (ms)" + }, + "MOZ_SQLITE_OTHER_READ_B": { + "expires_in_version": "default", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite read() (bytes)" + }, + "MOZ_SQLITE_PLACES_READ_B": { + "expires_in_version": "40", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite read() (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_COOKIES_READ_B": { + "expires_in_version": "40", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite read() (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_WEBAPPS_READ_B": { + "expires_in_version": "40", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite read() (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_PLACES_WRITE_B": { + "expires_in_version": "40", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite write (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_COOKIES_WRITE_B": { + "expires_in_version": "40", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite write (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_WEBAPPS_WRITE_B": { + "expires_in_version": "40", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite write (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_SQLITE_OTHER_WRITE_B": { + "expires_in_version": "default", + "kind": "linear", + "high": 32768, + "n_buckets": 3, + "description": "SQLite write (bytes)" + }, + "MOZ_STORAGE_ASYNC_REQUESTS_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "40", + "kind": "exponential", + "high": 32768, + "n_buckets": 20, + "description": "mozStorage async requests completion (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "40", + "kind": "boolean", + "description": "mozStorage async requests success *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "STARTUP_MEASUREMENT_ERRORS": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 16, + "description": "Flags errors in startup calculation()" + }, + "NETWORK_DISK_CACHE_OPEN": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Time spent opening disk cache (ms)" + }, + "NETWORK_DISK_CACHE_TRASHRENAME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Time spent renaming bad Cache to Cache.Trash (ms)" + }, + "NETWORK_DISK_CACHE_DELETEDIR": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Time spent deleting disk cache (ms)" + }, + "NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Time spent during showdown stopping thread deleting old disk cache (ms)" + }, + "NETWORK_DISK_CACHE_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Total Time spent (ms) during disk cache showdown" + }, + "NETWORK_DISK_CACHE_SHUTDOWN_V2": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Total Time spent (ms) during disk cache showdown [cache2]" + }, + "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Time spent (ms) during showdown deleting disk cache for 'clear private data' option" + }, + "NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 10, + "description": "Time spent (ms) during showdown deleting disk cache v2 for 'clear private data' option" + }, + "NETWORK_ID": { + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1240932], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 6, + "description": "Network identification (0=None, 1=New, 2=Same)" + }, + "IDLE_NOTIFY_IDLE_MS": { + "alert_emails": ["froydnj@mozilla.com"], + "bug_numbers": [731004], + "expires_in_version": "default", + "kind": "exponential", + "high": 5000, + "n_buckets": 10, + "description": "Time spent checking for and notifying listeners that the user is idle (ms)" + }, + "URLCLASSIFIER_LOOKUP_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 500, + "n_buckets": 10, + "description": "Time spent per dbservice lookup (ms)" + }, + "URLCLASSIFIER_SHUTDOWN_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "58", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "bug_numbers": [1315140], + "description": "Time spent per dbservice shutdown (ms)" + }, + "URLCLASSIFIER_CL_CHECK_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 500, + "n_buckets": 10, + "description": "Time spent per classifier lookup (ms)" + }, + "URLCLASSIFIER_CL_UPDATE_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "low": 20, + "high": 15000, + "n_buckets": 15, + "description": "Time spent per classifier update (ms)" + }, + "URLCLASSIFIER_PS_FILELOAD_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "description": "Time spent loading PrefixSet from file (ms)" + }, + "URLCLASSIFIER_PS_FALLOCATE_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "description": "Time spent fallocating PrefixSet (ms)" + }, + "URLCLASSIFIER_PS_CONSTRUCT_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 15, + "description": "Time spent constructing PrefixSet from DB (ms)" + }, + "URLCLASSIFIER_VLPS_FILELOAD_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "58", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "bug_numbers": [1283007], + "description": "Time spent loading Variable-Length PrefixSet from file (ms)" + }, + "URLCLASSIFIER_VLPS_FALLOCATE_TIME": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "58", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "bug_numbers": [1283007], + "description": "Time spent fallocating Variable-Length PrefixSet (ms)" + }, + "URLCLASSIFIER_VLPS_LOAD_CORRUPT": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "58", + "kind": "boolean", + "bug_numbers": [1305581], + "description": "Whether or not a variable-length prefix set loaded from disk is corrupted (true = file corrupted)." + }, + "URLCLASSIFIER_LC_PREFIXES": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "high": 1500000, + "n_buckets": 15, + "description": "Size of the prefix cache in entries" + }, + "URLCLASSIFIER_LC_COMPLETIONS": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 200, + "n_buckets": 10, + "description": "Size of the completion cache in entries" + }, + "URLCLASSIFIER_UPDATE_REMOTE_STATUS": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "bug_numbers": [1150921], + "description": "Server HTTP status code from SafeBrowsing database updates. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other)" + }, + "URLCLASSIFIER_COMPLETE_REMOTE_STATUS": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "bug_numbers": [1150921], + "description": "Server HTTP status code from remote SafeBrowsing gethash lookups. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other)" + }, + "URLCLASSIFIER_COMPLETE_TIMEOUT": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "56", + "kind": "boolean", + "bug_numbers": [1172688], + "description": "This metric is recorded every time a gethash lookup is performed, `true` is recorded if the lookup times out." + }, + "URLCLASSIFIER_UPDATE_ERROR_TYPE": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "58", + "kind": "enumerated", + "n_values": 10, + "bug_numbers": [1305801], + "description": "An error was encountered while parsing a partial update returned by a Safe Browsing V4 server (0 = addition of an already existing prefix, 1 = parser got into an infinite loop, 2 = removal index out of bounds, 3 = checksum mismatch, 4 = missing checksum)" + }, + "URLCLASSIFIER_PREFIX_MATCH": { + "alert_emails": ["safebrowsing-telemetry@mozilla.org"], + "expires_in_version": "58", + "kind": "enumerated", + "n_values": 4, + "bug_numbers": [1298257], + "description": "Classifier prefix matching result (0 = no match, 1 = match only V2, 2 = match only V4, 3 = match both V2 and V4)" + }, + "CSP_DOCUMENTS_COUNT": { + "alert_emails": ["seceng@mozilla.com"], + "bug_numbers": [1252829], + "expires_in_version": "55", + "kind": "count", + "description": "Number of unique pages that contain a CSP" + }, + "CSP_UNSAFE_INLINE_DOCUMENTS_COUNT": { + "alert_emails": ["seceng@mozilla.com"], + "bug_numbers": [1252829], + "expires_in_version": "55", + "kind": "count", + "description": "Number of unique pages that contain an unsafe-inline CSP directive" + }, + "CSP_UNSAFE_EVAL_DOCUMENTS_COUNT": { + "alert_emails": ["seceng@mozilla.com"], + "bug_numbers": [1252829], + "expires_in_version": "55", + "kind": "count", + "description": "Number of unique pages that contain an unsafe-eval CSP directive" + }, + "PLACES_DATABASE_CORRUPTION_HANDLING_STAGE": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1356812], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 6, + "releaseChannelCollection": "opt-out", + "description": "PLACES: stage reached when trying to fix a database corruption , see Places::Database::eCorruptDBReplaceStatus" + }, + "PLACES_PAGES_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "low": 1000, + "high": 150000, + "n_buckets": 20, + "releaseChannelCollection": "opt-out", + "description": "PLACES: Number of unique pages" + }, + "PLACES_MOST_RECENT_EXPIRED_VISIT_DAYS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "linear", + "low": 30, + "high": 730, + "n_buckets": 12, + "description": "PLACES: the most recent expired visit in days" + }, + "PLACES_BOOKMARKS_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "low": 100, + "high": 8000, + "n_buckets": 15, + "releaseChannelCollection": "opt-out", + "description": "PLACES: Number of bookmarks" + }, + "PLACES_TAGS_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 200, + "n_buckets": 10, + "description": "PLACES: Number of tags" + }, + "PLACES_KEYWORDS_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 200, + "n_buckets": 10, + "description": "PLACES: Number of keywords" + }, + "PLACES_BACKUPS_DAYSFROMLAST": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 15, + "description": "PLACES: Days from last backup" + }, + "PLACES_BACKUPS_BOOKMARKSTREE_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 2000, + "n_buckets": 10, + "description": "PLACES: Time to build the bookmarks tree" + }, + "PLACES_BACKUPS_TOJSON_MS": { + "expires_in_version": "default", + "kind": "exponential", + "low": 50, + "high": 2000, + "n_buckets": 10, + "description": "PLACES: Time to convert and write the backup" + }, + "PLACES_EXPORT_TOHTML_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 2000, + "n_buckets": 10, + "description": "PLACES: Time to convert and write bookmarks.html" + }, + "PLACES_FAVICON_ICO_SIZES": { + "expires_in_version" : "never", + "kind": "exponential", + "high": 524288, + "n_buckets" : 100, + "description": "PLACES: Size of the ICO favicon files loaded from the web (Bytes)" + }, + "PLACES_FAVICON_PNG_SIZES": { + "expires_in_version" : "never", + "kind": "exponential", + "high": 524288, + "n_buckets" : 100, + "description": "PLACES: Size of the PNG favicon files loaded from the web (Bytes)" + }, + "PLACES_FAVICON_GIF_SIZES": { + "expires_in_version" : "never", + "kind": "exponential", + "high": 524288, + "n_buckets" : 100, + "description": "PLACES: Size of the GIF favicon files loaded from the web (Bytes)" + }, + "PLACES_FAVICON_JPEG_SIZES": { + "expires_in_version" : "never", + "kind": "exponential", + "high": 524288, + "n_buckets" : 100, + "description": "PLACES: Size of the JPEG favicon files loaded from the web (Bytes)" + }, + "PLACES_FAVICON_BMP_SIZES": { + "expires_in_version" : "never", + "kind": "exponential", + "high": 524288, + "n_buckets" : 100, + "description": "PLACES: Size of the BMP favicon files loaded from the web (Bytes)" + }, + "PLACES_FAVICON_SVG_SIZES": { + "expires_in_version" : "never", + "kind": "exponential", + "high": 524288, + "n_buckets" : 100, + "description": "PLACES: Size of the SVG favicon files loaded from the web (Bytes)" + }, + "PLACES_FAVICON_OTHER_SIZES": { + "expires_in_version" : "never", + "kind": "exponential", + "high": 524288, + "n_buckets" : 100, + "description": "PLACES: Size of favicon files without a specific file type probe, loaded from the web (Bytes)" + }, + "LINK_ICON_SIZES_ATTR_USAGE": { + "expires_in_version" : "never", + "kind": "enumerated", + "n_values": 4, + "description": "The possible types of the 'sizes' attribute for . 0: Attribute not specified, 1: 'any', 2: Integer dimensions, 3: Invalid value." + }, + "LINK_ICON_SIZES_ATTR_DIMENSION": { + "expires_in_version" : "never", + "kind": "linear", + "high": 513, + "n_buckets" : 64, + "description": "The width dimension of the 'sizes' attribute for ." + }, + "FENNEC_DISTRIBUTION_REFERRER_INVALID": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the referrer intent specified an invalid distribution name", + "cpp_guard": "ANDROID" + }, + "FENNEC_DISTRIBUTION_CODE_CATEGORY": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "description": "First digit of HTTP result code, or error category, during distribution download", + "cpp_guard": "ANDROID" + }, + "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 100, + "high": 40000, + "n_buckets": 30, + "description": "Time taken to download a specified distribution file (msec)", + "cpp_guard": "ANDROID" + }, + "FENNEC_BOOKMARKS_COUNT": { + "expires_in_version": "60", + "kind": "exponential", + "high": 8000, + "n_buckets": 20, + "description": "Number of bookmarks stored in the browser DB", + "alert_emails": ["mobile-frontend@mozilla.com"], + "bug_numbers": [1244704] + }, + "FENNEC_ORBOT_INSTALLED": { + "expires_in_version": "60", + "kind": "flag", + "cpp_guard": "ANDROID", + "description": "Whether or not users have Orbot installed", + "alert_emails": ["seceng@mozilla.org"], + "bug_numbers": [1314784] + }, + "FENNEC_READING_LIST_COUNT": { + "expires_in_version": "50", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "cpp_guard": "ANDROID", + "description": "Number of reading list items stored in the browser DB *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "FENNEC_READER_VIEW_CACHE_SIZE": { + "expires_in_version": "60", + "alert_emails": ["mobile-frontend@mozilla.com"], + "kind": "exponential", + "low": 32, + "high": 51200, + "n_buckets": 20, + "description": "Total disk space used by items in the reader view cache (KB)", + "bug_numbers": [1246159] + }, + "PLACES_SORTED_BOOKMARKS_PERC": { + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 10, + "description": "PLACES: Percentage of bookmarks organized in folders" + }, + "PLACES_TAGGED_BOOKMARKS_PERC": { + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 10, + "description": "PLACES: Percentage of tagged bookmarks" + }, + "PLACES_DATABASE_FILESIZE_MB": { + "expires_in_version": "never", + "kind": "exponential", + "low": 5, + "high": 200, + "n_buckets": 10, + "description": "PLACES: Database filesize (MB)" + }, + "PLACES_DATABASE_PAGESIZE_B": { + "expires_in_version": "never", + "kind": "exponential", + "low": 1024, + "high": 32768, + "n_buckets": 10, + "description": "PLACES: Database page size (bytes)" + }, + "PLACES_DATABASE_SIZE_PER_PAGE_B": { + "expires_in_version": "never", + "kind": "exponential", + "low": 500, + "high": 10240, + "n_buckets": 20, + "description": "PLACES: Average size of a place in the database (bytes)" + }, + "PLACES_EXPIRATION_STEPS_TO_CLEAN2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "PLACES: Expiration steps to cleanup the database" + }, + "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 500, + "n_buckets": 10, + "description": "PLACES: Time for first autocomplete result if > 50ms (ms)" + }, + "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 1000, + "n_buckets": 30, + "description": "PLACES: Time for the 6 first autocomplete results (ms)" + }, + "HISTORY_LASTVISITED_TREE_QUERY_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 2000, + "n_buckets": 30, + "description": "PLACES: Time to load the sidebar history tree sorted by last visit (ms)" + }, + "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 1000, + "n_buckets": 30, + "description": "PLACES: Time to search the history library (ms)" + }, + "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 2000, + "n_buckets": 10, + "description": "PLACES: Duration of the domain query for the url inline autocompletion (ms)" + }, + "PLACES_IDLE_FRECENCY_DECAY_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 10000, + "n_buckets": 10, + "description": "PLACES: Time to decay all frecencies values on idle (ms)" + }, + "PLACES_IDLE_MAINTENANCE_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 1000, + "high": 30000, + "n_buckets": 10, + "description": "PLACES: Time to execute maintenance tasks on idle (ms)" + }, + "PLACES_ANNOS_BOOKMARKS_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 5000, + "n_buckets": 10, + "description": "PLACES: Number of bookmarks annotations" + }, + "PLACES_ANNOS_PAGES_COUNT": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 5000, + "n_buckets": 10, + "description": "PLACES: Number of pages annotations" + }, + "PLACES_MAINTENANCE_DAYSFROMLAST": { + "expires_in_version" : "never", + "kind": "exponential", + "low": 7, + "high": 60, + "n_buckets" : 10, + "description": "PLACES: Days from last maintenance" + }, + "UPDATE_CHECK_NO_UPDATE_EXTERNAL" : { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of no updates were found for a background update check (externally initiated)" + }, + "UPDATE_CHECK_NO_UPDATE_NOTIFY" : { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of no updates were found for a background update check (timer initiated)" + }, + "UPDATE_CHECK_CODE_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "releaseChannelCollection": "opt-out", + "description": "Update: background update check result code except for no updates found (externally initiated)" + }, + "UPDATE_CHECK_CODE_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "releaseChannelCollection": "opt-out", + "description": "Update: background update check result code except for no updates found (timer initiated)" + }, + "UPDATE_CHECK_EXTENDED_ERROR_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Update: keyed count (key names are prefixed with AUS_CHECK_EX_ERR_) of background update check extended error code (externally initiated)" + }, + "UPDATE_CHECK_EXTENDED_ERROR_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Update: keyed count (key names are prefixed with AUS_CHECK_EX_ERR_) of background update check extended error code (timer initiated)" + }, + "UPDATE_INVALID_LASTUPDATETIME_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that have a last update time greater than the current time (externally initiated)" + }, + "UPDATE_INVALID_LASTUPDATETIME_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that have a last update time greater than the current time (timer initiated)" + }, + "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "n_buckets": 60, + "high": 365, + "releaseChannelCollection": "opt-out", + "description": "Update: interval in days since the last background update check (externally initiated)" + }, + "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "n_buckets": 30, + "high": 180, + "releaseChannelCollection": "opt-out", + "description": "Update: interval in days since the last background update check (timer initiated)" + }, + "UPDATE_PING_COUNT_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems for this ping for comparison with other pings (externally initiated)" + }, + "UPDATE_PING_COUNT_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems for this ping for comparison with other pings (timer initiated)" + }, + "UPDATE_SERVICE_INSTALLED_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "Update: whether the service is installed (externally initiated)" + }, + "UPDATE_SERVICE_INSTALLED_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "Update: whether the service is installed (timer initiated)" + }, + "UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that manually uninstalled the service (externally initiated)" + }, + "UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that manually uninstalled the service (timer initiated)" + }, + "UPDATE_UNABLE_TO_APPLY_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that cannot apply updates (externally initiated)" + }, + "UPDATE_UNABLE_TO_APPLY_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that cannot apply updates (timer initiated)" + }, + "UPDATE_CANNOT_STAGE_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that cannot stage updates (externally initiated)" + }, + "UPDATE_CANNOT_STAGE_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of systems that cannot stage updates (timer initiated)" + }, + "UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: number of sequential update elevation request cancelations greater than 0 (externally initiated)" + }, + "UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: number of sequential update elevation request cancelations greater than 0 (timer initiated)" + }, + "UPDATE_PREF_SERVICE_ERRORS_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 30, + "releaseChannelCollection": "opt-out", + "description": "Update: number of sequential update service errors greater than 0 (externally initiated)" + }, + "UPDATE_PREF_SERVICE_ERRORS_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 30, + "releaseChannelCollection": "opt-out", + "description": "Update: number of sequential update service errors greater than 0 (timer initiated)" + }, + "UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.auto boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.auto boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_NOT_PREF_UPDATE_ENABLED_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.enabled boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_NOT_PREF_UPDATE_ENABLED_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.enabled boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.staging.enabled boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.staging.enabled boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.service.enabled boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Update: count of when the app.update.service.enabled boolean preference is not the default value of true (true values are not submitted)" + }, + "UPDATE_DOWNLOAD_CODE_COMPLETE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "releaseChannelCollection": "opt-out", + "description": "Update: complete patch download result code" + }, + "UPDATE_DOWNLOAD_CODE_PARTIAL": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "releaseChannelCollection": "opt-out", + "description": "Update: complete patch download result code" + }, + "UPDATE_STATE_CODE_COMPLETE_STARTUP": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Update: the state of a complete update from update.status on startup" + }, + "UPDATE_STATE_CODE_PARTIAL_STARTUP": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Update: the state of a partial patch update from update.status on startup" + }, + "UPDATE_STATE_CODE_UNKNOWN_STARTUP": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Update: the state of an unknown patch update from update.status on startup" + }, + "UPDATE_STATE_CODE_COMPLETE_STAGE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Update: the state of a complete patch update from update.status after staging" + }, + "UPDATE_STATE_CODE_PARTIAL_STAGE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Update: the state of a partial patch update from update.status after staging" + }, + "UPDATE_STATE_CODE_UNKNOWN_STAGE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Update: the state of an unknown patch update from update.status after staging" + }, + "UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: the status error code for a failed complete patch update from update.status on startup" + }, + "UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: the status error code for a failed partial patch update from update.status on startup" + }, + "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: the status error code for a failed unknown patch update from update.status on startup" + }, + "UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: the status error code for a failed complete patch update from update.status after staging" + }, + "UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: the status error code for a failed partial patch update from update.status after staging" + }, + "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Update: the status error code for a failed unknown patch update from update.status after staging" + }, + "UPDATE_WIZ_LAST_PAGE_CODE": { + "alert_emails": ["application-update-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 30, + "releaseChannelCollection": "opt-out", + "description": "Update: the update wizard page displayed when the UI was closed (mapped in toolkit/mozapps/update/UpdateTelemetry.jsm)" + }, + "THUNDERBIRD_GLODA_SIZE_MB": { + "expires_in_version": "never", + "kind": "linear", + "high": 1000, + "n_buckets": 40, + "description": "Gloda: size of global-messages-db.sqlite (MB)" + }, + "THUNDERBIRD_CONVERSATIONS_TIME_TO_2ND_GLODA_QUERY_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 30, + "description": "Conversations: time between the moment we click and the second gloda query returns (ms)" + }, + "THUNDERBIRD_INDEXING_RATE_MSG_PER_S": { + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 20, + "description": "Gloda: indexing rate (message/s)" + }, + "FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE": { + "expires_in_version": "50", + "kind": "exponential", + "high": 1000, + "n_buckets": 30, + "description": "Firefox: Time taken to store the image capture of the page to a canvas, for reuse while swiping through history (ms)." + }, + "FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE": { + "expires_in_version": "50", + "kind": "exponential", + "high": 1000, + "n_buckets": 30, + "description": "Firefox: Time taken to kick off image compression of the canvas that will be used during swiping through history (ms)." + }, + "FX_TAB_ANIM_OPEN_PREVIEW_FRAME_INTERVAL_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 7, + "high": 500, + "n_buckets": 50, + "description": "Average frame interval during tab open animation of about:newtab (preview=on), when other tabs are unaffected" + }, + "FX_TAB_ANIM_OPEN_FRAME_INTERVAL_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 7, + "high": 500, + "n_buckets": 50, + "description": "Average frame interval during tab open animation of about:newtab (preview=off), when other tabs are unaffected" + }, + "FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 7, + "high": 500, + "n_buckets": 50, + "description": "Average frame interval during any tab open/close animation (excluding tabstrip scroll)" + }, + "FX_REFRESH_DRIVER_CHROME_FRAME_DELAY_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "bug_numbers": [1220699], + "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver in the chrome process." + }, + "FX_REFRESH_DRIVER_CONTENT_FRAME_DELAY_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "bug_numbers": [1221674], + "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver in the content process." + }, + "FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "bug_numbers": [1228147], + "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver while scrolling synchronously." + }, + "FX_TAB_SWITCH_UPDATE_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 20, + "description": "Firefox: Time in ms spent updating UI in response to a tab switch" + }, + "FX_TAB_SWITCH_TOTAL_MS": { + "expires_in_version": "56", + "kind": "exponential", + "high": 1000, + "n_buckets": 20, + "releaseChannelCollection": "opt-out", + "description": "Firefox: Time in ms till a tab switch is complete including the first paint" + }, + "FX_TAB_SWITCH_TOTAL_E10S_MS": { + "expires_in_version": "56", + "kind": "exponential", + "high": 1000, + "n_buckets": 20, + "releaseChannelCollection": "opt-out", + "description": "Firefox: Time in ms between tab selection and tab content paint." + }, + "FX_TAB_SWITCH_SPINNER_VISIBLE_MS": { + "expires_in_version": "56", + "kind": "exponential", + "high": 1000, + "n_buckets": 20, + "releaseChannelCollection": "opt-out", + "description": "Firefox: If the spinner interstitial displays during tab switching, records the time in ms the graphic is visible" + }, + "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS": { + "expires_in_version": "56", + "kind": "exponential", + "low": 1000, + "high": 64000, + "n_buckets": 7, + "bug_numbers": [1301104], + "alert_emails": ["mconley@mozilla.com"], + "releaseChannelCollection": "opt-out", + "description": "Firefox: If the spinner interstitial displays during tab switching, records the time in ms the graphic is visible. This probe is similar to FX_TAB_SWITCH_SPINNER_VISIBLE_MS, but is for truly degenerate cases." + }, + "FX_TAB_CLICK_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 1000, + "n_buckets": 20, + "description": "Firefox: Time in ms spent on switching tabs in response to a tab click" + }, + "FX_BOOKMARKS_TOOLBAR_INIT_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 5000, + "n_buckets": 10, + "description": "Firefox: Time to initialize the bookmarks toolbar view (ms)" + }, + "FX_BROWSER_FULLSCREEN_USED": { + "expires_in_version": "46", + "kind": "count", + "description": "The number of times that a session enters browser fullscreen (f11-fullscreen)" + }, + "FX_NEW_WINDOW_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "description": "Firefox: Time taken to open a new browser window (ms)" + }, + "FX_PAGE_LOAD_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "description": "Firefox: Time taken to load a page (ms). This includes all static contents, no dynamic content. Loading of about: pages is not counted." + }, + "FX_TOTAL_TOP_VISITS": { + "expires_in_version": "default", + "kind": "boolean", + "description": "Count the number of times a new top page was starting to load" + }, + "FX_THUMBNAILS_CAPTURE_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 500, + "n_buckets": 15, + "description": "THUMBNAILS: Time (ms) it takes to capture a thumbnail" + }, + "FX_THUMBNAILS_STORE_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 500, + "n_buckets": 15, + "description": "THUMBNAILS: Time (ms) it takes to store a thumbnail in the cache" + }, + "FX_THUMBNAILS_HIT_OR_MISS": { + "expires_in_version": "never", + "kind": "boolean", + "description": "THUMBNAILS: Thumbnail found" + }, + "FX_MIGRATION_ENTRY_POINT": { + "bug_numbers": [731025], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 10, + "releaseChannelCollection": "opt-out", + "description": "Where the migration wizard was entered from. 0=Other/catch-all, 1=first-run, 2=refresh-firefox, 3=Places window, 4=Password manager" + }, + "FX_MIGRATION_SOURCE_BROWSER": { + "bug_numbers": [731025], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 15, + "releaseChannelCollection": "opt-out", + "description": "The browser that data is pulled from. The values correspond to the internal browser ID (see MigrationUtils.jsm)" + }, + "FX_MIGRATION_ERRORS": { + "bug_numbers": [731025], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "keyed": true, + "n_values": 12, + "releaseChannelCollection": "opt-out", + "description": "Errors encountered during migration in buckets defined by the datatype, keyed by the string description of the browser." + }, + "FX_MIGRATION_USAGE": { + "bug_numbers": [731025], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "keyed": true, + "n_values": 12, + "releaseChannelCollection": "opt-out", + "description": "Usage of migration for each datatype when migration is run through the post-firstrun flow which allows individual datatypes, keyed by the string description of the browser." + }, + "FX_MIGRATION_IMPORTED_HOMEPAGE": { + "bug_numbers": [731025, 1298208], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Whether the homepage was imported during browser migration. Only available on release builds during firstrun." + }, + "FX_MIGRATION_BOOKMARKS_IMPORT_MS": { + "bug_numbers": [1289436], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "54", + "kind": "exponential", + "n_buckets": 70, + "high": 100000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "How long it took to import bookmarks from another browser, keyed by the name of the browser." + }, + "FX_MIGRATION_HISTORY_IMPORT_MS": { + "bug_numbers": [1289436], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "54", + "kind": "exponential", + "n_buckets": 70, + "high": 100000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "How long it took to import history from another browser, keyed by the name of the browser." + }, + "FX_MIGRATION_LOGINS_IMPORT_MS": { + "bug_numbers": [1289436], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "54", + "kind": "exponential", + "n_buckets": 70, + "high": 100000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "How long it took to import logins (passwords) from another browser, keyed by the name of the browser." + }, + "FX_MIGRATION_BOOKMARKS_JANK_MS": { + "bug_numbers": [1338522], + "alert_emails": ["dao@mozilla.com"], + "expires_in_version": "58", + "kind": "exponential", + "n_buckets": 20, + "high": 60000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing bookmarks from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably." + }, + "FX_MIGRATION_HISTORY_JANK_MS": { + "bug_numbers": [1338522], + "alert_emails": ["dao@mozilla.com"], + "expires_in_version": "58", + "kind": "exponential", + "n_buckets": 20, + "high": 60000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing history from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably." + }, + "FX_MIGRATION_LOGINS_JANK_MS": { + "bug_numbers": [1338522], + "alert_emails": ["dao@mozilla.com"], + "expires_in_version": "58", + "kind": "exponential", + "n_buckets": 20, + "high": 60000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing logins / passwords from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably." + }, + "FX_MIGRATION_BOOKMARKS_QUANTITY": { + "bug_numbers": [1279501], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "56", + "kind": "exponential", + "n_buckets": 20, + "high": 1000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "How many bookmarks we imported from another browser, keyed by the name of the browser." + }, + "FX_MIGRATION_HISTORY_QUANTITY": { + "bug_numbers": [1279501], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "56", + "kind": "exponential", + "n_buckets": 40, + "high": 10000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "How many history visits we imported from another browser, keyed by the name of the browser." + }, + "FX_MIGRATION_LOGINS_QUANTITY": { + "bug_numbers": [1279501], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "56", + "kind": "exponential", + "n_buckets": 20, + "high": 1000, + "releaseChannelCollection": "opt-out", + "keyed": true, + "description": "How many logins (passwords) we imported from another browser, keyed by the name of the browser." + }, + "FX_STARTUP_MIGRATION_BROWSER_COUNT": { + "bug_numbers": [1275114], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 15, + "releaseChannelCollection": "opt-out", + "description": "Number of browsers from which the user could migrate on initial profile migration. Only available on release builds during firstrun." + }, + "FX_STARTUP_MIGRATION_EXISTING_DEFAULT_BROWSER": { + "bug_numbers": [1275114], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 15, + "releaseChannelCollection": "opt-out", + "description": "The browser that was the default on the initial profile migration. The values correspond to the internal browser ID (see MigrationUtils.jsm)" + }, + "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_PROCESS_SUCCESS": { + "bug_numbers": [1271775], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 27, + "releaseChannelCollection": "opt-out", + "description": "Where automatic migration was attempted, indicates to what degree we succeeded. Values 0-25 indicate progress through the automatic migration sequence, with 25 indicating the migration finished. 26 is only used when the migration produced errors before it finished." + }, + "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_UNDO": { + "bug_numbers": [1283565], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 31, + "releaseChannelCollection": "opt-out", + "description": "Where undo of the automatic migration was attempted, indicates to what degree we succeeded to undo. 0 means we started to undo, 5 means we bailed out from the undo because it was not possible to complete it (there was nothing to undo or the user was signed in to sync). All higher values indicate progression through the undo sequence, with 30 indicating we finished the undo without exceptions in the middle." + }, + "FX_STARTUP_MIGRATION_UNDO_REASON": { + "bug_numbers": [1289906], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "54", + "keyed": true, + "kind": "enumerated", + "n_values": 10, + "releaseChannelCollection": "opt-out", + "description": "Why the undo functionality of an automatic migration was disabled: 0 means we used undo, 1 means the user signed in to sync, 2 means the user created/modified a password, 3 means the user created/modified a bookmark (item or folder), 4 means we showed an undo option repeatedly and the user did not use it, 5 means we showed an undo option and the user actively elected to keep the data. The whole thing is keyed to the identifiers of different browsers (so 'chrome', 'ie', 'edge', 'safari', etc.)." + }, + "FX_STARTUP_MIGRATION_UNDO_OFFERED": { + "bug_numbers": [1309617], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "56", + "kind": "enumerated", + "n_values": 5, + "releaseChannelCollection": "opt-out", + "description": "Indicates we showed a 'would you like to undo this automatic migration?' notification bar. The bucket indicates which nth day we're on (1st/2nd/3rd, by default - 0 would be indicative the pref didn't get set which shouldn't happen). After 3 days on which the notification gets shown, it will get disabled and never shown again." + }, + "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_ERRORCOUNT": { + "bug_numbers": [1333233], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "58", + "keyed": true, + "kind": "exponential", + "n_buckets": 20, + "high": 100, + "releaseChannelCollection": "opt-out", + "description": "Indicates how many errors we find when trying to 'undo' bookmarks import. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc." + }, + "FX_STARTUP_MIGRATION_UNDO_LOGINS_ERRORCOUNT": { + "bug_numbers": [1333233], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "58", + "keyed": true, + "kind": "exponential", + "n_buckets": 20, + "high": 100, + "releaseChannelCollection": "opt-out", + "description": "Indicates how many errors we find when trying to 'undo' login (password) import. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc." + }, + "FX_STARTUP_MIGRATION_UNDO_VISITS_ERRORCOUNT": { + "bug_numbers": [1333233], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "58", + "keyed": true, + "kind": "exponential", + "n_buckets": 20, + "high": 100, + "releaseChannelCollection": "opt-out", + "description": "Indicates how many errors we find when trying to 'undo' history import. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc." + }, + "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_MS": { + "bug_numbers": [1333233], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "58", + "keyed": true, + "kind": "exponential", + "n_buckets": 20, + "high": 60000, + "releaseChannelCollection": "opt-out", + "description": "Indicates how long it took to undo the startup import of bookmarks, in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc." + }, + "FX_STARTUP_MIGRATION_UNDO_LOGINS_MS": { + "bug_numbers": [1333233], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "58", + "keyed": true, + "kind": "exponential", + "n_buckets": 20, + "high": 60000, + "releaseChannelCollection": "opt-out", + "description": "Indicates how long it took to undo the startup import of logins, in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc." + }, + "FX_STARTUP_MIGRATION_UNDO_VISITS_MS": { + "bug_numbers": [1333233], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "58", + "keyed": true, + "kind": "exponential", + "n_buckets": 20, + "high": 60000, + "releaseChannelCollection": "opt-out", + "description": "Indicates how long it took to undo the startup import of visits (history), in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc." + }, + "FX_STARTUP_MIGRATION_UNDO_TOTAL_MS": { + "bug_numbers": [1333233], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "58", + "keyed": true, + "kind": "exponential", + "n_buckets": 20, + "high": 60000, + "releaseChannelCollection": "opt-out", + "description": "Indicates how long it took to undo the entirety of the startup undo, in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc." + }, + "FX_STARTUP_MIGRATION_DATA_RECENCY": { + "bug_numbers": [1276694], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "keyed": true, + "kind": "exponential", + "n_buckets": 50, + "high": 8760, + "releaseChannelCollection": "opt-out", + "description": "The 'last modified' time of the data we imported on the initial profile migration (time delta with 'now' at the time of migration, in hours). Collected for all browsers for which migration data is available, and stored keyed by browser identifier (e.g. 'ie', 'edge', 'safari', etc.)." + }, + "FX_STARTUP_MIGRATION_USED_RECENT_BROWSER": { + "bug_numbers": [1276694], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "53", + "keyed": true, + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "Whether the browser we migrated from was the browser with the most recent data. Keyed by that browser's identifier (e.g. 'ie', 'edge', 'safari', etc.)." + }, + "FX_STARTUP_EXTERNAL_CONTENT_HANDLER": { + "bug_numbers": [1276027], + "alert_emails": ["jaws@mozilla.com"], + "expires_in_version": "53", + "kind": "count", + "description": "Count how often the browser is opened as an external app handler. This is generally used when the browser is set as the default browser." + }, + "FX_PREFERENCES_CATEGORY_OPENED": { + "bug_numbers": [1324167], + "alert_emails": ["jaws@mozilla.com"], + "expires_in_version": "56", + "kind": "categorical", + "labels": ["unknown", "general", "search", "content", "applications", "privacy", "security", "sync", "advancedGeneral", "advancedDataChoices", "advancedNetwork", "advancedUpdates", "advancedCerts"], + "releaseChannelCollection": "opt-out", + "description": "Count how often each preference category is opened." + }, + "INPUT_EVENT_RESPONSE_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "bug_numbers": [1235908], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time (ms) from the Input event being created to the end of it being handled" + }, + "LOAD_INPUT_EVENT_RESPONSE_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "bug_numbers": [1298101], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time (ms) from the Input event being created to the end of it being handled for events handling during page load only" + }, + "EVENTLOOP_UI_ACTIVITY_EXP_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "bug_numbers": [1198196], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Widget: Time it takes for the message before a UI message (ms)" + }, + "FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Session restore: Time it takes to prepare the data structures for restoring a session (ms)" + }, + "FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Session restore: Time it takes to finish restoration once we have first opened a window (ms)" + }, + "FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "Session restore: Time to collect all window data (ms)" + }, + "FX_SESSION_RESTORE_COLLECT_COOKIES_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "Session restore: Time to collect cookies (ms)" + }, + "FX_SESSION_RESTORE_COLLECT_DATA_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "Session restore: Time to collect all window and tab data (ms)" + }, + "FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "Session restore: Duration of the longest uninterruptible operation while collecting all window and tab data (ms)" + }, + "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "high": 30000, + "n_buckets": 10, + "description": "Session restore: Duration of the longest uninterruptible operation while collecting data in the content process (ms)" + }, + "FX_SESSION_RESTORE_SERIALIZE_DATA_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "description": "Session restore: Time to JSON serialize session data (ms)" + }, + "FX_SESSION_RESTORE_READ_FILE_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Session restore: Time to read the session data from the file on disk (ms)" + }, + "FX_SESSION_RESTORE_WRITE_FILE_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Session restore: Time to write the session data to the file on disk (ms)" + }, + "FX_SESSION_RESTORE_FILE_SIZE_BYTES": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "high": 50000000, + "n_buckets": 30, + "description": "Session restore: The size of file sessionstore.js (bytes)" + }, + "FX_SESSION_RESTORE_CORRUPT_FILE": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "boolean", + "description": "Session restore: Whether the file read on startup contained parse-able JSON" + }, + "FX_SESSION_RESTORE_ALL_FILES_CORRUPT": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "boolean", + "description": "Session restore: Whether none of the backup files contained parse-able JSON" + }, + "FX_SESSION_RESTORE_RESTORE_WINDOW_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Session restore: Time spent blocking the main thread while restoring a window state (ms)" + }, + "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "count", + "description": "Count of messages sent by SessionRestore from child frames to the parent and that cannot be transmitted as they eat up too much memory." + }, + "FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 30000000, + "n_buckets": 20, + "description": "Session restore: Number of characters in DOM Storage for a tab. Pages without DOM Storage or with an empty DOM Storage are ignored." + }, + "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "low": 100, + "high": 100000, + "n_buckets": 20, + "description": "Session restore: If the browser is setup to auto-restore tabs, this probe measures the time elapsed between the instant we start Session Restore and the instant we have finished restoring tabs eagerly. At this stage, the tabs that are restored on demand are not restored yet." + }, + "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS": { + "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "expires_in_version": "default", + "kind": "exponential", + "low": 100, + "high": 100000, + "n_buckets": 20, + "description": "Session restore: If a session is restored by the user clicking on 'Restore Session', this probe measures the time elapsed between the instant the user has clicked and the instant we have finished restoring tabs eagerly. At this stage, the tabs that are restored on demand are not restored yet." + }, + "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED": { + "expires_in_version": "default", + "kind": "exponential", + "high": 500, + "n_buckets": 20, + "description": "Session restore: Number of tabs in the session that has just been restored." + }, + "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 50, + "description": "Session restore: Number of windows in the session that has just been restored." + }, + "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 50, + "description": "Session restore: Number of tabs restored eagerly in the session that has just been restored." + }, + "FX_TABLETMODE_PAGE_LOAD": { + "expires_in_version": "47", + "kind": "exponential", + "high": 100000, + "n_buckets": 30, + "keyed": true, + "description": "Number of toplevel location changes in tablet and desktop mode (only used on win10 where tablet mode is available)" + }, + "FX_TOUCH_USED": { + "expires_in_version": "46", + "kind": "count", + "description": "Windows only. Counts occurrences of touch events" + }, + "FX_URLBAR_SELECTED_RESULT_INDEX": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 17, + "bug_numbers": [775825], + "description": "Firefox: The index of the selected result in the URL bar popup" + }, + "FX_URLBAR_SELECTED_RESULT_TYPE": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 14, + "bug_numbers": [775825], + "description": "Firefox: The type of the selected result in the URL bar popup. See nsBrowserGlue.js::_handleURLBarTelemetry for the result types." + }, + "INNERWINDOWS_WITH_MUTATION_LISTENERS": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Deleted or to-be-reused innerwindow which has had mutation event listeners." + }, + "CHARSET_OVERRIDE_SITUATION": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Labeling status of top-level page when overriding charset (0: unlabeled file URL without detection, 1: unlabeled non-TLD-guessed non-file URL without detection, 2: unlabeled file URL with detection, 3: unlabeled non-file URL with detection, 4: labeled, 5: already overridden, 6: bug, 7: unlabeled with TLD guessing)" + }, + "CHARSET_OVERRIDE_USED": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the character encoding menu was used to override an encoding in this session." + }, + "DECODER_INSTANTIATED_ISO2022JP": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for ISO-2022-JP has been instantiated in this session." + }, + "DECODER_INSTANTIATED_IBM866": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for IBM866 has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACGREEK": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACGREEK has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACICELANDIC": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACICELANDIC has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACCE": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACCE has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACHEBREW": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACHEBREW has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACARABIC": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACARABIC has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACFARSI": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACFARSI has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACCROATIAN": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACCROATIAN has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACCYRILLIC": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACCYRILLIC has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACROMANIAN": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACROMANIAN has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACTURKISH": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACTURKISH has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACDEVANAGARI": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACDEVANAGARI has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACGUJARATI": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACGUJARATI has been instantiated in this session." + }, + "DECODER_INSTANTIATED_MACGURMUKHI": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for MACGURMUKHI has been instantiated in this session." + }, + "DECODER_INSTANTIATED_KOI8R": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for KOI8R has been instantiated in this session." + }, + "DECODER_INSTANTIATED_KOI8U": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for KOI8U has been instantiated in this session." + }, + "DECODER_INSTANTIATED_ISO_8859_5": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the decoder for ISO-8859-5 has been instantiated in this session." + }, + "LONG_REFLOW_INTERRUPTIBLE": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Long running reflow, interruptible or not" + }, + "XMLHTTPREQUEST_ASYNC_OR_SYNC": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Type of XMLHttpRequest, async or sync" + }, + "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to flush and close the localStorage database (ms)" + }, + "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS": { + "expires_in_version": "default", + "kind": "boolean", + "description": "True when we had to wait for a pending preload on first access to localStorage data, false otherwise" + }, + "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to block before we return a list of all keys in domain's LocalStorage (ms)" + }, + "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to block before we return a key name in domain's LocalStorage (ms)" + }, + "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to block before we return number of keys in domain's LocalStorage (ms)" + }, + "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to block before we return a value for a key in LocalStorage (ms)" + }, + "LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to block before we set a single key's value in LocalStorage (ms)" + }, + "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to block before we remove a single key from LocalStorage (ms)" + }, + "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to block before we clear LocalStorage for all domains (ms)" + }, + "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to fetch LocalStorage data before we can clean the cache (ms)" + }, + "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS": { + "expires_in_version": "40", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to fetch LocalStorage data before we can expose them as session only data (ms)" + }, + "RANGE_CHECKSUM_ERRORS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Number of histograms with range checksum errors" + }, + "BUCKET_ORDER_ERRORS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Number of histograms with bucket order errors" + }, + "TOTAL_COUNT_HIGH_ERRORS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Number of histograms with total count high errors" + }, + "TOTAL_COUNT_LOW_ERRORS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Number of histograms with total count low errors" + }, + "TELEMETRY_ARCHIVE_DIRECTORIES_COUNT": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 13, + "n_buckets": 12, + "bug_numbers": [1162538], + "description": "Number of directories in the archive at scan" + }, + "TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 13, + "n_buckets": 12, + "bug_numbers": [1162538], + "description": "The age of the oldest Telemetry archive directory in months" + }, + "TELEMETRY_ARCHIVE_SCAN_PING_COUNT": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 100, + "bug_numbers": [1162538], + "description": "Number of Telemetry pings in the archive at scan" + }, + "TELEMETRY_ARCHIVE_SESSION_PING_COUNT": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1162538], + "description": "Number of Telemetry pings added to the archive during the session" + }, + "TELEMETRY_ARCHIVE_SIZE_MB": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 300, + "n_buckets": 60, + "bug_numbers": [1162538], + "description": "The size of the Telemetry archive (MB)" + }, + "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 100, + "bug_numbers": [1162538], + "description": "Number of Telemetry pings evicted from the archive during cleanup, because they were over the quota" + }, + "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 13, + "n_buckets": 12, + "bug_numbers": [1162538], + "description": "Number of Telemetry directories evicted from the archive during cleanup, because they were too old" + }, + "TELEMETRY_ARCHIVE_EVICTING_DIRS_MS": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 20, + "bug_numbers": [1162538], + "description": "Time (ms) it takes for evicting old directories" + }, + "TELEMETRY_ARCHIVE_CHECKING_OVER_QUOTA_MS": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 20, + "bug_numbers": [1162538], + "description": "Time (ms) it takes for checking if the archive is over-quota" + }, + "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 20, + "bug_numbers": [1162538], + "description": "Time (ms) it takes for evicting over-quota pings" + }, + "TELEMETRY_PENDING_LOAD_FAILURE_READ": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Number of pending Telemetry pings that failed to load from the disk" + }, + "TELEMETRY_PENDING_LOAD_FAILURE_PARSE": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Number of pending Telemetry pings that failed to parse once loaded from the disk" + }, + "TELEMETRY_PENDING_PINGS_SIZE_MB": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 17, + "n_buckets": 16, + "description": "The size of the Telemetry pending pings directory (MB). The special value 17 is used to indicate over quota pings." + }, + "TELEMETRY_PENDING_PINGS_AGE": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 365, + "n_buckets": 30, + "description": "The age, in days, of the pending pings." + }, + "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 100, + "description": "Number of Telemetry pings evicted from the pending pings directory during cleanup, because they were over the quota" + }, + "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 20, + "description": "Time (ms) it takes for evicting over-quota pending pings" + }, + "TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 300000, + "n_buckets": 20, + "description": "Time (ms) it takes for checking if the pending pings are over-quota" + }, + "TELEMETRY_PING_SIZE_EXCEEDED_SEND": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Number of Telemetry pings discarded before sending because they exceeded the maximum size" + }, + "TELEMETRY_PING_SIZE_EXCEEDED_PENDING": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Number of Telemetry pending pings discarded because they exceeded the maximum size" + }, + "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Number of archived Telemetry pings discarded because they exceeded the maximum size" + }, + "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "bug_numbers": [1233986], + "description": "The number of pings that were submitted and had to wait for a client id (i.e. before it was cached or loaded from disk)" + }, + "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 30, + "n_buckets": 29, + "description": "The size (MB) of the Telemetry pending pings exceeding the maximum file size" + }, + "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 30, + "n_buckets": 29, + "description": "The size (MB) of the Telemetry archived, compressed, pings exceeding the maximum file size" + }, + "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 30, + "n_buckets": 29, + "description": "The size (MB) of the ping data submitted to Telemetry exceeding the maximum size" + }, + "TELEMETRY_DISCARDED_CONTENT_PINGS_COUNT": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Count of discarded content payloads." + }, + "TELEMETRY_COMPRESS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time taken to compress telemetry object (ms)" + }, + "TELEMETRY_SEND_SUCCESS" : { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1318284], + "expires_in_version": "never", + "kind": "exponential", + "high": 120000, + "n_buckets": 20, + "description": "Time needed (in ms) for a successful send of a Telemetry ping to the servers and getting a reply back." + }, + "TELEMETRY_SEND_FAILURE" : { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1318284], + "expires_in_version": "never", + "kind": "exponential", + "high": 120000, + "n_buckets": 20, + "description": "Time needed (in ms) for a failed send of a Telemetry ping to the servers and getting a reply back." + }, + "TELEMETRY_STRINGIFY" : { + "expires_in_version": "never", + "kind": "exponential", + "high": 3000, + "n_buckets": 10, + "description": "Time to stringify telemetry object (ms)" + }, + "TELEMETRY_SUCCESS": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Successful telemetry submission" + }, + "TELEMETRY_INVALID_PING_TYPE_SUBMITTED": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "Count of individual invalid ping types that were submitted to Telemetry." + }, + "TELEMETRY_INVALID_PAYLOAD_SUBMITTED": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "bug_numbers": [1292226], + "kind": "count", + "description": "Count of individual invalid payloads that were submitted to Telemetry." + }, + "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Number of Telemetry ping files evicted due to server errors (4XX HTTP code received)" + }, + "TELEMETRY_SESSIONDATA_FAILED_LOAD": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if Telemetry failed to load the session data from disk." + }, + "TELEMETRY_SESSIONDATA_FAILED_PARSE": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if Telemetry failed to parse the session data loaded from disk." + }, + "TELEMETRY_SESSIONDATA_FAILED_VALIDATION": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if Telemetry failed to validate the session data loaded from disk." + }, + "TELEMETRY_SESSIONDATA_FAILED_SAVE": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if Telemetry failed to save the session data to disk." + }, + "TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1250640], + "expires_in_version": "53", + "kind": "count", + "description": "Count of exceptions in TelemetrySession.getSessionPayload()." + }, + "TELEMETRY_SCHEDULER_TICK_EXCEPTION": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1250640], + "expires_in_version": "53", + "kind": "count", + "description": "Count of exceptions during executing the TelemetrySession scheduler tick logic." + }, + "TELEMETRY_SCHEDULER_WAKEUP": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1250640], + "expires_in_version": "53", + "kind": "count", + "description": "Count of TelemetrySession scheduler ticks that were delayed long enough to suspect sleep." + }, + "TELEMETRY_SCHEDULER_SEND_DAILY": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1250640], + "expires_in_version": "53", + "kind": "count", + "description": "Count of TelemetrySession triggering a daily ping." + }, + "TELEMETRY_TEST_FLAG": { + "expires_in_version": "never", + "kind": "flag", + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_COUNT": { + "expires_in_version": "never", + "kind": "count", + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_COUNT2": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1288745], + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_COUNT_INIT_NO_RECORD": { + "expires_in_version": "never", + "kind": "count", + "description": "a testing histogram; not meant to be touched - initially not recording" + }, + "TELEMETRY_TEST_CATEGORICAL": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1188888], + "expires_in_version": "never", + "kind": "categorical", + "labels": [ + "CommonLabel", + "Label2", + "Label3" + ], + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_CATEGORICAL_OPTOUT": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "bug_numbers": [1188888], + "expires_in_version": "never", + "releaseChannelCollection": "opt-out", + "kind": "categorical", + "labels": [ + "CommonLabel", + "Label4", + "Label5", + "Label6" + ], + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD": { + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "a testing histogram; not meant to be touched - initially not recording" + }, + "TELEMETRY_TEST_KEYED_FLAG": { + "expires_in_version": "never", + "kind": "flag", + "keyed": true, + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_KEYED_COUNT": { + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_KEYED_BOOLEAN": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "bug_numbers": [1299144], + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_RELEASE_OPTOUT": { + "expires_in_version": "never", + "kind": "flag", + "releaseChannelCollection": "opt-out", + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_RELEASE_OPTIN": { + "expires_in_version": "never", + "kind": "flag", + "releaseChannelCollection": "opt-in", + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_KEYED_RELEASE_OPTIN": { + "expires_in_version": "never", + "kind": "flag", + "keyed": true, + "releaseChannelCollection": "opt-in", + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT": { + "expires_in_version": "never", + "kind": "flag", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_EXPONENTIAL": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "low": 1, + "high": 2147483646, + "n_buckets": 10, + "bug_numbers": [1288745], + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_LINEAR": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "low": 1, + "high": 2147483646, + "n_buckets": 10, + "bug_numbers": [1288745], + "description": "a testing histogram; not meant to be touched" + }, + "TELEMETRY_TEST_BOOLEAN": { + "alert_emails": ["telemetry-client-dev@mozilla.com"], + "expires_in_version" : "never", + "kind": "boolean", + "bug_numbers": [1288745], + "description": "a testing histogram; not meant to be touched" + }, + "STARTUP_CRASH_DETECTED": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether there was a crash during the last startup" + }, + "SAFE_MODE_USAGE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "Whether the user is in safe mode (No, Yes, Forced)" + }, + "SCRIPT_BLOCK_INCORRECT_MIME": { + "alert_emails": ["ckerschbaumer@mozilla.com"], + "bug_numbers": [1288361, 1299267], + "expires_in_version": "56", + "kind": "enumerated", + "n_values": 15, + "description": "Whether the script load has a MIME type of ...? (0=unknown, 1=js, 2=image, 3=audio, 4=video, 5=text/plain, 6=text/csv, 7=text/xml, 8=application/octet-stream, 9=application/xml, 10=text/html, 11=empty)" + }, + "XCTO_NOSNIFF_BLOCK_IMAGE": { + "alert_emails": ["ckerschbaumer@mozilla.com"], + "bug_numbers": [1302539], + "expires_in_version": "56", + "kind": "enumerated", + "n_values": 3, + "description": "Whether XCTO: nosniff would allow/block an image load? (0=allow, 1=block)" + }, + "NEWTAB_PAGE_ENABLED": { + "expires_in_version": "default", + "kind": "boolean", + "description": "New tab page is enabled." + }, + "NEWTAB_PAGE_ENHANCED": { + "expires_in_version": "default", + "kind": "boolean", + "description": "New tab page is enhanced (showing suggestions)." + }, + "NEWTAB_PAGE_LIFE_SPAN": { + "expires_in_version": "default", + "kind": "exponential", + "high": 1200, + "n_buckets": 100, + "description": "Life-span of a new tab without suggested tile: time delta between first-visible and unload events (half-seconds)." + }, + "NEWTAB_PAGE_LIFE_SPAN_SUGGESTED": { + "expires_in_version": "default", + "kind": "exponential", + "high": 1200, + "n_buckets": 100, + "description": "Life-span of a new tab with suggested tile: time delta between first-visible and unload events (half-seconds)." + }, + "NEWTAB_PAGE_PINNED_SITES_COUNT": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 9, + "description": "Number of pinned sites on the new tab page." + }, + "NEWTAB_PAGE_BLOCKED_SITES_COUNT": { + "expires_in_version": "default", + "kind": "exponential", + "high": 100, + "n_buckets": 10, + "description": "Number of sites blocked from the new tab page." + }, + "NEWTAB_PAGE_SHOWN": { + "expires_in_version": "35", + "kind": "boolean", + "description": "Number of times about:newtab was shown from opening a new tab or window. *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "NEWTAB_PAGE_SITE_CLICKED": { + "expires_in_version": "35", + "kind": "enumerated", + "n_values": 10, + "description": "Track click count on about:newtab tiles per index (0-8). For non-default row or column configurations all clicks into the '9' bucket. *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + }, + "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 50000, + "n_buckets": 20, + "description": "Number of bookmarks in the original XUL places database", + "cpp_guard": "ANDROID" + }, + "FENNEC_GLOBALHISTORY_ADD_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 10, + "high": 20000, + "n_buckets": 20, + "description": "Time for a record to be added to history (ms)", + "cpp_guard": "ANDROID" + }, + "FENNEC_GLOBALHISTORY_UPDATE_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 10, + "high": 20000, + "n_buckets": 20, + "description": "Time for a record to be updated in history (ms)", + "cpp_guard": "ANDROID" + }, + "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 10, + "high": 20000, + "n_buckets": 20, + "description": "Time to update the visited link set (ms)", + "cpp_guard": "ANDROID" + }, + "FENNEC_RESTORING_ACTIVITY": { + "expires_in_version": "never", + "kind": "flag", + "description": "Fennec is starting up but the Gecko thread was still running", + "cpp_guard": "ANDROID" + }, + "FENNEC_SEARCH_LOADER_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 10, + "high": 20000, + "n_buckets": 20, + "description": "Time for a URL bar DB search to return (ms)", + "cpp_guard": "ANDROID" + }, + "FENNEC_STARTUP_TIME_GECKOREADY": { + "expires_in_version": "never", + "kind": "exponential", + "low": 500, + "high": 20000, + "n_buckets": 20, + "description": "Time for the Gecko:Ready message to arrive (ms)", + "cpp_guard": "ANDROID" + }, + "FENNEC_STARTUP_TIME_JAVAUI": { + "expires_in_version": "never", + "kind": "exponential", + "low": 100, + "high": 5000, + "n_buckets": 20, + "description": "Time for the Java UI to load (ms)", + "cpp_guard": "ANDROID" + }, + "FENNEC_TOPSITES_LOADER_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 10, + "high": 20000, + "n_buckets": 20, + "description": "Time for the home screen Top Sites query to return with no filter set (ms)", + "cpp_guard": "ANDROID" + }, + "FENNEC_ACTIVITY_STREAM_TOPSITES_LOADER_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 10, + "high": 20000, + "n_buckets": 20, + "description": "Time for the Activity Stream home screen Top Sites query to return (ms)", + "alert_emails": ["mobile-frontend@mozilla.com"], + "bug_numbers": [1293790], + "cpp_guard": "ANDROID" + }, + "FENNEC_HOMEPANELS_CUSTOM": { + "expires_in_version": "54", + "kind": "boolean", + "bug_numbers": [1245368], + "description": "Whether the user has customized their homepanels", + "cpp_guard": "ANDROID" + }, + "FENNEC_WAS_KILLED": { + "expires_in_version": "never", + "kind": "flag", + "description": "Killed, likely due to an OOM condition", + "cpp_guard": "ANDROID" + }, + "FIPS_ENABLED": { + "alert_emails": ["seceng@mozilla.org"], + "expires_in_version": "54", + "kind": "flag", + "bug_numbers": [1241317], + "releaseChannelCollection": "opt-out", + "description": "Has FIPS mode been enabled?" + }, + "SECURITY_UI": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "description": "Security UI Telemetry" + }, + "JS_TELEMETRY_ADDON_EXCEPTIONS" : { + "expires_in_version" : "never", + "kind": "count", + "keyed" : true, + "description" : "Exceptions thrown by add-ons" + }, + "IPC_TRANSACTION_CANCEL": { + "alert_emails": ["billm@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "True when an IPC transaction is canceled" + }, + "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB": { + "expires_in_version": "50", + "kind": "exponential", + "low": 100, + "high": 10000000, + "n_buckets": 10, + "description": "Whenever the same-process MessageManager cannot be sent through sendAsyncMessage as it would cause an OOM, the size of the message content, in kb." + }, + "SLOW_ADDON_WARNING_STATES": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "description": "The states the Slow Add-on Warning goes through. 0: Displayed the warning. 1: User clicked on 'Disable add-on'. 2: User clicked 'Ignore add-on for now'. 3: User clicked 'Ignore add-on permanently'. 4: User closed notification. Other values are reserved for future uses." + }, + "SLOW_ADDON_WARNING_RESPONSE_TIME": { + "expires_in_version": "never", + "kind": "exponential", + "high": 86400000, + "n_buckets": 30, + "description": "Time elapsed between before responding to Slow Add-on Warning UI (ms). Not updated if the user doesn't respond at all." + }, + "SEARCH_COUNTS": { + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Record the search counts for search engines" + }, + "SEARCH_RESET_RESULT": { + "alert_emails": ["fqueze@mozilla.com"], + "bug_numbers": [1203168], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 5, + "releaseChannelCollection": "opt-out", + "description": "Result of showing the search reset prompt to the user. 0=restored original default, 1=kept current engine, 2=changed engine, 3=closed the page, 4=opened search settings" + }, + "SEARCH_SERVICE_INIT_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 15, + "description": "Time (ms) it takes to initialize the search service" + }, + "SEARCH_SERVICE_INIT_SYNC": { + "alert_emails": ["rvitillo@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "search service has been initialized synchronously" + }, + "SEARCH_SERVICE_ENGINE_COUNT": { + "releaseChannelCollection": "opt-out", + "alert_emails": ["florian@mozilla.com"], + "expires_in_version": "55", + "bug_numbers": [1268424], + "kind": "linear", + "high": 200, + "n_buckets": 50, + "description": "Recorded once per session near startup: records the search plugin count, including both built-in plugins (including the ones the user has hidden) and user-installed plugins." + }, + "SEARCH_SERVICE_HAS_UPDATES": { + "alert_emails": ["florian@mozilla.com"], + "expires_in_version": "55", + "kind": "boolean", + "bug_numbers": [1259510], + "description": "Recorded once per session near startup: records true/false whether the search service has engines with update URLs.", + "releaseChannelCollection": "opt-out" + }, + "SEARCH_SERVICE_HAS_ICON_UPDATES": { + "alert_emails": ["florian@mozilla.com"], + "expires_in_version": "55", + "kind": "boolean", + "bug_numbers": [1259510], + "description": "Recorded once per session near startup: records true/false whether the search service has engines with icon update URLs.", + "releaseChannelCollection": "opt-out" + }, + "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "n_buckets": 30, + "high": 100000, + "description": "Time (ms) it takes to fetch the country code" + }, + "SEARCH_SERVICE_COUNTRY_FETCH_RESULT": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Result of XHR request fetching the country-code. 0=SUCCESS, 1=SUCCESS_WITHOUT_DATA, 2=XHRTIMEOUT, 3=ERROR (rest reserved for finer-grained error codes later)" + }, + "SEARCH_SERVICE_COUNTRY_TIMEOUT": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "True if we stopped waiting for the XHR response before it completed" + }, + "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "True if the search service was synchronously initialized while we were waiting for the XHR response" + }, + "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if the fetched country-code indicates US but the time-zone heuristic doesn't" + }, + "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "flag", + "description": "Set if the time-zone heuristic indicates US but the fetched country code doesn't" + }, + "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "If we are on OSX and either the OSX countryCode or the geoip countryCode indicates we are in the US, set to false if they both do or true otherwise" + }, + "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "If we are on OSX and neither the OSX countryCode nor the geoip countryCode indicates we are in the US, set to false if they both agree on the value or true otherwise" + }, + "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "If we are on Windows and either the Windows countryCode or the geoip countryCode indicates we are in the US, set to false if they both do or true otherwise" + }, + "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN": { + "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "If we are on Windows and neither the Windows countryCode nor the geoip countryCode indicates we are in the US, set to false if they both agree on the value or true otherwise" + }, + "SOCIAL_ENABLED_ON_SESSION": { + "expires_in_version": "never", + "kind": "flag", + "description": "Social has been enabled at least once on the current session" + }, + "ENABLE_PRIVILEGE_EVER_CALLED": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether enablePrivilege has ever been called during the current session" + }, + "SUBJECT_PRINCIPAL_ACCESSED_WITHOUT_SCRIPT_ON_STACK": { + "expires_in_version": "46", + "alert_emails": ["bholley@mozilla.com"], + "kind": "flag", + "description": "Whether the subject principal was accessed without script on the stack during the current session" + }, + "TOUCH_ENABLED_DEVICE": { + "expires_in_version": "never", + "kind": "boolean", + "description": "The device supports touch input", + "cpp_guard": "XP_WIN" + }, + "COMPONENTS_SHIM_ACCESSED_BY_CONTENT": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether content ever accesed the Components shim in this session" + }, + "CHECK_ADDONS_MODIFIED_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 15, + "description": "Time (ms) it takes to figure out extension last modified time" + }, + "TELEMETRY_MEMORY_REPORTER_MS": { + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 10, + "description": "Time (ms) it takes to run memory reporters when sending a telemetry ping" + }, + "SSL_SUCCESFUL_CERT_VALIDATION_TIME_MOZILLAPKIX" : { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time spent on a successful cert verification in mozilla::pkix mode (ms)" + }, + "SSL_INITIAL_FAILED_CERT_VALIDATION_TIME_MOZILLAPKIX" : { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time spent on an initially failed cert verification in mozilla::pkix mode (ms)" + }, + "CRASH_STORE_COMPRESSED_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 202, + "description": "Size (in bytes) of the compressed crash store JSON file." + }, + "PDF_VIEWER_USED": { + "expires_in_version": "default", + "kind": "boolean", + "description": "How many times PDF Viewer was used" + }, + "PDF_VIEWER_FALLBACK_SHOWN": { + "expires_in_version": "default", + "kind": "boolean", + "description": "How many times PDF Viewer fallback bar was shown" + }, + "PDF_VIEWER_PRINT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "How many times PDF Viewer print functionality was used" + }, + "PDF_VIEWER_DOCUMENT_VERSION": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 20, + "description": "The PDF document version (1.1, 1.2, etc.)" + }, + "PDF_VIEWER_DOCUMENT_GENERATOR": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 30, + "description": "The PDF document generator" + }, + "PDF_VIEWER_DOCUMENT_SIZE_KB": { + "expires_in_version": "default", + "kind": "exponential", + "low": 2, + "high": 65536, + "n_buckets": 20, + "description": "The PDF document size (KB)" + }, + "PDF_VIEWER_FONT_TYPES": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 19, + "description": "The PDF document font types used" + }, + "PDF_VIEWER_EMBED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "A PDF document was embedded: true using OBJECT/EMBED and false using IFRAME" + }, + "PDF_VIEWER_FORM": { + "expires_in_version": "default", + "kind": "boolean", + "description": "A PDF form expected: true for AcroForm and false for XFA" + }, + "PDF_VIEWER_STREAM_TYPES": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 19, + "description": "The PDF document compression stream types used" + }, + "PDF_VIEWER_TIME_TO_VIEW_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to display first page in PDF Viewer (ms)" + }, + "PLUGINS_NOTIFICATION_SHOWN": { + "expires_in_version": "never", + "kind": "boolean", + "description": "The number of times the click-to-activate notification was shown: false: shown by in-content activation true: shown by location bar activation" + }, + "PLUGINS_NOTIFICATION_PLUGIN_COUNT": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "The number of plugins present in the click-to-activate notification, minus one (1, 2, 3, 4, more than 4)" + }, + "PLUGINS_NOTIFICATION_USER_ACTION": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "User actions taken in the plugin notification: 0: allownow 1: allowalways 2: block" + }, + "PLUGINS_INFOBAR_SHOWN": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Count of when the hidden-plugin infobar was displayed." + }, + "PLUGINS_INFOBAR_BLOCK": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Count the number of times the user clicked 'block' on the hidden-plugin infobar." + }, + "PLUGINS_INFOBAR_ALLOW": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Count the number of times the user clicked 'allow' on the hidden-plugin infobar." + }, + "POPUP_NOTIFICATION_STATS": { + "releaseChannelCollection": "opt-out", + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1207089], + "expires_in_version": "55", + "kind": "enumerated", + "keyed": true, + "n_values": 40, + "description": "(Bug 1207089) Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action, 5 = Click outside, 6 = Leave page, 7 = Use 'X', 8 = Not now, 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)" + }, + "POPUP_NOTIFICATION_MAIN_ACTION_MS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1207089], + "expires_in_version": "55", + "kind": "exponential", + "keyed": true, + "low": 100, + "high": 600000, + "n_buckets": 40, + "description": "(Bug 1207089) Time in ms between initially requesting a popup notification and triggering the main action, keyed by ID" + }, + "POPUP_NOTIFICATION_DISMISSAL_MS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1207089], + "expires_in_version": "55", + "kind": "exponential", + "keyed": true, + "low": 200, + "high": 20000, + "n_buckets": 50, + "description": "(Bug 1207089) Time in ms between displaying a popup notification and dismissing it without an action the first time, keyed by ID" + }, + "PRINT_PREVIEW_OPENED_COUNT": { + "alert_emails": ["carnold@mozilla.org"], + "bug_numbers": [1275570], + "expires_in_version": "56", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "A counter incremented every time the browser enters print preview." + }, + "PRINT_PREVIEW_SIMPLIFY_PAGE_OPENED_COUNT": { + "alert_emails": ["carnold@mozilla.org"], + "bug_numbers": [1275570], + "expires_in_version": "56", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "A counter incremented every time the browser enters simplified mode on print preview." + }, + "PRINT_PREVIEW_SIMPLIFY_PAGE_UNAVAILABLE_COUNT": { + "alert_emails": ["carnold@mozilla.org"], + "bug_numbers": [1287587], + "expires_in_version": "56", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "A counter incremented every time the simplified mode is unavailable on print preview." + }, + "PRINT_DIALOG_OPENED_COUNT": { + "alert_emails": ["carnold@mozilla.org"], + "bug_numbers": [1306624], + "expires_in_version": "56", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "A counter incremented every time the user opens print dialog." + }, + "PRINT_COUNT": { + "alert_emails": ["carnold@mozilla.org"], + "bug_numbers": [1287587], + "expires_in_version": "56", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "A counter incremented every time the user prints a document." + }, + "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "The time (in milliseconds) that it took to display a selected source to the user." + }, + "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "The time (in milliseconds) that it took to display a selected source to the user." + }, + "MEDIA_RUST_MP4PARSE_SUCCESS": { + "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"], + "expires_in_version": "55", + "kind": "boolean", + "bug_numbers": [1220885], + "description": "(Bug 1220885) Whether the rust mp4 demuxer successfully parsed a stream segment.", + "cpp_guard": "MOZ_RUST_MP4PARSE" + }, + "MEDIA_RUST_MP4PARSE_ERROR_CODE": { + "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 32, + "bug_numbers": [1238420], + "description": "The error code reported when an MP4 parse attempt has failed.0 = OK, 1 = bad argument, 2 = invalid data, 3 = unsupported, 4 = unexpected end of file, 5 = read error.", + "cpp_guard": "MOZ_RUST_MP4PARSE" + }, + "MEDIA_RUST_MP4PARSE_TRACK_MATCH_AUDIO": { + "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"], + "expires_in_version": "55", + "kind": "boolean", + "bug_numbers": [1231169], + "description": "Whether rust and stagefight mp4 parser audio track results match.", + "cpp_guard": "MOZ_RUST_MP4PARSE" + }, + "MEDIA_RUST_MP4PARSE_TRACK_MATCH_VIDEO": { + "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"], + "expires_in_version": "55", + "kind": "boolean", + "bug_numbers": [1231169], + "description": "Whether rust and stagefight mp4 parser video track results match.", + "cpp_guard": "MOZ_RUST_MP4PARSE" + }, + "MEDIA_WMF_DECODE_ERROR": { + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 256, + "description": "WMF media decoder error or success (0) codes." + }, + "MEDIA_OGG_LOADED_IS_CHAINED": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "description": "Whether Ogg audio/video encountered are chained or not.", + "bug_numbers": [1230295] + }, + "MEDIA_HLS_CANPLAY_REQUESTED": { + "alert_emails": ["ajones@mozilla.com", "giles@mozilla.com"], + "expires_in_version": "55", + "kind": "boolean", + "description": "Reports a true value when a page requests canPlayType for an HTTP Live Streaming media type (or generic m3u playlist).", + "bug_numbers": [1262659] + }, + "MEDIA_HLS_DECODER_SUCCESS": { + "alert_emails": ["ajones@mozilla.com", "giles@mozilla.com"], + "expires_in_version": "55", + "kind": "boolean", + "description": "Reports whether a decoder for an HTTP Live Streaming media type was created when requested.", + "bug_numbers": [1262659] + }, + "MEDIA_DECODING_PROCESS_CRASH": { + "alert_emails": ["bwu@mozilla.com", "jolin@mozilla.com", "jacheng@mozilla.com"], + "expires_in_version": "57", + "kind": "count", + "bug_numbers": [1297556, 1257777], + "description": "Records a value each time Fennec remote decoding process crashes unexpected while decoding media content.", + "releaseChannelCollection": "opt-out" + }, + "VIDEO_MFT_OUTPUT_NULL_SAMPLES": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 10, + "description": "Does the WMF video decoder return success but null output? 0 = playback successful, 1 = excessive null output but able to decode some frames, 2 = excessive null output and gave up, 3 = null output but recovered, 4 = non-excessive null output without being able to decode frames.", + "bug_numbers": [1176071] + }, + "AUDIO_MFT_OUTPUT_NULL_SAMPLES": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "53", + "kind": "count", + "description": "How many times the audio MFT decoder returns success but output nothing.", + "bug_numbers": [1176071] + }, + "VIDEO_CAN_CREATE_AAC_DECODER": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "58", + "kind": "boolean", + "description": "Whether at startup we report we can playback MP4 (AAC) audio. This is single value is recorded at every startup.", + "releaseChannelCollection": "opt-out" + }, + "VIDEO_CAN_CREATE_H264_DECODER": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "58", + "kind": "boolean", + "description": "Whether at startup we report we can playback MP4 (H.264) video. This is single value is recorded at every startup.", + "releaseChannelCollection": "opt-out" + }, + "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG": { + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 128, + "description": "The H.264 constraint set flag as extracted from the codecs parameter passed to HTMLMediaElement.canPlayType, with the addition of 0 for unknown values." + }, + "VIDEO_CANPLAYTYPE_H264_LEVEL": { + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 51, + "description": "The H.264 level (level_idc) as extracted from the codecs parameter passed to HTMLMediaElement.canPlayType, from levels 1 (10) to 5.2 (51), with the addition of 0 for unknown values." + }, + "VIDEO_CANPLAYTYPE_H264_PROFILE": { + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 244, + "description": "The H.264 profile number (profile_idc) as extracted from the codecs parameter passed to HTMLMediaElement.canPlayType." + }, + "DECODER_DOCTOR_INFOBAR_STATS": { + "alert_emails": ["gsquelart@mozilla.com"], + "bug_numbers": [1271483], + "expires_in_version": "53", + "kind": "enumerated", + "keyed": true, + "n_values": 8, + "description": "Counts of various Decoder Doctor notification events. Used to track efficacy of Decoder Doctor at helping users fix problems with their audio/video codecs. Keys are localized string names that identify problem with audio/video codecs that Decoder Doctor attempts to solve; see string values in dom.properties for verbose description of problems being solved. 0=recorded every time the Decoder Doctor notification is shown, 1=recorded the first time in a profile when notification is shown, 2=recorded when 'Learn how' button clicked, 3=recorded when 'Learn how' button first clicked in a profile, 4=recorded when issue solved after infobar has been shown at least once in a profile." + }, + "VIDEO_DECODED_H264_SPS_CONSTRAINT_SET_FLAG": { + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 128, + "description": "A bit pattern to collect H.264 constraint set flag from the decoded SPS. Bits 0 through 5 represent constraint_set0_flag through constraint_set5_flag, respectively." + }, + "VIDEO_DECODED_H264_SPS_LEVEL": { + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 51, + "description": "The H.264 level (level_idc) as extracted from the decoded SPS, from levels 1 (10) to 5.2 (51), with the addition of 0 for unknown values." + }, + "VIDEO_DECODED_H264_SPS_PROFILE": { + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 244, + "description": "The H.264 profile number (profile_idc) as extracted from the decoded SPS." + }, + "VIDEO_H264_SPS_MAX_NUM_REF_FRAMES": { + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 17, + "description": "SPS.max_num_ref_frames indicates how deep the H.264 queue is going to be, and as such the minimum memory usage by the decoder, from 0 to 16. 17 indicates an invalid value." + }, + "WEBRTC_ICE_FINAL_CONNECTION_STATE": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 7, + "description": "The ICE connection state when the PC was closed" + }, + "WEBRTC_ICE_ON_TIME_TRICKLE_ARRIVAL_TIME": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "description": "The length of time (in milliseconds) that a trickle candidate took to arrive after the start of ICE, given that it arrived when ICE was not in a failure state (ie; a candidate that we could do something with, hence 'on time')" + }, + "WEBRTC_ICE_LATE_TRICKLE_ARRIVAL_TIME": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "description": "The length of time (in milliseconds) that a trickle candidate took to arrive after the start of ICE, given that it arrived after ICE failed." + }, + "WEBRTC_ICE_SUCCESS_TIME": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "description": "The length of time (in milliseconds) it took for ICE to complete, given that ICE succeeded." + }, + "WEBRTC_ICE_FAILURE_TIME": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "description": "The length of time (in milliseconds) it took for ICE to complete, given that it failed." + }, + "WEBRTC_ICE_SUCCESS_RATE": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "description": "The number of failed ICE Connections (0) vs. number of successful ICE connections (1)." + }, + "WEBRTC_STUN_RATE_LIMIT_EXCEEDED_BY_TYPE_GIVEN_SUCCESS": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 4, + "description": "For each successful PeerConnection, bit 0 indicates the short-duration rate limit was reached, bit 1 indicates the long-duration rate limit was reached" + }, + "WEBRTC_STUN_RATE_LIMIT_EXCEEDED_BY_TYPE_GIVEN_FAILURE": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "enumerated", + "n_values": 4, + "description": "For each failed PeerConnection, bit 0 indicates the short-duration rate limit was reached, bit 1 indicates the long-duration rate limit was reached" + }, + "WEBRTC_AVSYNC_WHEN_AUDIO_LAGS_VIDEO_MS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "The delay (in milliseconds) when audio is behind video. Zero delay is counted. Measured every second of a call." + }, + "WEBRTC_AVSYNC_WHEN_VIDEO_LAGS_AUDIO_MS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 1000, + "description": "The delay (in milliseconds) when video is behind audio. Zero delay is not counted. Measured every second of a call." + }, + "WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 1000, + "description": "Locally measured data rate of inbound video (kbit/s). Computed every second of a call." + }, + "WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 1000, + "description": "Locally measured data rate on inbound audio (kbit/s). Computed every second of a call." + }, + "WEBRTC_VIDEO_QUALITY_OUTBOUND_BANDWIDTH_KBITS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 1000, + "description": "Data rate deduced from RTCP from remote recipient of outbound video (kbit/s). Computed every second of a call (for easy comparison)." + }, + "WEBRTC_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000000, + "n_buckets": 1000, + "description": "Data rate deduced from RTCP from remote recipient of outbound audio (kbit/s). Computed every second of a call (for easy comparison)." + }, + "WEBRTC_VIDEO_QUALITY_INBOUND_PACKETLOSS_RATE": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 100, + "description": "Locally measured packet loss on inbound video (permille). Sampled every second of a call." + }, + "WEBRTC_AUDIO_QUALITY_INBOUND_PACKETLOSS_RATE": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 100, + "description": "Locally measured packet loss on inbound audio (permille). Sampled every second of a call." + }, + "WEBRTC_VIDEO_QUALITY_OUTBOUND_PACKETLOSS_RATE": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 100, + "description": "RTCP-reported packet loss by remote recipient of outbound video (permille). Sampled every second of a call (for easy comparison)." + }, + "WEBRTC_AUDIO_QUALITY_OUTBOUND_PACKETLOSS_RATE": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 100, + "description": "RTCP-reported packet loss by remote recipient of outbound audio (permille). Sampled every second of a call (for easy comparison)." + }, + "WEBRTC_VIDEO_QUALITY_INBOUND_JITTER": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 100, + "description": "Locally measured jitter on inbound video (ms). Sampled every second of a call." + }, + "WEBRTC_AUDIO_QUALITY_INBOUND_JITTER": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "Locally measured jitter on inbound audio (ms). Sampled every second of a call." + }, + "WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "RTCP-reported jitter by remote recipient of outbound video (ms). Sampled every second of a call (for easy comparison)." + }, + "WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "RTCP-reported jitter by remote recipient of outbound audio (ms). Sampled every second of a call (for easy comparison)." + }, + "WEBRTC_VIDEO_ERROR_RECOVERY_MS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 500, + "description": "Time to recover from a video error in ms" + }, + "WEBRTC_VIDEO_RECOVERY_BEFORE_ERROR_PER_MIN": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 200, + "description": "Number of losses recovered before error per min" + }, + "WEBRTC_VIDEO_RECOVERY_AFTER_ERROR_PER_MIN": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 200, + "description": "Number of losses recovered after error per min" + }, + "WEBRTC_VIDEO_DECODE_ERROR_TIME_PERMILLE": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 1000, + "n_buckets": 100, + "description": "Percentage*10 (permille) of call decoding with errors or frozen due to errors" + }, + "WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "Roundtrip time of outbound video (ms). Sampled every second of a call." + }, + "WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "Roundtrip time of outbound audio (ms). Sampled every second of a call." + }, + "WEBRTC_VIDEO_ENCODER_BITRATE_AVG_PER_CALL_KBPS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 100, + "description": "Video encoder's average bitrate (in kbits/s) over an entire call" + }, + "WEBRTC_VIDEO_ENCODER_BITRATE_STD_DEV_PER_CALL_KBPS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 100, + "description": "Standard deviation from video encoder's average bitrate (in kbits/s) over an entire call" + }, + "WEBRTC_VIDEO_ENCODER_FRAMERATE_AVG_PER_CALL": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 200, + "n_buckets": 50, + "description": "Video encoder's average framerate (in fps) over an entire call" + }, + "WEBRTC_VIDEO_ENCODER_FRAMERATE_10X_STD_DEV_PER_CALL": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 200, + "n_buckets": 50, + "description": "Standard deviation from video encoder's average framerate (in 1/10 fps) over an entire call" + }, + "WEBRTC_VIDEO_ENCODER_DROPPED_FRAMES_PER_CALL_FPM": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 50000, + "n_buckets": 100, + "description": "Video encoder's number of frames dropped (in frames/min) over an entire call" + }, + "WEBRTC_VIDEO_DECODER_BITRATE_AVG_PER_CALL_KBPS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 100, + "description": "Video decoder's average bitrate (in kbits/s) over an entire call" + }, + "WEBRTC_VIDEO_DECODER_BITRATE_STD_DEV_PER_CALL_KBPS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 100, + "description": "Standard deviation from video decoder's average bitrate (in kbits/s) over an entire call" + }, + "WEBRTC_VIDEO_DECODER_FRAMERATE_AVG_PER_CALL": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 200, + "n_buckets": 50, + "description": "Video decoder's average framerate (in fps) over an entire call" + }, + "WEBRTC_VIDEO_DECODER_FRAMERATE_10X_STD_DEV_PER_CALL": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 200, + "n_buckets": 50, + "description": "Standard deviation from video decoder's average framerate (in 1/10 fps) over an entire call" + }, + "WEBRTC_VIDEO_DECODER_DISCARDED_PACKETS_PER_CALL_PPM": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 50000, + "n_buckets": 100, + "description": "Video decoder's number of discarded packets (in packets/min) over an entire call" + }, + "WEBRTC_CALL_DURATION": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 1000, + "description": "The length of time (in seconds) that a call lasted." + }, + "WEBRTC_CALL_COUNT": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "48", + "kind": "exponential", + "high": 500, + "n_buckets": 50, + "description": "The number of calls made during a session." + }, + "WEBRTC_CALL_COUNT_2": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "bug_numbers": [1261063], + "expires_in_version": "never", + "kind": "count", + "description": "The number of calls made during a session." + }, + "WEBRTC_ICE_ADD_CANDIDATE_ERRORS_GIVEN_SUCCESS": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "linear", + "high": 30, + "n_buckets": 29, + "description": "The number of times AddIceCandidate failed on a given PeerConnection, given that ICE succeeded." + }, + "WEBRTC_ICE_ADD_CANDIDATE_ERRORS_GIVEN_FAILURE": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "linear", + "high": 30, + "n_buckets": 29, + "description": "The number of times AddIceCandidate failed on a given PeerConnection, given that ICE failed." + }, + "WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN": { + "alert_emails": ["seceng@mozilla.org"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 15, + "description": "Origins for getUserMedia calls (0=other, 1=HTTPS, 2=file, 3=app, 4=localhost, 5=loop, 6=privileged)", + "releaseChannelCollection": "opt-out" + }, + "WEBRTC_GET_USER_MEDIA_TYPE": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Type for media in getUserMedia calls (0=Camera, 1=Screen, 2=Application, 3=Window, 4=Browser, 5=Microphone, 6=AudioCapture, 7=Other)" + }, + "WEBRTC_LOAD_STATE_RELAXED": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 25, + "description": "Percentage of time spent in the Relaxed load state in calls over 30 seconds." + }, + "WEBRTC_LOAD_STATE_RELAXED_SHORT": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 25, + "description": "Percentage of time spent in the Relaxed load state in calls 5-30 seconds." + }, + "WEBRTC_LOAD_STATE_NORMAL": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 25, + "description": "Percentage of time spent in the Normal load state in calls over 30 seconds." + }, + "WEBRTC_LOAD_STATE_NORMAL_SHORT": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 25, + "description": "Percentage of time spent in the Normal load state in calls over 5-30 seconds." + }, + "WEBRTC_LOAD_STATE_STRESSED": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 25, + "description": "Percentage of time spent in the Stressed load state in calls over 30 seconds." + }, + "WEBRTC_LOAD_STATE_STRESSED_SHORT": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 100, + "n_buckets": 25, + "description": "Percentage of time spent in the Stressed load state in calls 5-30 seconds." + }, + "WEBRTC_RENEGOTIATIONS": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 21, + "n_buckets": 20, + "description": "Number of Renegotiations during each call" + }, + "WEBRTC_MAX_VIDEO_SEND_TRACK": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 10, + "n_buckets": 9, + "description": "Number of Video tracks sent simultaneously" + }, + "WEBRTC_MAX_VIDEO_RECEIVE_TRACK": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 20, + "n_buckets": 19, + "description": "Number of Video tracks received simultaneously" + }, + "WEBRTC_MAX_AUDIO_SEND_TRACK": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 20, + "n_buckets": 19, + "description": "Number of Audio tracks sent simultaneously" + }, + "WEBRTC_MAX_AUDIO_RECEIVE_TRACK": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "linear", + "high": 30, + "n_buckets": 29, + "description": "Number of Audio tracks received simultaneously" + }, + "WEBRTC_DATACHANNEL_NEGOTIATED": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Was DataChannels negotiated" + }, + "WEBRTC_CALL_TYPE": { + "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Type of call: (Bitmask) Audio = 1, Video = 2, DataChannels = 4" + }, + "DEVTOOLS_TOOLBOX_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools toolbox has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_OPTIONS_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools options panel has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_WEBCONSOLE_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Web Console has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_BROWSERCONSOLE_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Browser Console has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_INSPECTOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Inspector has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_RULEVIEW_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Rule View has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Computed View has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Font Inspector has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Animation Inspector has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_JSDEBUGGER_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Debugger has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Browser Debugger has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_STYLEEDITOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Style Editor has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_SHADEREDITOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Shader Editor has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_WEBAUDIOEDITOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Web Audio Editor has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_CANVASDEBUGGER_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Canvas Debugger has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_JSPROFILER_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools JS Profiler has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_MEMORY_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Memory Tool has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_NETMONITOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Network Monitor has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_STORAGE_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Storage Inspector has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_PAINTFLASHING_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Paint Flashing has been opened via the toolbox button.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_TILT_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Tilt has been opened via the toolbox button.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_SCRATCHPAD_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Scratchpad toolbox panel has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_SCRATCHPAD_WINDOW_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1214352, 1247985], + "description": "Number of times the DevTools Scratchpad standalone window has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_RESPONSIVE_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Responsive Design Mode tool has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_EYEDROPPER_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Eyedropper tool has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Eyedropper has been opened via the DevTools menu.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Eyedropper has been opened via the color picker.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools Developer Toolbar / GCLI has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org", "jan@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985, 1204601], + "description": "Number of times about:debugging has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_WEBIDE_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools WebIDE has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times the DevTools WebIDE project editor has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times a file has been saved in the DevTools WebIDE project editor.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_WEBIDE_NEW_PROJECT_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times a new project has been created in the DevTools WebIDE.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times a project has been imported into the DevTools WebIDE.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_CUSTOM_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1247985], + "description": "Number of times a custom developer tool has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_RELOAD_ADDON_INSTALLED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "55", + "kind": "count", + "description": "Number of times the reload addon has been installed.", + "bug_numbers": [1248435] + }, + "DEVTOOLS_RELOAD_ADDON_RELOAD_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "55", + "kind": "count", + "description": "Number of times the tools have been reloaded by the reload addon.", + "bug_numbers": [1248435] + }, + "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the toolbox been active (seconds)" + }, + "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the options panel been active (seconds)" + }, + "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the web console been active (seconds)" + }, + "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the browser console been active (seconds)" + }, + "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the inspector been active (seconds)" + }, + "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the rule view been active (seconds)" + }, + "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the computed view been active (seconds)" + }, + "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the font inspector been active (seconds)" + }, + "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the animation inspector been active (seconds)" + }, + "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the JS debugger been active (seconds)" + }, + "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the JS browser debugger been active (seconds)" + }, + "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the style editor been active (seconds)" + }, + "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the Shader Editor been active (seconds)" + }, + "DEVTOOLS_WEBAUDIOEDITOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the Web Audio Editor been active (seconds)" + }, + "DEVTOOLS_CANVASDEBUGGER_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the Canvas Debugger been active (seconds)" + }, + "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the JS profiler been active (seconds)" + }, + "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the Memory Tool been active (seconds)" + }, + "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the network monitor been active (seconds)" + }, + "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the storage inspector been active (seconds)" + }, + "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has paint flashing been active (seconds)" + }, + "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has Tilt been active (seconds)" + }, + "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has Scratchpad been active (seconds)" + }, + "DEVTOOLS_SCRATCHPAD_WINDOW_TIME_ACTIVE_SECONDS": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "50", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has Scratchpad standalone window been active (seconds)", + "bug_numbers": [1214352] + }, + "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS": { + "expires_in_version": "55", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "bug_numbers": [1242057], + "description": "How long has the responsive view been active (seconds)", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has the developer toolbar been active (seconds)" + }, + "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org", "jan@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has about:debugging been active? (seconds) (bug 1204601)" + }, + "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has WebIDE been active (seconds)" + }, + "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has WebIDE's project editor been active (seconds)" + }, + "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long has a custom developer tool been active (seconds)" + }, + "DEVTOOLS_WEBIDE_CONNECTION_RESULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Did WebIDE runtime connection succeed?" + }, + "DEVTOOLS_WEBIDE_USB_CONNECTION_RESULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Did WebIDE USB runtime connection succeed?" + }, + "DEVTOOLS_WEBIDE_WIFI_CONNECTION_RESULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Did WebIDE WiFi runtime connection succeed?" + }, + "DEVTOOLS_WEBIDE_SIMULATOR_CONNECTION_RESULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Did WebIDE simulator runtime connection succeed?" + }, + "DEVTOOLS_WEBIDE_REMOTE_CONNECTION_RESULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Did WebIDE remote runtime connection succeed?" + }, + "DEVTOOLS_WEBIDE_LOCAL_CONNECTION_RESULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Did WebIDE local runtime connection succeed?" + }, + "DEVTOOLS_WEBIDE_OTHER_CONNECTION_RESULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Did WebIDE other runtime connection succeed?" + }, + "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 100, + "description": "How long was WebIDE connected to a runtime (seconds)?" + }, + "DEVTOOLS_WEBIDE_CONNECTION_PLAY_USED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Was WebIDE's play button used during this runtime connection?" + }, + "DEVTOOLS_WEBIDE_CONNECTION_DEBUG_USED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Was WebIDE's debug button used during this runtime connection?" + }, + "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "What runtime type did WebIDE connect to?" + }, + "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "What runtime ID did WebIDE connect to?" + }, + "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "What runtime processor did WebIDE connect to?" + }, + "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "What runtime OS did WebIDE connect to?" + }, + "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "What runtime platform version did WebIDE connect to?" + }, + "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "What runtime app type did WebIDE connect to?" + }, + "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "What runtime version did WebIDE connect to?" + }, + "DEVTOOLS_OS_ENUMERATED_PER_USER": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 13, + "description": "OS of DevTools user (0:Windows XP, 1:Windows Vista, 2:Windows 7, 3:Windows 8, 4:Windows 8.1, 5:OSX, 6:Linux 7:Windows 10, 8:reserved, 9:reserved, 10:reserved, 11:reserved, 12:other)" + }, + "DEVTOOLS_OS_IS_64_BITS_PER_USER": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "OS bit size of DevTools user (0:32bit, 1:64bit, 2:128bit)" + }, + "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 13, + "description": "Screen resolution of DevTools user (0:lower, 1:800x600, 2:1024x768, 3:1280x800, 4:1280x1024, 5:1366x768, 6:1440x900, 7:1920x1080, 8:2560×1440, 9:2560×1600, 10:2880x1800, 11:other, 12:higher)" + }, + "DEVTOOLS_TABS_OPEN_PEAK_LINEAR": { + "expires_in_version": "never", + "kind": "linear", + "high": 101, + "n_buckets": 100, + "description": "The peak number of open tabs in all windows for a session for devtools users." + }, + "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR": { + "expires_in_version": "never", + "kind": "linear", + "high": 101, + "n_buckets": 100, + "description": "The mean number of open tabs in all windows for a session for devtools users." + }, + "DEVTOOLS_TABS_PINNED_PEAK_LINEAR": { + "expires_in_version": "never", + "kind": "linear", + "high": 101, + "n_buckets": 100, + "description": "The peak number of pinned tabs (app tabs) in all windows for a session for devtools users." + }, + "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR": { + "expires_in_version": "never", + "kind": "linear", + "high": 101, + "n_buckets": 100, + "description": "The mean number of pinned tabs (app tabs) in all windows for a session for devtools users." + }, + "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 1000, + "description": "The time (in milliseconds) that it took to save a heap snapshot in mozilla::devtools::ChromeUtils::SaveHeapSnapshot." + }, + "DEVTOOLS_READ_HEAP_SNAPSHOT_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 100000, + "n_buckets": 1000, + "description": "The time (in milliseconds) that it took to read a heap snapshot in mozilla::devtools::ChromeUtils::ReadHeapSnapshot." + }, + "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT": { + "expires_in_version": "never", + "kind": "linear", + "high": 10000000, + "n_buckets": 10000, + "description": "The number of nodes serialized into a heap snapshot." + }, + "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT": { + "expires_in_version": "never", + "kind": "linear", + "high": 10000000, + "n_buckets": 10000, + "description": "The number of edges serialized into a heap snapshot." + }, + "DEVTOOLS_PERFTOOLS_RECORDING_COUNT": { + "expires_in_version": "never", + "kind": "count", + "description": "Incremented whenever a performance tool recording is completed." + }, + "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT": { + "expires_in_version": "never", + "kind": "count", + "description": "Incremented whenever a performance tool recording is completed that was initiated via console.profile." + }, + "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG": { + "expires_in_version": "never", + "kind": "flag", + "description": "When a user imports a recording in the performance tool." + }, + "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG": { + "expires_in_version": "never", + "kind": "flag", + "description": "When a user imports a recording in the performance tool." + }, + "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "description": "When a user starts a recording with specific recording options, keyed by feature name (withMarkers, withAllocations, etc.)." + }, + "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 600000, + "n_buckets": 20, + "description": "The length of a duration in MS of a performance tool recording." + }, + "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS": { + "expires_in_version": "never", + "kind": "exponential", + "keyed": true, + "high": 600000, + "n_buckets": 20, + "description": "The amount of time spent in a specific performance tool view, keyed by view name (waterfall, js-calltree, js-flamegraph, etc)." + }, + "DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED": { + "alert_emails": ["mphillips@mozilla.com"], + "bug_numbers": [1255133], + "expires_in_version": "55", + "kind": "boolean", + "keyed": true, + "description": "Measures whether a particular JavaScript error has been displayed in the webconsole." + }, + "DEVTOOLS_TOOLBOX_HOST": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "58", + "kind": "enumerated", + "bug_numbers": [1205845], + "n_values": 9, + "releaseChannelCollection": "opt-out", + "description": "Records DevTools toolbox host each time the toolbox is opened and when the host is changed (0:Bottom, 1:Side, 2:Window, 3:Custom, 9:Unknown)." + }, + "VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN": { + "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "description": "How many times has view source in browser / tab been opened?" + }, + "VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN": { + "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "description": "How many times has view source in a new window been opened?" + }, + "VIEW_SOURCE_EXTERNAL_RESULT_BOOLEAN": { + "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "description": "How many times has view source in an external editor been opened, and did it succeed?" + }, + "BROWSER_IS_USER_DEFAULT": { + "expires_in_version": "never", + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "The result of the startup default desktop browser check." + }, + "BROWSER_IS_USER_DEFAULT_ERROR": { + "expires_in_version": "never", + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "True if the browser was unable to determine if the browser was set as default." + }, + "BROWSER_SET_DEFAULT_DIALOG_PROMPT_RAWCOUNT": { + "expires_in_version": "never", + "kind": "exponential", + "high": 250, + "n_buckets": 15, + "releaseChannelCollection": "opt-out", + "description": "The number of times that a profile has seen the 'Set Default Browser' dialog." + }, + "BROWSER_SET_DEFAULT_ALWAYS_CHECK": { + "expires_in_version": "never", + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "True if the profile has `browser.shell.checkDefaultBrowser` set to true." + }, + "BROWSER_SET_DEFAULT_RESULT": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "releaseChannelCollection": "opt-out", + "description": "Result of the Set Default Browser dialog (0=Use Firefox + 'Always perform check' unchecked, 1=Use Firefox + 'Always perform check' checked, 2=Not Now + 'Always perform check' unchecked, 3=Not Now + 'Always perform check' checked)" + }, + "BROWSER_SET_DEFAULT_ERROR": { + "expires_in_version": "never", + "kind": "boolean", + "releaseChannelCollection": "opt-out", + "description": "True if the browser was unable to set Firefox as the default browser" + }, + "BROWSER_SET_DEFAULT_TIME_TO_COMPLETION_SECONDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 500, + "n_buckets": 15, + "releaseChannelCollection": "opt-out", + "description": "Time to successfully set Firefox as the default browser after clicking 'Set Firefox as Default'. Should be near-instant in some environments, others require user interaction. Measured in seconds." + }, + "BROWSER_IS_ASSIST_DEFAULT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "The result of the default browser check for assist intent." + }, + "MIXED_CONTENT_PAGE_LOAD": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "Accumulates type of content per page load (0=no mixed or non-secure page, 1=mixed passive, 2=mixed active, 3=mixed passive and mixed active)" + }, + "MIXED_CONTENT_UNBLOCK_COUNTER": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "A simple counter of daily mixed-content unblock operations and top documents loaded" + }, + "MIXED_CONTENT_HSTS": { + "alert_emails": ["seceng@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS" + }, + "MIXED_CONTENT_HSTS_PRIMING": { + "alert_emails": ["seceng@mozilla.org"], + "bug_numbers": [1246540], + "expires_in_version": "60", + "kind": "enumerated", + "n_values": 16, + "description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed, including how often would we send an HSTS priming request? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS, 4=display/no-HSTS-priming, 5=display/do-HSTS-priming, 6=active/no-HSTS-priming, 7=active/do-HSTS-priming" + }, + "MIXED_CONTENT_HSTS_PRIMING_RESULT": { + "alert_emails": ["seceng@mozilla.org"], + "bug_numbers": [1246540], + "expires_in_version": "60", + "kind": "enumerated", + "n_values": 16, + "description": "How often do we get back an HSTS priming result which upgrades the connection to HTTPS? 0=cached (no upgrade), 1=cached (do upgrade), 2=cached (blocked), 3=already upgraded, 4=priming succeeded, 5=priming succeeded (block due to pref), 6=priming succeeded (no upgrade due to pref), 7=priming failed (block), 8=priming failed (accept)" + }, + "HSTS_PRIMING_REQUEST_DURATION": { + "alert_emails": ["seceng-telemetry@mozilla.org"], + "bug_numbers": [1311893], + "expires_in_version": "58", + "kind": "exponential", + "low": 100, + "high": 30000, + "n_buckets": 100, + "keyed": true, + "description": "The amount of time required for HSTS priming requests (ms), keyed by success or failure of the priming request. (success, failure)" + }, + "MIXED_CONTENT_OBJECT_SUBREQUEST": { + "alert_emails": ["seceng@mozilla.org"], + "bug_numbers": [1244116], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "How often objects load insecure content on secure pages (counting pages, not objects). 0=pages with no mixed object subrequests, 1=pages with mixed object subrequests" + }, + "COOKIE_SCHEME_SECURITY": { + "alert_emails": ["seceng@mozilla.org"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "releaseChannelCollection": "opt-out", + "description": "How often are secure cookies set from non-secure origins, and vice-versa? 0=nonsecure/http, 1=nonsecure/https, 2=secure/http, 3=secure/https" + }, + "COOKIE_LEAVE_SECURE_ALONE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "bug_numbers": [976073], + "expires_in_version": "57", + "kind": "enumerated", + "n_values": 10, + "releaseChannelCollection": "opt-out", + "description": "Measuring the effects of draft-ietf-httpbis-cookie-alone blocking. 0=blocked http setting secure cookie; 1=blocked http downgrading secure cookie; 2=blocked evicting secure cookie; 3=evicting newer insecure cookie; 4=evicting the oldest insecure cookie; 5=evicting the preferred cookie; 6=evicting the secure blocked" + }, + "NTLM_MODULE_USED_2": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "The module used for the NTLM protocol (Windows_API, Kerberos, Samba_auth or Generic) and whether or not the authentication was used to connect to a proxy server. This data is collected only once per session (at first NTLM authentification) ; fixed version." + }, + "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE": { + "expires_in_version": "default", + "kind": "exponential", + "high": 100, + "n_buckets": 15, + "description": "BACKGROUND THUMBNAILS: Size of capture queue when a capture request is received" + }, + "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 300000, + "n_buckets": 20, + "description": "BACKGROUND THUMBNAILS: Time the capture request spent in the queue before being serviced (ms)" + }, + "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "BACKGROUND THUMBNAILS: Time the capture took once it started and successfully completed (ms)" + }, + "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 10, + "description": "BACKGROUND THUMBNAILS: Reason the capture completed (see TEL_CAPTURE_DONE_* constants in BackgroundPageThumbs.jsm)" + }, + "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 60000, + "n_buckets": 20, + "description": "BACKGROUND THUMBNAILS: Time the capture's page load took (ms)" + }, + "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 500, + "n_buckets": 15, + "description": "BACKGROUND THUMBNAILS: Time it took to draw the capture's window to canvas (ms)" + }, + "NETWORK_CACHE_V2_MISS_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to find out a cache entry file is missing" + }, + "NETWORK_CACHE_V2_HIT_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to open an existing file" + }, + "NETWORK_CACHE_V1_TRUNCATE_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to reopen an entry with OPEN_TRUNCATE" + }, + "NETWORK_CACHE_V1_MISS_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to find out a cache entry is missing" + }, + "NETWORK_CACHE_V1_HIT_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to open an existing cache entry" + }, + "NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 7, + "description": "Final status of the CacheFileOutputStream (0=ok, 1=other error, 2=out of memory, 3=disk full, 4=file corrupted, 5=file not found, 6=binding aborted)" + }, + "NETWORK_CACHE_V2_INPUT_STREAM_STATUS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 7, + "description": "Final status of the CacheFileInputStream (0=ok, 1=other error, 2=out of memory, 3=disk full, 4=file corrupted, 5=file not found, 6=binding aborted)" + }, + "NETWORK_CACHE_FS_TYPE": { + "expires_in_version": "42", + "kind": "enumerated", + "n_values": 5, + "description": "Type of FS that the cache is stored on (0=NTFS (Win), 1=FAT32 (Win), 2=FAT (Win), 3=other FS (Win), 4=other OS)" + }, + "NETWORK_CACHE_SIZE_FULL_FAT": { + "expires_in_version": "42", + "kind": "linear", + "high": 500, + "n_buckets": 50, + "description": "Size (in MB) of a cache that reached a file count limit" + }, + "NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 40, + "description": "Hit/Miss count split by cache size in file count (0=Hit 0-5000, 1=Miss 0-5000, 2=Hit 5001-10000, ...)" + }, + "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 400, + "description": "Hit rate for a specific cache size in file count. The hit rate is split into 20 buckets, the lower limit of the range in percents is 5*n/20. The cache size is divided into 20 ranges of length 5000, the lower limit of the range is 5000*(n%20)" + }, + "NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to read the first part of the metadata from the cache entry file." + }, + "NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "description": "Time spent to read the missing part of the metadata from the cache entry file." + }, + "NETWORK_CACHE_METADATA_FIRST_READ_SIZE": { + "expires_in_version": "never", + "kind": "linear", + "high": 5119, + "n_buckets": 256, + "description": "Guessed size of the metadata that we read from the cache file as the first part." + }, + "NETWORK_CACHE_METADATA_SIZE": { + "expires_in_version": "never", + "kind": "linear", + "high": 5119, + "n_buckets": 256, + "description": "Actual size of the metadata parsed from the disk." + }, + "NETWORK_CACHE_HASH_STATS": { + "expires_in_version": "46", + "kind": "enumerated", + "n_values": 160, + "description": "The longest hash match between a newly added entry and all the existing entries." + }, + "DATABASE_LOCKED_EXCEPTION": { + "expires_in_version": "42", + "kind": "enumerated", + "description": "Record database locks when opening one of Fennec's databases. The index corresponds to how many attempts, beginning with 0.", + "n_values": 5 + }, + "DATABASE_SUCCESSFUL_UNLOCK": { + "expires_in_version": "42", + "kind": "enumerated", + "description": "Record on which attempt we successfully unlocked a database. See DATABASE_LOCKED_EXCEPTION.", + "n_values": 5 + }, + "SSL_TLS13_INTOLERANCE_REASON_PRE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "bug_numbers": [1250568], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.3 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_TLS13_INTOLERANCE_REASON_POST": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "bug_numbers": [1250568], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.3 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_TLS12_INTOLERANCE_REASON_PRE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.2 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_TLS12_INTOLERANCE_REASON_POST": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.2 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_TLS11_INTOLERANCE_REASON_PRE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.1 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_TLS11_INTOLERANCE_REASON_POST": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.1 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_TLS10_INTOLERANCE_REASON_PRE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.0 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_TLS10_INTOLERANCE_REASON_POST": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Potential TLS 1.0 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_VERSION_FALLBACK_INAPPROPRIATE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "TLS/SSL version intolerance was falsely detected, server rejected handshake (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)." + }, + "SSL_WEAK_CIPHERS_FALLBACK": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 64, + "description": "Fallback attempted when server did not support any strong cipher suites" + }, + "SSL_CIPHER_SUITE_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 128, + "description": "Negotiated cipher suite in full handshake (see key in HandshakeCallback in nsNSSCallbacks.cpp)" + }, + "SSL_CIPHER_SUITE_RESUMED": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 128, + "description": "Negotiated cipher suite in resumed handshake (see key in HandshakeCallback in nsNSSCallbacks.cpp)" + }, + "SSL_KEA_RSA_KEY_SIZE_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 24, + "description": "RSA KEA (TLS_RSA_*) key size in full handshake" + }, + "SSL_KEA_DHE_KEY_SIZE_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 24, + "description": "DHE KEA (TLS_DHE_*) key size in full handshake" + }, + "SSL_KEA_ECDHE_CURVE_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 36, + "description": "ECDHE KEA (TLS_ECDHE_*) curve (23=P-256, 24=P-384, 25=P-521) in full handshake" + }, + "SSL_AUTH_ALGORITHM_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 16, + "description": "SSL Authentication Algorithm (null=0, rsa=1, dsa=2, ecdsa=4) in full handshake" + }, + "SSL_AUTH_RSA_KEY_SIZE_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 24, + "description": "RSA signature key size for TLS_*_RSA_* in full handshake" + }, + "SSL_AUTH_ECDSA_CURVE_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 36, + "description": "ECDSA signature curve for TLS_*_ECDSA_* in full handshake (23=P-256, 24=P-384, 25=P-521)" + }, + "SSL_SYMMETRIC_CIPHER_FULL": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 32, + "description": "Symmetric cipher used in full handshake (null=0, rc4=1, 3des=4, aes-cbc=7, camellia=8, seed=9, aes-gcm=10)" + }, + "SSL_SYMMETRIC_CIPHER_RESUMED": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 32, + "description": "Symmetric cipher used in resumed handshake (null=0, rc4=1, 3des=4, aes-cbc=7, camellia=8, seed=9, aes-gcm=10)" + }, + "SSL_REASONS_FOR_NOT_FALSE_STARTING": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 512, + "description": "Bitmask of reasons we did not false start when libssl would have let us (see key in nsNSSCallbacks.cpp)" + }, + "SSL_HANDSHAKE_TYPE": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Type of handshake (1=resumption, 2=false started, 3=chose not to false start, 4=not allowed to false start)" + }, + "SSL_OCSP_STAPLING": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Status of OCSP stapling on this handshake (1=present, good; 2=none; 3=present, expired; 4=present, other error)" + }, + "SSL_OCSP_MAY_FETCH": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 8, + "description": "For non-stapling cases, is OCSP fetching a possibility? (0=yes, 1=no because missing/invalid OCSP URI, 2=no because fetching disabled, 3=no because both)" + }, + "SSL_CERT_ERROR_OVERRIDES": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 24, + "description": "Was a certificate error overridden on this handshake? What was it? (0=unknown error (indicating bug), 1=no, >1=a specific error)" + }, + "SSL_CERT_VERIFICATION_ERRORS": { + "alert_emails": ["seceng@mozilla.org"], + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 100, + "description": "If certificate verification failed in a TLS handshake, what was the error? (see MapCertErrorToProbeValue in security/manager/ssl/SSLServerCertVerification.cpp and the values in security/pkix/include/pkix/Result.h)" + }, + "SSL_PERMANENT_CERT_ERROR_OVERRIDES": { + "alert_emails": ["seceng@mozilla.org"], + "expires_in_version": "default", + "kind": "exponential", + "high": 1024, + "n_buckets": 10, + "description": "How many permanent certificate overrides a user has stored." + }, + "SSL_SCTS_ORIGIN": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "bug_numbers": [1293231], + "releaseChannelCollection": "opt-out", + "description": "Origin of Signed Certificate Timestamps received (1=Embedded, 2=TLS handshake extension, 3=Stapled OCSP response)" + }, + "SSL_SCTS_PER_CONNECTION": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "bug_numbers": [1293231], + "releaseChannelCollection": "opt-out", + "description": "Histogram of Signed Certificate Timestamps per SSL connection, from all sources (embedded / OCSP Stapling / TLS handshake). Bucket 0 counts the cases when no SCTs were received, or none were extracted due to parsing errors." + }, + "SSL_SCTS_VERIFICATION_STATUS": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "bug_numbers": [1293231], + "releaseChannelCollection": "opt-out", + "description": "Verification status of Signed Certificate Timestamps received (0=Decoding error, 1=SCT verified, 2=SCT from unknown log, 3=Invalid SCT signature, 4=SCT timestamp is in the future)" + }, + "SSL_SERVER_AUTH_EKU": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "Presence of of the Server Authenticaton EKU in accepted SSL server certificates (0=No EKU, 1=EKU present and has id_kp_serverAuth, 2=EKU present and has id_kp_serverAuth as well as some other EKU, 3=EKU present but does not contain id_kp_serverAuth)" + }, + "TELEMETRY_TEST_EXPIRED": { + "expires_in_version": "4.0a1", + "kind": "flag", + "description": "a testing histogram; not meant to be touched" + }, + "TLS_ERROR_REPORT_UI": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 15, + "description": "User interaction with the TLS Error Reporter in about:neterror (0=Error seen, 1='auto' checked, 2='auto' unchecked, 3=Sent manually, 4=Sent automatically, 5=Send success, 6=Send failure, 7=Report section expanded)" + }, + "CERT_OCSP_ENABLED": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Is OCSP fetching enabled? (pref security.OCSP.enabled)" + }, + "CERT_OCSP_REQUIRED": { + "alert_emails": ["seceng-telemetry@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Is OCSP required when the cert has an OCSP URI? (pref security.OCSP.require)" + }, + "OSFILE_WORKER_LAUNCH_MS": { + "expires_in_version": "default", + "kind": "exponential", + "description": "The duration between the instant the first message is sent to OS.File and the moment the OS.File worker starts executing JavaScript, in milliseconds", + "high": 5000, + "n_buckets": 10 + }, + "OSFILE_WORKER_READY_MS": { + "expires_in_version": "default", + "kind": "exponential", + "description": "The duration between the instant the first message is sent to OS.File and the moment the OS.File worker has finished executing its startup JavaScript and is ready to receive requests, in milliseconds", + "high": 5000, + "n_buckets": 10 + }, + "OSFILE_WRITEATOMIC_JANK_MS": { + "expires_in_version": "default", + "kind": "exponential", + "description": "The duration during which the main thread is blocked during a call to OS.File.writeAtomic, in milliseconds", + "high": 5000, + "n_buckets": 10 + }, + "CERT_EV_STATUS": { + "expires_in_version": "never", + "alert_emails": ["seceng@mozilla.org"], + "bug_numbers": [1254653], + "kind": "enumerated", + "n_values": 10, + "description": "EV status of a certificate, recorded on each TLS connection. 0=invalid, 1=DV, 2=EV" + }, + "CERT_VALIDATION_SUCCESS_BY_CA": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 256, + "description": "Successful SSL server cert validations by CA (see RootHashes.inc for names of CAs)" + }, + "CERT_PINNING_FAILURES_BY_CA": { + "alert_emails": ["pinning@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 256, + "description": "Pinning failures by CA (see RootHashes.inc for names of CAs)" + }, + "CERT_PINNING_RESULTS": { + "alert_emails": ["pinning@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Certificate pinning results (0 = failure, 1 = success)" + }, + "CERT_PINNING_TEST_RESULTS": { + "alert_emails": ["pinning@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Certificate pinning test results (0 = failure, 1 = success)" + }, + "CERT_PINNING_MOZ_RESULTS": { + "alert_emails": ["pinning@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Certificate pinning results for Mozilla sites (0 = failure, 1 = success)" + }, + "CERT_PINNING_MOZ_TEST_RESULTS": { + "alert_emails": ["pinning@mozilla.org"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Certificate pinning test results for Mozilla sites (0 = failure, 1 = success)" + }, + "CERT_PINNING_MOZ_RESULTS_BY_HOST": { + "alert_emails": ["pinning@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 512, + "description": "Certificate pinning results by host for Mozilla operational sites" + }, + "CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST": { + "alert_emails": ["pinning@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 512, + "description": "Certificate pinning test results by host for Mozilla operational sites" + }, + "CERT_CHAIN_KEY_SIZE_STATUS": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 4, + "description": "Does enforcing a larger minimum RSA key size cause verification failures? 1 = no, 2 = yes, 3 = another error prevented finding a verified chain" + }, + "CERT_CHAIN_SHA1_POLICY_STATUS": { + "expires_in_version": "default", + "kind": "enumerated", + "n_values": 6, + "description": "1 = No SHA1 signatures, 2 = SHA1 certificates issued by an imported root, 3 = SHA1 certificates issued before 2016, 4 = SHA1 certificates issued after 2015, 5 = another error prevented successful verification" + }, + "WEAVE_CONFIGURED": { + "expires_in_version": "default", + "kind": "boolean", + "description": "If any version of Firefox Sync is configured for this device", + "releaseChannelCollection": "opt-out" + }, + "WEAVE_CONFIGURED_MASTER_PASSWORD": { + "expires_in_version": "never", + "kind": "boolean", + "description": "If both Firefox Sync and Master Password are configured for this device" + }, + "WEAVE_START_COUNT": { + "expires_in_version": "default", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "description": "The number of times a sync started in this session" + }, + "WEAVE_COMPLETE_SUCCESS_COUNT": { + "expires_in_version": "default", + "kind": "exponential", + "high": 1000, + "n_buckets": 10, + "description": "The number of times a sync successfully completed in this session" + }, + "WEAVE_WIPE_SERVER_SUCCEEDED": { + "expires_in_version": "55", + "alert_emails": ["fx-team@mozilla.com"], + "kind": "boolean", + "bug_numbers": [1241699], + "description": "Stores 1 if a wipeServer call succeeded, and 0 if it failed." + }, + "WEBCRYPTO_EXTRACTABLE_IMPORT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether an imported key was marked as extractable" + }, + "WEBCRYPTO_EXTRACTABLE_GENERATE": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a generated key was marked as extractable" + }, + "WEBCRYPTO_EXTRACTABLE_ENC": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a key used in an encrypt/decrypt operation was marked as extractable" + }, + "WEBCRYPTO_EXTRACTABLE_SIG": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a key used in a sign/verify operation was marked as extractable" + }, + "WEBCRYPTO_RESOLVED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a promise created by WebCrypto was resolved (vs rejected)" + }, + "WEBCRYPTO_METHOD": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "description": "Methods invoked under window.crypto.subtle (0=encrypt, 1=decrypt, 2=sign, 3=verify, 4=digest, 5=generateKey, 6=deriveKey, 7=deriveBits, 8=importKey, 9=exportKey, 10=wrapKey, 11=unwrapKey)" + }, + "WEBCRYPTO_ALG": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 30, + "description": "Algorithms used with WebCrypto (see table in WebCryptoTask.cpp)" + }, + "MASTER_PASSWORD_ENABLED": { + "expires_in_version": "never", + "kind": "flag", + "description": "If a master-password is enabled for this profile" + }, + "DISPLAY_SCALING_OSX" : { + "expires_in_version": "never", + "kind": "linear", + "high": 500, + "n_buckets": 100, + "description": "Scaling percentage for the display where the first window is opened (OS X only)", + "cpp_guard": "XP_MACOSX" + }, + "DISPLAY_SCALING_MSWIN" : { + "expires_in_version": "never", + "kind": "linear", + "high": 500, + "n_buckets": 100, + "description": "Scaling percentage for the display where the first window is opened (MS Windows only)", + "cpp_guard": "XP_WIN" + }, + "DISPLAY_SCALING_LINUX" : { + "expires_in_version": "never", + "kind": "linear", + "high": 500, + "n_buckets": 100, + "description": "Scaling percentage for the display where the first window is opened (Linux only)", + "cpp_guard": "XP_LINUX" + }, + "SOCIAL_SIDEBAR_STATE": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Social Sidebar state 0: closed, 1: opened. Toggling between providers will result in a higher opened rate." + }, + "SOCIAL_TOOLBAR_BUTTONS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "Social toolbar button has been used (0:share, 1:status, 2:bookmark)" + }, + "SOCIAL_PANEL_CLICKS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "Social content has been interacted with (0:share, 1:status, 2:bookmark, 3: sidebar)" + }, + "SOCIAL_SIDEBAR_OPEN_DURATION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 10, + "description": "Sidebar showing: seconds that the sidebar has been opened" + }, + "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 65, + "n_buckets": 10, + "description": "Duration of shutdown phase quit-application, as measured by the shutdown terminator, in seconds of activity" + }, + "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 65, + "n_buckets": 10, + "description": "Duration of shutdown phase profile-change-teardown, as measured by the shutdown terminator, in seconds of activity" + }, + "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN": { + "expires_in_version": "never", + "kind": "exponential", + "high": 65, + "n_buckets": 10, + "description": "Duration of shutdown phase xpcom-will-shutdown, as measured by the shutdown terminator, in seconds of activity" + }, + "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE": { + "expires_in_version": "never", + "kind": "exponential", + "high": 65, + "n_buckets": 10, + "description": "Duration of shutdown phase profile-before-change, as measured by the shutdown terminator, in seconds of activity" + }, + "BR_9_2_1_SUBJECT_ALT_NAMES": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Baseline Requirements section 9.2.1: subject alternative names extension (0: ok, 1 or more: error)" + }, + "BR_9_2_2_SUBJECT_COMMON_NAME": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 8, + "description": "Baseline Requirements section 9.2.2: subject common name field (0: present, in subject alt. names; 1: not present; 2: not present in subject alt. names)" + }, + "TAP_TO_LOAD_ENABLED": { + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 3, + "description": "Whether or not a user has tap-to-load enabled.", + "bug_numbers": [1208167] + }, + "ZOOMED_VIEW_ENABLED": { + "expires_in_version": "60", + "kind": "boolean", + "description": "Whether or not a user has the zoomed view (a.k.a. \"Magnify small areas\") enabled.", + "alert_emails": ["mobile-frontend@mozilla.com"], + "bug_numbers": [1235061] + }, + "TRACKING_PROTECTION_ENABLED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether or not a session has tracking protection enabled" + }, + "TRACKING_PROTECTION_PBM_DISABLED": { + "expires_in_version": "60", + "kind": "boolean", + "description": "Is the tracking protection in private browsing mode disabled?" + }, + "FENNEC_TRACKING_PROTECTION_STATE": { + "expires_in_version": "60", + "kind": "enumerated", + "n_values": 5, + "description": "The state of the user-visible tracking protection setting (0 = Disabled, 1 = Enabled in PB, 2 = Enabled)", + "alert_emails": ["mleibovic@mozilla.com"], + "bug_numbers": [1228090] + }, + "TRACKING_PROTECTION_SHIELD": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "Tracking protection shield (0 = not shown, 1 = loaded, 2 = blocked)" + }, + "TRACKING_PROTECTION_EVENTS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "Doorhanger shown = 0, Disable = 1, Enable = 2" + }, + "SERVICE_WORKER_REGISTRATION_LOADING": { + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 20, + "description": "Tracking how ServiceWorkerRegistrar loads data before the first content is shown. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SERVICE_WORKER_REQUEST_PASSTHROUGH": { + "expires_in_version": "50", + "kind": "boolean", + "description": "Intercepted fetch sending back same Request object. File bugs in Core::DOM in case of a Telemetry regression." + }, + "E10S_STATUS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 12, + "releaseChannelCollection": "opt-out", + "bug_numbers": [1241294], + "description": "Why e10s is enabled or disabled (0=ENABLED_BY_USER, 1=ENABLED_BY_DEFAULT, 2=DISABLED_BY_USER, 3=DISABLED_IN_SAFE_MODE, 4=DISABLED_FOR_ACCESSIBILITY, 5=DISABLED_FOR_MAC_GFX, 6=DISABLED_FOR_BIDI, 7=DISABLED_FOR_ADDONS, 8=FORCE_DISABLED, 9=DISABLED_FOR_XPLAYERS, 10=DISABLED_FOR_OS_VERSION)" + }, + "E10S_WINDOW": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a browser window is set as an e10s window" + }, + "E10S_BLOCKED_FROM_RUNNING": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether the e10s pref was set but it was blocked from running due to blacklisted conditions" + }, + "BLOCKED_ON_PLUGIN_MODULE_INIT_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "keyed": true, + "description": "Time (ms) that the main thread has been blocked on LoadModule and NP_Initialize in PluginModuleParent" + }, + "BLOCKED_ON_PLUGIN_INSTANCE_INIT_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "keyed": true, + "description": "Time (ms) that the main thread has been blocked on NPP_New in an IPC plugin" + }, + "BLOCKED_ON_PLUGIN_STREAM_INIT_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "keyed": true, + "description": "Time (ms) that the main thread has been blocked on NPP_NewStream in an IPC plugin" + }, + "BLOCKED_ON_PLUGINASYNCSURROGATE_WAITFORINIT_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "50", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "keyed": true, + "description": "Time (ms) that the main thread has been blocked on PluginAsyncSurrogate::WaitForInit in an IPC plugin" + }, + "BLOCKED_ON_PLUGIN_INSTANCE_DESTROY_MS": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 10000, + "n_buckets": 20, + "keyed": true, + "description": "Time (ms) that the main thread has been blocked on NPP_Destroy in an IPC plugin" + }, + "ONBEFOREUNLOAD_PROMPT_ACTION" : { + "expires_in_version": "45", + "kind": "enumerated", + "n_values": 3, + "description": "What button a user clicked in an onbeforeunload prompt. (Stay on Page = 0, Leave Page = 1, prompt aborted = 2)" + }, + "ONBEFOREUNLOAD_PROMPT_COUNT" : { + "expires_in_version": "45", + "kind": "count", + "description": "How many onbeforeunload prompts has the user encountered in their session?" + }, + "SUBPROCESS_ABNORMAL_ABORT": { + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Counts of plugin/content process abnormal shutdown, whether or not a crash report was available." + }, + "SUBPROCESS_CRASHES_WITH_DUMP": { + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Counts of plugin and content process crashes which are reported with a crash dump." + }, + "SUBPROCESS_LAUNCH_FAILURE": { + "alert_emails": ["haftandilian@mozilla.com"], + "expires_in_version": "never", + "bug_numbers": [1275430], + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Counts the number of times launching a subprocess fails. Counts are by subprocess-type using the GeckoProcessType enum." + }, + "PROCESS_CRASH_SUBMIT_ATTEMPT": { + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "An attempt to submit a crash. Keyed on the CrashManager Crash.type." + }, + "PROCESS_CRASH_SUBMIT_SUCCESS": { + "expires_in_version": "never", + "kind": "boolean", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "The submission status when main/plugin/content crashes are submitted. 1 is success, 0 is failure. Keyed on the CrashManager Crash.type." + }, + "STUMBLER_TIME_BETWEEN_UPLOADS_SEC": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 259200, + "description": "Stumbler: The time in seconds between uploads." + }, + "STUMBLER_VOLUME_BYTES_UPLOADED_PER_SEC": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 1000000, + "description": "Stumbler: Volume measurement of bytes uploaded, normalized to per-second." + }, + "STUMBLER_TIME_BETWEEN_START_SEC": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 259200, + "description": "Stumbler: The time between the service starts." + }, + "STUMBLER_UPLOAD_BYTES": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 1000000, + "description": "Stumbler: The bytes per upload." + }, + "STUMBLER_UPLOAD_OBSERVATION_COUNT": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 10000, + "description": "Stumbler: The observations per upload." + }, + "STUMBLER_UPLOAD_CELL_COUNT": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 10000, + "description": "Stumbler: The cells per upload." + }, + "STUMBLER_UPLOAD_WIFI_AP_COUNT": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 10000, + "description": "Stumbler: The Wi-Fi APs per upload." + }, + "STUMBLER_OBSERVATIONS_PER_DAY": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 10000, + "description": "Stumbler: The number of observations between upload events, normalized to per day." + }, + "STUMBLER_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC": { + "expires_in_version": "45", + "kind": "exponential", + "n_buckets": 50, + "high": 86400, + "description": "Stumbler: The time between receiving passive locations." + }, + "DATA_STORAGE_ENTRIES": { + "expires_in_version": "default", + "kind": "linear", + "high": 1024, + "n_buckets": 16, + "description": "The number of entries in persistent DataStorage (HSTS and HPKP data, basically)" + }, + "VIDEO_EME_PLAY_SUCCESS": { + "expires_in_version": "45", + "kind": "boolean", + "description": "EME video playback success or failure" + }, + "VIDEO_PLAY_TIME_MS" : { + "alert_emails": ["ajones@mozilla.com"], + "expires_in_version": "55", + "description": "Total time spent playing video in milliseconds. This reports the total play time for an HTML Media Element whenever it is suspended or resumed, such as when the page is unloaded, or when the mute status changes when the AudioChannelAPI pref is set.", + "kind": "exponential", + "high": 7200000, + "n_buckets": 100, + "bug_numbers": [1261955, 1127646] + }, + "VIDEO_HIDDEN_PLAY_TIME_MS" : { + "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"], + "expires_in_version": "55", + "description": "Total time spent playing video while element is hidden, in milliseconds. This reports the total hidden play time for an HTML Media Element whenever it is suspended or resumed, such as when the page is unloaded, or when the mute status changes when the AudioChannelAPI pref is set.", + "kind": "exponential", + "high": 7200000, + "n_buckets": 100, + "bug_numbers": [1285419] + }, + "VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE" : { + "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"], + "expires_in_version": "55", + "description": "Percentage of total time spent playing video while element is hidden. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,02160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.", + "keyed": true, + "kind": "linear", + "high": 100, + "n_buckets": 50, + "bug_numbers": [1287987] + }, + "VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE" : { + "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"], + "expires_in_version": "55", + "description": "Percentage of total time spent *not* fully decoding video while element is hidden (simulated, even when feature is not enabled). Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,02160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.", + "keyed": true, + "kind": "linear", + "high": 100, + "n_buckets": 50, + "bug_numbers": [1293145] + }, + "VIDEO_INTER_KEYFRAME_AVERAGE_MS" : { + "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"], + "expires_in_version": "55", + "description": "Average interval between video keyframes in played videos, in milliseconds. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,02160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.", + "keyed": true, + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "bug_numbers": [1289668] + }, + "VIDEO_INTER_KEYFRAME_MAX_MS" : { + "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"], + "expires_in_version": "55", + "description": "Maximum interval between video keyframes in played videos, in milliseconds; '0' means only 1 keyframe found. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,02160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.", + "keyed": true, + "kind": "exponential", + "high": 60000, + "n_buckets": 100, + "bug_numbers": [1289668] + }, + "VIDEO_SUSPEND_RECOVERY_TIME_MS" : { + "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"], + "expires_in_version": "55", + "description": "Time taken for a video to resume after decoding was suspended, in milliseconds. Keyed by audio presence, hw acceleration, and by height ranges (boundaries: 240. 480, 720, 1080, 2160), e.g.: 'V,0-240', 'AV(hw),2160+'; and 'All' will accumulate all percentages.", + "keyed": true, + "kind": "exponential", + "high": 10000, + "n_buckets": 100, + "bug_numbers": [1294349] + }, + "VIDEO_AS_CONTENT_SOURCE" : { + "alert_emails": ["ajones@mozilla.com", "kaku@mozilla.com"], + "expires_in_version": "58", + "description": "Usage of a {visible / invisible} video element as the source of {drawImage(), createPattern(), createImageBitmap() and captureStream()} APIs. (0 = ALL_VISIBLE, 1 = ALL_INVISIBLE, 2 = drawImage_VISIBLE, 3 = drawImage_INVISIBLE, 4 = createPattern_VISIBLE, 5 = createPattern_INVISIBLE, 6 = createImageBitmap_VISIBLE, 7 = createImageBitmap_INVISIBLE, 8 = captureStream_VISIBLE, 9 = captureStream_INVISIBLE)", + "kind": "enumerated", + "n_values": 12, + "bug_numbers": [1299718] + }, + "VIDEO_AS_CONTENT_SOURCE_IN_TREE_OR_NOT" : { + "alert_emails": ["ajones@mozilla.com", "kaku@mozilla.com"], + "expires_in_version": "58", + "description": "Usage of an invisible {in tree / not in tree} video element as the source of {drawImage(), createPattern(), createImageBitmap() and captureStream()} APIs. (0 = ALL_IN_TREE, 1 = ALL_NOT_IN_TREE, 2 = drawImage_IN_TREE, 3 = drawImage_NOT_IN_TREE, 4 = createPattern_IN_TREE, 5 = createPattern_NOT_IN_TREE, 6 = createImageBitmap_IN_TREE, 7 = createImageBitmap_NOT_IN_TREE, 8 = captureStream_IN_TREE, 9 = captureStream_NOT_IN_TREE)", + "kind": "enumerated", + "n_values": 12, + "bug_numbers": [1337301] + }, + "VIDEO_UNLOAD_STATE": { + "alert_emails": ["ajones@mozilla.com"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 5, + "description": "HTML Media Element state when unloading. ended = 0, paused = 1, stalled = 2, seeking = 3, other = 4", + "bug_numbers": [1261955, 1261955] + }, + "VIDEO_VP9_BENCHMARK_FPS": { + "alert_emails": ["ajones@mozilla.com"], + "expires_in_version": "55", + "bug_numbers": [1230265], + "kind": "linear", + "high": 1000, + "n_buckets": 100, + "description": "720p VP9 decode benchmark measurement in frames per second", + "releaseChannelCollection": "opt-out" + }, + "VIDEO_CDM_CREATED": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "58", + "bug_numbers": [1304207], + "kind": "enumerated", + "n_values": 6, + "description": "Note the type of CDM (0=ClearKey, 1=Primetime, 2=Widevine, 3=unknown) every time we successfully instantiate an EME MediaKeys object.", + "releaseChannelCollection": "opt-out" + }, + "VIDEO_CDM_GENERATE_REQUEST_CALLED": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "58", + "bug_numbers": [1305552], + "kind": "enumerated", + "n_values": 6, + "description": "Note the type of CDM (0=ClearKey, 1=Primetime, 2=Widevine, 3=unknown) every time we call MediaKeySession.generateRequest().", + "releaseChannelCollection": "opt-out" + }, + "MEDIA_CODEC_USED": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "never", + "keyed": true, + "kind": "count", + "description": "Count of use of audio/video codecs in HTMLMediaElements and WebAudio. Those with 'resource' prefix are approximate; report based on HTTP ContentType or sniffing. Those with 'webaudio' prefix are for WebAudio." + }, + "FX_SANITIZE_TOTAL": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Total time it takes to sanitize (ms)" + }, + "FX_SANITIZE_CACHE": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize the cache (ms)" + }, + "FX_SANITIZE_COOKIES_2": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize firefox cookies (ms). A subset of FX_SANITIZE_COOKIES." + }, + "FX_SANITIZE_LOADED_FLASH": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1251469], + "expires_in_version": "55", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize Flash when it's already loaded (ms). A subset of FX_SANITIZE_PLUGINS." + }, + "FX_SANITIZE_UNLOADED_FLASH": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1251469], + "expires_in_version": "55", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize Flash when it's not yet loaded (ms). A subset of FX_SANITIZE_PLUGINS." + }, + "FX_SANITIZE_HISTORY": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize history (ms)" + }, + "FX_SANITIZE_FORMDATA": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize stored form data (ms)" + }, + "FX_SANITIZE_DOWNLOADS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize recent downloads (ms)" + }, + "FX_SANITIZE_SESSIONS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize saved sessions (ms)" + }, + "FX_SANITIZE_SITESETTINGS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize site-specific settings (ms)" + }, + "FX_SANITIZE_OPENWINDOWS": { + "alert_emails": ["firefox-dev@mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 20, + "description": "Sanitize: Time it takes to sanitize the open windows list (ms)" + }, + "PWMGR_BLOCKLIST_NUM_SITES": { + "expires_in_version": "never", + "kind": "exponential", + "high": 100, + "n_buckets" : 10, + "description": "The number of sites for which the user has explicitly rejected saving logins" + }, + "PWMGR_FORM_AUTOFILL_RESULT": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values" : 20, + "description": "The result of auto-filling a login form. See http://mzl.la/1Mbs6jL for bucket descriptions." + }, + "PWMGR_LOGIN_LAST_USED_DAYS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 750, + "n_buckets" : 40, + "description": "Time in days each saved login was last used" + }, + "PWMGR_LOGIN_PAGE_SAFETY": { + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 8, + "description": "The safety of a page where we see a password field. (0: safe page & safe submit; 1: safe page & unsafe submit; 2: safe page & unknown submit; 3: unsafe page & safe submit; 4: unsafe page & unsafe submit; 5: unsafe page & unknown submit)" + }, + "PWMGR_MANAGE_COPIED_PASSWORD": { + "expires_in_version": "never", + "kind": "count", + "description": "Count of passwords copied from the password management interface" + }, + "PWMGR_MANAGE_COPIED_USERNAME": { + "expires_in_version": "never", + "kind": "count", + "description": "Count of usernames copied from the password management interface" + }, + "PWMGR_MANAGE_DELETED": { + "expires_in_version": "never", + "kind": "count", + "description": "Count of passwords deleted from the password management interface (including via Remove All)" + }, + "PWMGR_MANAGE_DELETED_ALL": { + "expires_in_version": "never", + "kind": "count", + "description": "Count of times that Remove All was used from the password management interface" + }, + "PWMGR_MANAGE_OPENED": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values" : 5, + "description": "Accumulates how the password management interface was opened. (0=Preferences, 1=Page Info)" + }, + "PWMGR_MANAGE_SORTED": { + "expires_in_version": "never", + "keyed": true, + "kind": "count", + "description": "Reports the column that logins are sorted by" + }, + "PWMGR_MANAGE_VISIBILITY_TOGGLED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether the visibility of passwords was toggled (0=Hide, 1=Show)" + }, + "PWMGR_NUM_PASSWORDS_PER_HOSTNAME": { + "expires_in_version": "never", + "kind": "linear", + "high": 21, + "n_buckets" : 20, + "description": "The number of passwords per hostname" + }, + "PWMGR_NUM_SAVED_PASSWORDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 750, + "n_buckets" : 50, + "description": "Total number of saved logins, including those that cannot be decrypted" + }, + "PWMGR_NUM_HTTPAUTH_PASSWORDS": { + "expires_in_version": "never", + "kind": "exponential", + "high": 750, + "n_buckets" : 50, + "description": "Number of HTTP Auth logins" + }, + "PWMGR_PASSWORD_INPUT_IN_FORM": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether an is associated with a
when it is added to a document" + }, + "PWMGR_PROMPT_REMEMBER_ACTION" : { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "Action taken by user through prompt for creating a login. (0=Prompt displayed [always recorded], 1=Add login, 2=Don't save now, 3=Never save)" + }, + "PWMGR_PROMPT_UPDATE_ACTION" : { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "Action taken by user through prompt for modifying a login. (0=Prompt displayed [always recorded], 1=Update login)" + }, + "PWMGR_SAVING_ENABLED": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Number of users who have password saving on globally" + }, + "PWMGR_USERNAME_PRESENT": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Whether a saved login has a username" + }, + "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN": { + "expires_in_version": "45", + "kind": "count", + "description": "The number of Sync 1.1 -> Sync 1.5 migration sentinels seen by Android Sync." + }, + "FENNEC_SYNC11_MIGRATIONS_FAILED": { + "expires_in_version": "45", + "kind": "count", + "description": "The number of Sync 1.1 -> Sync 1.5 migrations that failed during Android Sync." + }, + "FENNEC_SYNC11_MIGRATIONS_SUCCEEDED": { + "expires_in_version": "45", + "kind": "count", + "description": "The number of Sync 1.1 -> Sync 1.5 migrations that succeeded during Android Sync." + }, + "FENNEC_SYNC11_MIGRATION_NOTIFICATIONS_OFFERED": { + "expires_in_version": "45", + "kind": "exponential", + "high": 500, + "n_buckets": 5, + "description": "The number of Sync 1.5 'complete upgrade/migration' notifications offered by Android Sync." + }, + "FENNEC_SYNC11_MIGRATIONS_COMPLETED": { + "expires_in_version": "45", + "kind": "count", + "description": "The number of Sync 1.5 migrations completed by Android Sync." + }, + "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED": { + "expires_in_version": "45", + "kind": "count", + "description": "Counts the number of times that a sync has started." + }, + "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED": { + "expires_in_version": "45", + "kind": "count", + "description": "Counts the number of times that a sync has completed with no errors." + }, + "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED": { + "expires_in_version": "45", + "kind": "count", + "description": "Counts the number of times that a sync has failed with errors." + }, + "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF": { + "expires_in_version": "45", + "kind": "count", + "description": "Counts the number of times that a sync has failed because of trying to sync before server backoff interval has passed." + }, + "SLOW_SCRIPT_NOTICE_COUNT": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Count slow script notices" + }, + "SLOW_SCRIPT_PAGE_COUNT": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1251667], + "description": "The number of pages that trigger slow script notices" + }, + "SLOW_SCRIPT_NOTIFY_DELAY": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "bug_numbers": [1271978], + "description": "The difference between the js slow script timeout for content set in prefs and the actual time we waited before displaying the notification (msec)." + }, + "PLUGIN_HANG_NOTICE_COUNT": { + "alert_emails": ["perf-telemetry-alerts@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Count plugin hang notices in e10s" + }, + "SERVICE_WORKER_SPAWN_ATTEMPTS": { + "expires_in_version": "50", + "kind": "count", + "description": "Count attempts to spawn a ServiceWorker for a domain. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SERVICE_WORKER_WAS_SPAWNED": { + "expires_in_version": "50", + "kind": "count", + "description": "Count ServiceWorkers that really did get a thread created for them. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SERVICE_WORKER_SPAWN_GETS_QUEUED": { + "alert_emails": ["amarchesini@mozilla.com"], + "bug_numbers": [1286895], + "expires_in_version": "never", + "kind": "count", + "description": "Tracking whether a ServiceWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SHARED_WORKER_SPAWN_GETS_QUEUED": { + "alert_emails": ["amarchesini@mozilla.com"], + "bug_numbers": [1286895], + "expires_in_version": "never", + "kind": "count", + "description": "Tracking whether a SharedWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression." + }, + "DEDICATED_WORKER_SPAWN_GETS_QUEUED": { + "alert_emails": ["amarchesini@mozilla.com"], + "bug_numbers": [1286895], + "expires_in_version": "never", + "kind": "count", + "description": "Tracking whether a DedicatedWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SERVICE_WORKER_REGISTRATIONS": { + "expires_in_version": "50", + "kind": "count", + "description": "Count how many registrations occurs. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SERVICE_WORKER_CONTROLLED_DOCUMENTS": { + "expires_in_version": "50", + "kind": "count", + "description": "Count whenever a document is controlled. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SERVICE_WORKER_UPDATED": { + "expires_in_version": "50", + "kind": "count", + "description": "Count ServiceWorkers scripts that are updated. File bugs in Core::DOM in case of a Telemetry regression." + }, + "SERVICE_WORKER_LIFE_TIME": { + "expires_in_version": "50", + "kind": "exponential", + "high": 120000, + "n_buckets": 20, + "description": "Tracking how long a ServiceWorker stays alive after it is spawned. File bugs in Core::DOM in case of a Telemetry regression." + }, + "GRAPHICS_SANITY_TEST": { + "expires_in_version": "never", + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","msreckovic@mozilla.com"], + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Reports results from the graphics sanity test to track which drivers are having problems (0=TEST_PASSED, 1=TEST_FAILED_RENDER, 2=TEST_FAILED_VIDEO, 3=TEST_CRASHED)" + }, + "READER_MODE_PARSE_RESULT" : { + "expires_in_version": "54", + "alert_emails": ["firefox-dev@mozilla.org", "gijs@mozilla.com"], + "kind": "enumerated", + "n_values": 5, + "description": "The result of trying to parse a document to show in reader view (0=Success, 1=Error too many elements, 2=Error in worker, 3=Error no article)" + }, + "READER_MODE_DOWNLOAD_RESULT" : { + "expires_in_version": "54", + "alert_emails": ["firefox-dev@mozilla.org", "gijs@mozilla.com"], + "kind": "enumerated", + "n_values": 5, + "description": "The result of trying to download a document to show in reader view (0=Success, 1=Error XHR, 2=Error no document)" + }, + "FENNEC_LOAD_SAVED_PAGE": { + "expires_in_version": "60", + "alert_emails": ["mobile-frontend@mozilla.com"], + "kind": "enumerated", + "n_values": 10, + "description": "How often users load saved items when online/offline (0=RL online, 1=RL offline, 2=BM online, 3=BM offline)", + "bug_numbers": [1243387] + }, + "PERMISSIONS_SQL_CORRUPTED": { + "expires_in_version": "never", + "kind": "count", + "description": "Record the permissions.sqlite init failure" + }, + "DEFECTIVE_PERMISSIONS_SQL_REMOVED": { + "expires_in_version": "never", + "kind": "count", + "description": "Record the removal of defective permissions.sqlite" + }, + "FENNEC_TABQUEUE_QUEUESIZE" : { + "expires_in_version": "never", + "kind": "exponential", + "high": 50, + "n_buckets": 10, + "description": "The number of tabs queued when opened." + }, + "FENNEC_CUSTOM_HOMEPAGE": { + "expires_in_version": "60", + "alert_emails": ["mobile-frontend@mozilla.com"], + "bug_numbers": [1239102], + "kind": "boolean", + "description": "Whether the user has set a custom homepage." + }, + "GRAPHICS_DRIVER_STARTUP_TEST": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Reports whether or not graphics drivers crashed during startup." + }, + "GRAPHICS_SANITY_TEST_OS_SNAPSHOT": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "releaseChannelCollection": "opt-out", + "description": "Reports whether the graphics sanity test passed an OS snapshot test. 0=Pass, 1=Fail, 2=Error, 3=Timed out." + }, + "DEVTOOLS_HUD_JANK": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "exponential", + "keyed": true, + "description": "The duration which a thread is blocked in ms, keyed by appName.", + "high": 5000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_REFLOW_DURATION": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "exponential", + "keyed": true, + "description": "The duration a reflow takes in ms, keyed by appName.", + "high": 1000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_REFLOWS": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "count", + "keyed": true, + "description": "A count of the number of reflows, keyed by appName." + }, + "DEVTOOLS_HUD_SECURITY_CATEGORY": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "enumerated", + "keyed": true, + "description": "The security error enums, keyed by appName.", + "n_values": 8 + }, + "DEVTOOLS_HUD_ERRORS": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "count", + "keyed": true, + "description": "Number of errors, keyed by appName." + }, + "DEVTOOLS_HUD_WARNINGS": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "count", + "keyed": true, + "description": "Number of warnings, keyed by appName." + }, + "DEVTOOLS_HUD_USS": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "low": 20000000, + "high": 100000000, + "n_buckets": 52, + "description": "The USS memory consumed by an application, keyed by appName." + }, + "DEVTOOLS_HUD_APP_STARTUP_TIME_CONTENTINTERACTIVE": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The duration in ms between application launch and the 'contentInteractive' performance mark, keyed by appName.", + "high": 2000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_STARTUP_TIME_NAVIGATIONINTERACTIVE": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The duration in ms between application launch and the 'navigationInteractive' performance mark, keyed by appName.", + "high": 3000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_STARTUP_TIME_NAVIGATIONLOADED": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The duration in ms between application launch and the 'navigationLoaded' performance mark, keyed by appName.", + "high": 4000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_STARTUP_TIME_VISUALLYLOADED": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The duration in ms between application launch and the 'visuallyLoaded' performance mark, keyed by appName.", + "high": 5000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_STARTUP_TIME_MEDIAENUMERATED": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The duration in ms between application launch and the 'mediaEnumerated' performance mark, keyed by appName.", + "high": 5000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_STARTUP_TIME_FULLYLOADED": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The duration in ms between application launch and the 'fullyLoaded' performance mark, keyed by appName.", + "high": 30000, + "n_buckets": 30 + }, + "DEVTOOLS_HUD_APP_STARTUP_TIME_SCANEND": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The duration in ms between application launch and the 'scanEnd' performance mark, keyed by appName.", + "high": 30000, + "n_buckets": 30 + }, + "DEVTOOLS_HUD_APP_MEMORY_CONTENTINTERACTIVE_V2": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The USS memory consumed by an application at the time of the 'contentInteractive' performance mark, keyed by appName.", + "low": 20000000, + "high": 30000000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_MEMORY_NAVIGATIONINTERACTIVE_V2": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The USS memory consumed by an application at the time of the 'navigationInteractive' performance mark, keyed by appName.", + "low": 20000000, + "high": 30000000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_MEMORY_NAVIGATIONLOADED_V2": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The USS memory consumed by an application at the time of the 'navigationLoaded' performance mark, keyed by appName.", + "low": 20000000, + "high": 30000000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_MEMORY_VISUALLYLOADED_V2": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The USS memory consumed by an application at the time of the 'visuallyLoaded' performance mark, keyed by appName.", + "low": 20000000, + "high": 30000000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_MEMORY_MEDIAENUMERATED_V2": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The USS memory consumed by an application at the time of the 'mediaEnumerated' performance mark, keyed by appName.", + "low": 20000000, + "high": 40000000, + "n_buckets": 10 + }, + "DEVTOOLS_HUD_APP_MEMORY_FULLYLOADED_V2": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The USS memory consumed by an application at the time of the 'fullyLoaded' performance mark, keyed by appName.", + "low": 20000000, + "high": 40000000, + "n_buckets": 20 + }, + "DEVTOOLS_HUD_APP_MEMORY_SCANEND_V2": { + "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"], + "expires_in_version": "52", + "kind": "linear", + "keyed": true, + "description": "The USS memory consumed by an application at the time of the 'scanEnd' performance mark, keyed by appName.", + "low": 20000000, + "high": 40000000, + "n_buckets": 20 + }, + "DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT": { + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1221619], + "description": "The number of heap snapshots taken by a user" + }, + "DEVTOOLS_MEMORY_IMPORT_SNAPSHOT_COUNT": { + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1221619], + "description": "The number of heap snapshots imported by a user" + }, + "DEVTOOLS_MEMORY_EXPORT_SNAPSHOT_COUNT": { + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1221619], + "description": "The number of heap snapshots exported by a user" + }, + "DEVTOOLS_MEMORY_FILTER_CENSUS": { + "expires_in_version": "56", + "kind": "boolean", + "bug_numbers": [1221619], + "description": "Whether a census tree was filtered or not" + }, + "DEVTOOLS_MEMORY_DIFF_CENSUS": { + "expires_in_version": "56", + "kind": "boolean", + "bug_numbers": [1221619], + "description": "Whether a census was the result of diffing or not" + }, + "DEVTOOLS_MEMORY_INVERTED_CENSUS": { + "expires_in_version": "56", + "kind": "boolean", + "bug_numbers": [1221619], + "description": "Whether a census tree was inverted or not" + }, + "DEVTOOLS_MEMORY_BREAKDOWN_CENSUS_COUNT": { + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1221619], + "keyed": true, + "description": "The number of times a given type of breakdown was used for a census" + }, + "DEVTOOLS_MEMORY_DOMINATOR_TREE_COUNT": { + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1221619], + "description": "The number of times a user requested a dominator tree be computed" + }, + "DEVTOOLS_MEMORY_BREAKDOWN_DOMINATOR_TREE_COUNT": { + "expires_in_version": "56", + "kind": "count", + "bug_numbers": [1221619], + "keyed": true, + "description": "The number of times a given type of breakdown was used for a dominator tree" + }, + "GRAPHICS_SANITY_TEST_REASON": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com"], + "expires_in_version": "43", + "kind": "enumerated", + "n_values": 20, + "releaseChannelCollection": "opt-out", + "description": "Reports why a graphics sanity test was run. 0=First Run, 1=App Updated, 2=Device Change, 3=Driver Change." + }, + "TRANSLATION_OPPORTUNITIES": { + "expires_in_version": "default", + "kind": "boolean", + "description": "A number of successful and failed attempts to translate a document" + }, + "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE": { + "expires_in_version": "default", + "kind": "boolean", + "keyed": true, + "description": "A number of successful and failed attempts to translate a document grouped by language" + }, + "TRANSLATED_PAGES": { + "expires_in_version": "default", + "kind": "count", + "description": "A number of sucessfully translated pages" + }, + "TRANSLATED_PAGES_BY_LANGUAGE": { + "expires_in_version": "default", + "kind": "count", + "keyed": true, + "description": "A number of sucessfully translated pages by language" + }, + "TRANSLATED_CHARACTERS": { + "expires_in_version": "default", + "kind": "exponential", + "high": 10240, + "n_buckets": 50, + "description": "A number of sucessfully translated characters" + }, + "DENIED_TRANSLATION_OFFERS": { + "expires_in_version": "default", + "kind": "count", + "description": "A number of tranlation offers the user denied" + }, + "AUTO_REJECTED_TRANSLATION_OFFERS": { + "expires_in_version": "default", + "kind": "count", + "description": "A number of auto-rejected tranlation offers" + }, + "REQUESTS_OF_ORIGINAL_CONTENT": { + "expires_in_version": "default", + "kind": "count", + "description": "A number of times the user requested to see the original content of a translated page" + }, + "CHANGES_OF_TARGET_LANGUAGE": { + "expires_in_version": "default", + "kind": "count", + "description": "A number of times when the target language was changed by the user" + }, + "CHANGES_OF_DETECTED_LANGUAGE": { + "expires_in_version": "default", + "kind": "boolean", + "description": "A number of changes of detected language before (true) or after (false) translating a page for the first time." + }, + "SHOULD_TRANSLATION_UI_APPEAR": { + "expires_in_version": "default", + "kind": "flag", + "description": "Tracks situations when the user opts for displaying translation UI" + }, + "SHOULD_AUTO_DETECT_LANGUAGE": { + "expires_in_version": "default", + "kind": "flag", + "description": "Tracks situations when the user opts for auto-detecting the language of a page" + }, + "PERMISSIONS_REMIGRATION_COMPARISON": { + "alert_emails": ["michael@thelayzells.com"], + "expires_in_version": "44", + "kind": "enumerated", + "n_values": 10, + "description": "Reports a comparison between row count of original and re-migration of the v7 permissions DB. 0=New == 0, 1=New < Old, 2=New == Old, 3=New > Old" + }, + "PERMISSIONS_MIGRATION_7_ERROR": { + "alert_emails": ["michael@thelayzells.com"], + "expires_in_version": "44", + "kind": "boolean", + "description": "Was there an error while performing the v7 permissions DB migration?" + }, + "PERF_MONITORING_TEST_CPU_RESCHEDULING_PROPORTION_MOVED": { + "alert_emails": ["dteller@mozilla.com"], + "expires_in_version": "48", + "kind": "linear", + "high": 100, + "n_buckets": 20, + "description": "Proportion (%) of reschedulings of the main process to another CPU during the execution of code inside a JS compartment. Updated while we are measuring jank." + }, + "PERF_MONITORING_SLOW_ADDON_JANK_US": { + "expires_in_version": "never", + "kind": "exponential", + "low": 1, + "high": 10000000, + "n_buckets": 20, + "keyed": true, + "description": "Contiguous time spent by an add-on blocking the main loop (microseconds, keyed by add-on ID)." + }, + "PERF_MONITORING_SLOW_ADDON_CPOW_US": { + "expires_in_version": "70", + "kind": "exponential", + "low": 1, + "high": 10000000, + "n_buckets": 20, + "keyed": true, + "description": "Contiguous time spent by an add-on blocking the main loop by performing a blocking cross-process call (microseconds, keyed by add-on ID)." + }, + "VIDEO_EME_REQUEST_SUCCESS_LATENCY_MS": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 60000, + "n_buckets": 60, + "releaseChannelCollection": "opt-out", + "description": "Time spent waiting for a navigator.requestMediaKeySystemAccess call to succeed." + }, + "VIDEO_EME_REQUEST_FAILURE_LATENCY_MS": { + "alert_emails": ["cpearce@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 60000, + "n_buckets": 60, + "releaseChannelCollection": "opt-out", + "description": "Time spent waiting for a navigator.requestMediaKeySystemAccess call to fail." + }, + "FXA_CONFIGURED": { + "alert_emails": ["fx-team@mozilla.com"], + "bug_numbers": [1236383], + "expires_in_version": "never", + "kind": "flag", + "releaseChannelCollection": "opt-out", + "description": "If the user is signed in to a Firefox Account on this device. Recorded once per session just after startup as Sync is initialized." + }, + "WEAVE_DEVICE_COUNT_DESKTOP": { + "alert_emails": ["fx-team@mozilla.com"], + "bug_numbers": [1232050], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "releaseChannelCollection": "opt-out", + "description": "Number of desktop devices (including this device) associated with this Sync account. Recorded each time Sync successfully completes the 'clients' engine." + }, + "WEAVE_DEVICE_COUNT_MOBILE": { + "alert_emails": ["fx-team@mozilla.com"], + "bug_numbers": [1232050], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "releaseChannelCollection": "opt-out", + "description": "Number of mobile devices associated with this Sync account. Recorded each time Sync successfully completes the 'clients' engine." + }, + "WEAVE_ENGINE_SYNC_ERRORS": { + "alert_emails": ["fx-team@mozilla.com"], + "bug_numbers": [1236383], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Exceptions thrown by a Sync engine. Keyed on the engine name." + }, + "CONTENT_DOCUMENTS_DESTROYED": { + "expires_in_version": "never", + "kind": "count", + "description": "Number of content documents destroyed; used in conjunction with use counter histograms" + }, + "TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED": { + "expires_in_version": "never", + "kind": "count", + "description": "Number of top-level content documents destroyed; used in conjunction with use counter histograms" + }, + "PUSH_API_USED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "flag", + "description": "A Push API subscribe() operation was performed at least once this session." + }, + "PUSH_API_PERMISSION_REQUESTED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Count of number of times the PermissionManager explicitly prompted user for Push Notifications permission" + }, + "PUSH_API_PERMISSION_DENIED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "User explicitly denied Push Notifications permission" + }, + "PUSH_API_PERMISSION_GRANTED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "User explicitly granted Push Notifications permission" + }, + "PUSH_API_SUBSCRIBE_ATTEMPT": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Push Service attempts to subscribe with Push Server." + }, + "PUSH_API_SUBSCRIBE_FAILED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Attempt to subscribe with Push Server failed." + }, + "PUSH_API_SUBSCRIBE_SUCCEEDED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Attempt to subscribe with Push Server succeeded." + }, + "PUSH_API_UNSUBSCRIBE_ATTEMPT": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Push Service attempts to unsubscribe with Push Server." + }, + "PUSH_API_UNSUBSCRIBE_FAILED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Attempt to unsubscribe with Push Server failed." + }, + "PUSH_API_UNSUBSCRIBE_SUCCEEDED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Attempt to unsubscribe with Push Server succeeded." + }, + "PUSH_API_SUBSCRIBE_WS_TIME": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 15000, + "n_buckets": 10, + "description": "Time taken to subscribe over WebSocket (ms)." + }, + "PUSH_API_SUBSCRIBE_HTTP2_TIME": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 15000, + "n_buckets": 10, + "description": "Time taken to subscribe over HTTP2 (ms)." + }, + "PUSH_API_QUOTA_EXPIRATION_TIME": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 31622400, + "n_buckets": 20, + "description": "Time taken for a push subscription to expire its quota (seconds). The maximum is just over an year." + }, + "PUSH_API_QUOTA_RESET_TO": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 200, + "n_buckets": 10, + "description": "The value a push record quota (a count) is reset to based on the user's browsing history." + }, + "PUSH_API_NOTIFICATION_RECEIVED": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Push notification was received from server." + }, + "PUSH_API_NOTIFICATION_RECEIVED_BUT_DID_NOT_NOTIFY": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 16, + "description": "Push notification was received from server, but not delivered to ServiceWorker. Enumeration values are defined in dom/push/PushService.jsm as kDROP_NOTIFICATION_REASON_*." + }, + "PUSH_API_NOTIFY": { + "releaseChannelCollection": "opt-out", + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Number of push messages that were successfully decrypted and delivered to a ServiceWorker." + }, + "PUSH_API_NOTIFY_REGISTRATION_LOST": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "description": "Attempt to notify ServiceWorker of push notification resubscription." + }, + "D3D11_SYNC_HANDLE_FAILURE": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bschouten@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com","ashughes@mozilla.com"], + "expires_in_version": "60", + "releaseChannelCollection": "opt-out", + "kind": "count", + "description": "Number of times the D3D11 compositor failed to get a texture sync handle." + }, + "GFX_CONTENT_FAILED_TO_ACQUIRE_DEVICE": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","msreckovic@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 6, + "description": "Failed to create a gfx content device. 0=content d3d11, 1=image d3d11, 2=d2d1." + }, + "GFX_CRASH": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 100, + "releaseChannelCollection": "opt-out", + "description": "Graphics Crash Reason (...)" + }, + "PLUGIN_ACTIVATION_COUNT": { + "alert_emails": ["cpeterson@mozilla.com"], + "expires_in_version": "53", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "bug_numbers": [722110,1260065], + "description": "Counts number of times a certain plugin has been activated." + }, + "SCROLL_INPUT_METHODS": { + "alert_emails": ["botond@mozilla.com"], + "bug_numbers": [1238137], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 64, + "description": "Count of scroll actions triggered by different input methods. See gfx/layers/apz/util/ScrollInputMethods.h for a list of possible values and their meanings." + }, + "WEB_NOTIFICATION_CLICKED": { + "releaseChannelCollection": "opt-out", + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1225336], + "expires_in_version": "55", + "kind": "count", + "description": "Count of times a web notification was clicked" + }, + "WEB_NOTIFICATION_MENU": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1225336], + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 5, + "description": "Count of times a contextual menu item was used from a Notification (0: DND, 1: Disable, 2: Settings)" + }, + "WEB_NOTIFICATION_SHOWN": { + "releaseChannelCollection": "opt-out", + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1225336], + "expires_in_version": "55", + "kind": "count", + "description": "Count of times a Notification was rendered (accounting for XUL DND). A system backend may put the notification directly into the tray if its own DND is on." + }, + "WEBFONT_DOWNLOAD_TIME": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time to download a webfont (ms)" + }, + "WEBFONT_DOWNLOAD_TIME_AFTER_START": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "n_buckets": 50, + "description": "Time after navigationStart webfont download completed (ms)" + }, + "WEBFONT_FONTTYPE": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "Font format type (woff/woff2/ttf/...)" + }, + "WEBFONT_SRCTYPE": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 5, + "description": "Font src type loaded (1 = local, 2 = url, 3 = data)" + }, + "WEBFONT_PER_PAGE": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "description": "Number of fonts loaded at page load" + }, + "WEBFONT_SIZE_PER_PAGE": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 50, + "description": "Size of all fonts loaded at page load (kb)" + }, + "WEBFONT_SIZE": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 50, + "description": "Size of font loaded (kb)" + }, + "WEBFONT_COMPRESSION_WOFF": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "description": "Compression ratio of WOFF data (%)" + }, + "WEBFONT_COMPRESSION_WOFF2": { + "alert_emails": ["jdaggett@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "description": "Compression ratio of WOFF2 data (%)" + }, + "WEBRTC_ICE_CHECKING_RATE": { + "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"], + "expires_in_version": "53", + "kind": "boolean", + "bug_numbers": [1188391], + "description": "The number of ICE connections which immediately failed (0) vs. reached at least checking state (1)." + }, + "ALERTS_SERVICE_DND_ENABLED": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1219030], + "expires_in_version": "50", + "kind": "boolean", + "description": "XUL-only: whether the user has toggled do not disturb." + }, + "ALERTS_SERVICE_DND_SUPPORTED_FLAG": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1219030], + "expires_in_version": "50", + "kind": "flag", + "description": "Whether the do not disturb option is supported. True if the browser uses XUL alerts." + }, + "WEB_NOTIFICATION_EXCEPTIONS_OPENED": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1219030], + "expires_in_version": "50", + "kind": "count", + "description": "Number of times the Notification Permissions dialog has been opened." + }, + "WEB_NOTIFICATION_PERMISSIONS": { + "releaseChannelCollection": "opt-out", + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1219030], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "description": "Number of origins with the web notifications permission (0 = denied, 1 = allowed)." + }, + "WEB_NOTIFICATION_PERMISSION_REMOVED": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1219030], + "expires_in_version": "50", + "kind": "enumerated", + "n_values": 10, + "description": "Number of removed web notifications permissions (0 = remove deny, 1 = remove allow)." + }, + "WEB_NOTIFICATION_SENDERS": { + "releaseChannelCollection": "opt-out", + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1219030], + "expires_in_version": "50", + "kind": "count", + "description": "Number of origins that have shown a web notification. Excludes system alerts like update reminders and add-ons." + }, + "YOUTUBE_REWRITABLE_EMBED_SEEN": { + "alert_emails": ["cpeterson@mozilla.com"], + "expires_in_version": "53", + "kind": "flag", + "bug_numbers": [1229971], + "releaseChannelCollection": "opt-out", + "description": "Flag activated whenever a rewritable youtube flash embed is seen during a session." + }, + "YOUTUBE_NONREWRITABLE_EMBED_SEEN": { + "alert_emails": ["cpeterson@mozilla.com"], + "expires_in_version": "53", + "kind": "flag", + "bug_numbers": [1237401], + "releaseChannelCollection": "opt-out", + "description": "Flag activated whenever a non-rewritable (enablejsapi=1) youtube flash embed is seen during a session." + }, + "PLUGIN_DRAWING_MODEL": { + "alert_emails": ["danderson@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "bug_numbers": [1229961], + "n_values": 12, + "description": "Plugin drawing model. 0 when windowed, otherwise NPDrawingModel + 1." + }, + "WEB_NOTIFICATION_REQUEST_PERMISSION_CALLBACK": { + "alert_emails": ["push@mozilla.com"], + "expires_in_version": "55", + "bug_numbers": [1241278], + "kind": "boolean", + "description": "Usage of the deprecated Notification.requestPermission() callback argument" + }, + "VIDEO_FASTSEEK_USED": { + "alert_emails": ["lchristie@mozilla.com", "cpearce@mozilla.com"], + "expires_in_version": "55", + "bug_numbers": [1245982], + "kind": "count", + "description": "Uses of HTMLMediaElement.fastSeek" + }, + "VIDEO_DROPPED_FRAMES_PROPORTION" : { + "alert_emails": ["lchristie@mozilla.com", "cpearce@mozilla.com"], + "expires_in_version": "55", + "kind": "linear", + "high": 100, + "n_buckets": 50, + "bug_numbers": [1238433], + "description": "Percentage of frames decoded frames dropped in an HTMLVideoElement" + }, + "TAB_SWITCH_CACHE_POSITION": { + "expires_in_version": "55", + "bug_numbers": [1242013], + "kind": "linear", + "high": 100, + "n_buckets": 50, + "description": "Position in (theoretical) tab cache of tab being switched to" + }, + "REMOTE_JAR_PROTOCOL_USED": { + "alert_emails": ["dev-platform@lists.mozilla.org"], + "expires_in_version": "55", + "bug_numbers": [1255934], + "kind": "count", + "description": "Remote JAR protocol usage" + }, + "MEDIA_DECODER_BACKEND_USED": { + "alert_emails": ["danderson@mozilla.com"], + "bug_numbers": [1259695], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 10, + "description": "Media decoder backend (0=WMF Software, 1=DXVA2D3D9, 2=DXVA2D3D11)" + }, + "PLUGIN_BLOCKED_FOR_STABILITY": { + "alert_emails": ["cpeterson@mozilla.com"], + "expires_in_version": "52", + "kind": "count", + "bug_numbers": [1237198], + "description": "Count plugins blocked for stability" + }, + "PLUGIN_TINY_CONTENT": { + "alert_emails": ["cpeterson@mozilla.com"], + "expires_in_version": "52", + "kind": "count", + "bug_numbers": [1237198], + "description": "Count tiny plugin content" + }, + "IPC_MESSAGE_SIZE": { + "alert_emails": ["wmccloskey@mozilla.com"], + "bug_numbers": [1260908], + "expires_in_version": "55", + "kind": "exponential", + "high": 8000000, + "n_buckets": 50, + "keyed": true, + "description": "Measures the size of IPC messages by message name" + }, + "IPC_REPLY_SIZE": { + "alert_emails": ["wmccloskey@mozilla.com"], + "bug_numbers": [1264820], + "expires_in_version": "55", + "kind": "exponential", + "high": 8000000, + "n_buckets": 50, + "keyed": true, + "description": "Measures the size of IPC messages by message name" + }, + "MESSAGE_MANAGER_MESSAGE_SIZE2": { + "alert_emails": ["wmccloskey@mozilla.com","amccreight@mozilla.com"], + "bug_numbers": [1260908], + "expires_in_version": "55", + "kind": "exponential", + "low": 8192, + "high": 8000000, + "n_buckets": 50, + "keyed": true, + "description": "Each key is the message name, with digits removed, from an async message manager message that was larger than 8192 bytes, recorded in the sending process at the time of sending." + }, + "REJECTED_MESSAGE_MANAGER_MESSAGE": { + "alert_emails": ["amccreight@mozilla.com"], + "bug_numbers": [1272423], + "expires_in_version": "55", + "kind": "count", + "keyed": true, + "description": "Each key is the message name, with digits removed, from an async message manager message that was rejected for being over approximately 128MB, recorded in the sending process at the time of sending." + }, + "SANDBOX_BROKER_INITIALIZED": { + "alert_emails": ["bowen@mozilla.com"], + "bug_numbers": [1256992], + "expires_in_version": "55", + "kind": "boolean", + "description": "Result of call to SandboxBroker::Initialize" + }, + "SANDBOX_HAS_SECCOMP_BPF": { + "alert_emails": ["gcp@mozilla.com"], + "bug_numbers": [1098428], + "expires_in_version": "55", + "kind": "boolean", + "cpp_guard": "XP_LINUX", + "description": "Whether the system has seccomp-bpf capability" + }, + "SANDBOX_HAS_SECCOMP_TSYNC": { + "alert_emails": ["gcp@mozilla.com"], + "bug_numbers": [1098428], + "expires_in_version": "55", + "kind": "boolean", + "cpp_guard": "XP_LINUX", + "description": "Whether the system has seccomp-bpf thread-sync capability" + }, + "SANDBOX_HAS_USER_NAMESPACES": { + "alert_emails": ["gcp@mozilla.com"], + "bug_numbers": [1098428], + "expires_in_version": "55", + "kind": "boolean", + "cpp_guard": "XP_LINUX", + "description": "Whether our process succedeed in creating a user namespace" + }, + "SANDBOX_HAS_USER_NAMESPACES_PRIVILEGED": { + "alert_emails": ["gcp@mozilla.com"], + "bug_numbers": [1098428], + "expires_in_version": "55", + "kind": "boolean", + "cpp_guard": "XP_LINUX", + "description": "Whether the system has the capability to create privileged user namespaces" + }, + "SANDBOX_MEDIA_ENABLED": { + "alert_emails": ["gcp@mozilla.com"], + "bug_numbers": [1098428], + "expires_in_version": "55", + "kind": "boolean", + "cpp_guard": "XP_LINUX", + "description": "Whether the sandbox is enabled for media/GMP plugins" + }, + "SANDBOX_CONTENT_ENABLED": { + "alert_emails": ["gcp@mozilla.com"], + "bug_numbers": [1098428], + "expires_in_version": "55", + "kind": "boolean", + "cpp_guard": "XP_LINUX", + "description": "Whether the sandbox is enabled for the content process" + }, + "SYNC_WORKER_OPERATION": { + "alert_emails": ["amarchesini@mozilla.com", "khuey@mozilla.com" ], + "bug_numbers": [1267904], + "expires_in_version": "never", + "kind": "exponential", + "high": 5000, + "n_buckets": 20, + "keyed": true, + "description": "Tracking how long a Worker thread is blocked when a sync operation is executed on the main-thread." + }, + "SUBPROCESS_KILL_HARD": { + "alert_emails": ["wmccloskey@mozilla.com"], + "bug_numbers": [1269961], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-out", + "description": "Counts the number of times a subprocess was forcibly killed, and the reason." + }, + "FX_CONTENT_CRASH_DUMP_UNAVAILABLE": { + "alert_emails": ["wmccloskey@mozilla.com"], + "bug_numbers": [1269961], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Counts the number of times that about:tabcrashed was unable to find a crash dump." + }, + "FX_CONTENT_CRASH_PRESENTED": { + "alert_emails": ["wmccloskey@mozilla.com"], + "bug_numbers": [1269961], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Counts the number of times that about:tabcrashed appeared and found a crash dump." + }, + "FX_CONTENT_CRASH_NOT_SUBMITTED": { + "alert_emails": ["wmccloskey@mozilla.com"], + "bug_numbers": [1269961], + "expires_in_version": "never", + "kind": "count", + "releaseChannelCollection": "opt-out", + "description": "Counts the number of times that about:tabcrashed was unloaded without submitting." + }, + "ABOUTCRASHES_OPENED_COUNT": { + "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bgirard@mozilla.com","msreckovic@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "bug_numbers": [1276714, 1276716], + "description": "Number of times about:crashes has been opened.", + "releaseChannelCollection": "opt-out" + }, + "D3D9_COMPOSITING_FAILURE_ID": { + "alert_emails": ["bgirard@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "D3D9 compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.", + "bug_numbers": [1002846] + }, + "D3D11_COMPOSITING_FAILURE_ID": { + "alert_emails": ["bgirard@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "D3D11 compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.", + "bug_numbers": [1002846] + }, + "OPENGL_COMPOSITING_FAILURE_ID": { + "alert_emails": ["bgirard@mozilla.com"], + "expires_in_version": "never", + "kind": "count", + "keyed": true, + "description": "OpenGL compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.", + "bug_numbers": [1002846] + }, + "XHR_IN_WORKER": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "bug_numbers": [1280229], + "description": "Number of the use of XHR in workers." + }, + "WEBVTT_TRACK_KINDS": { + "alert_emails": ["alwu@mozilla.com"], + "expires_in_version": "55", + "kind": "enumerated", + "n_values": 10, + "bug_numbers": [1280644], + "description": "Number of the use of the subtitles kind track. 0=Subtitles, 1=Captions, 2=Descriptions, 3=Chapters, 4=Metadata, 5=Undefined Error", + "releaseChannelCollection": "opt-out" + }, + "WEBVTT_USED_VTT_CUES": { + "alert_emails": ["alwu@mozilla.com"], + "expires_in_version": "55", + "kind": "count", + "bug_numbers": [1280644], + "description": "Number of the use of the vtt cues.", + "releaseChannelCollection": "opt-out" + }, + "BLINK_FILESYSTEM_USED": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "bug_numbers": [1272501], + "releaseChannelCollection": "opt-out", + "description": "Webkit/Blink filesystem used" + }, + "WEBKIT_DIRECTORY_USED": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "never", + "kind": "boolean", + "bug_numbers": [1272501], + "releaseChannelCollection": "opt-out", + "description": "HTMLInputElement.webkitdirectory attribute used" + }, + "CONTAINER_USED": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "55", + "kind": "enumerated", + "bug_numbers": [1276006], + "n_values": 5, + "description": "Records a value each time a builtin container is opened. 1=personal 2=work 3=banking 4=shopping. Does not record usage of user-created containers." + }, + "UNIQUE_CONTAINERS_OPENED": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "never", + "bug_numbers": [1276006], + "kind": "count", + "description": "Tracking the unique number of opened Containers." + }, + "TOTAL_CONTAINERS_OPENED": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "never", + "bug_numbers": [1276006], + "kind": "count", + "description": "Tracking the total number of opened Containers." + }, + "FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE": { + "alert_emails": ["jh+bugzilla@buttercookie.de"], + "expires_in_version": "56", + "kind": "flag", + "bug_numbers": [1284017], + "description": "When restoring tabs on startup, reading from sessionstore.js failed, even though the file exists and is not containing an explicitly empty window.", + "cpp_guard": "ANDROID" + }, + "SHARED_WORKER_COUNT": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "58", + "kind": "count", + "bug_numbers": [1295980], + "description": "Number of the use of SharedWorkers." + }, + "FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP": { + "alert_emails": ["jh+bugzilla@buttercookie.de"], + "expires_in_version": "56", + "kind": "flag", + "bug_numbers": [1190627], + "description": "When restoring tabs on startup, reading from sessionstore.js failed, but sessionstore.bak was read successfully.", + "cpp_guard": "ANDROID" + }, + "NUMBER_OF_PROFILES": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "58", + "bug_numbers": [1296606], + "kind": "count", + "description": "Number of named browser profiles for the current user, as reported by the profile service at startup." + }, + "WEB_PERMISSION_CLEARED": { + "alert_emails": ["firefox-dev@mozilla.org"], + "bug_numbers": [1286118], + "expires_in_version": "55", + "kind": "enumerated", + "keyed": true, + "n_values": 6, + "description": "Number of revoke actions on permissions in the control center, keyed by permission id. Values represent the permission type that was revoked. (0=unknown, 1=permanently allowed, 2=permanently blocked, 3=temporarily allowed, 4=temporarily blocked)" + }, + "JS_AOT_USAGE": { + "alert_emails": ["luke@mozilla.com", "bbouvier@mozilla.com"], + "bug_numbers": [1288778], + "expires_in_version": "56", + "kind": "enumerated", + "n_values": 4, + "description": "Counts the number of asm.js vs WebAssembly modules instanciations, at the time modules are getting instanciated." + }, + "DOCUMENT_WITH_EXPANDED_PRINCIPAL": { + "alert_emails": ["dev-platform@lists.mozilla.org"], + "bug_numbers": [1301123], + "expires_in_version": "58", + "kind": "count", + "description": "Number of documents encountered using an expanded principal." + }, + "CONTENT_PAINT_TIME": { + "alert_emails": ["danderson@mozilla.com"], + "bug_numbers": [1309442], + "expires_in_version": "56", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "Time spent in the paint pipeline for content." + }, + "CONTENT_LARGE_PAINT_PHASE_WEIGHT": { + "alert_emails": ["danderson@mozilla.com"], + "bug_numbers": [1309442], + "expires_in_version": "56", + "keyed": true, + "kind": "linear", + "high": 100, + "n_buckets": 10, + "description": "Percentage of time taken by phases in expensive content paints." + }, + "NARRATE_CONTENT_BY_LANGUAGE_2": { + "alert_emails": ["eisaacson@mozilla.com"], + "bug_numbers": [1308030, 1324868], + "releaseChannelCollection": "opt-out", + "expires_in_version": "56", + "kind": "enumerated", + "keyed": true, + "n_values": 4, + "description": "Number of Narrate initialization attempts and successes broken up by content's language (ISO 639-1 code) (0 = initialization attempt, 1 = successfully initialized)" + }, + "NARRATE_CONTENT_SPEAKTIME_MS": { + "alert_emails": ["eisaacson@mozilla.com"], + "bug_numbers": [1308030], + "releaseChannelCollection": "opt-out", + "expires_in_version": "56", + "kind": "linear", + "high": 300000, + "n_buckets": 30, + "description": "Time in MS that content is narrated in 10 second increments up to 5 minutes" + }, + "HANDLE_UNLOAD_MS": { + "alert_emails": ["kchen@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "bug_numbers": [1301346], + "description": "The time spent handling unload event in milliseconds. It measures all documents and subframes separately. If there are multiple handlers for the unload event in a document, this will record a single value across all handlers in the document." + }, + "HANDLE_BEFOREUNLOAD_MS": { + "alert_emails": ["kchen@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "bug_numbers": [1301346], + "description": "The time spent handling beforeunload event in milliseconds. It measures all documents and subframes separately. If there are multiple handlers for the unload event in a document, this will record a single value across all handlers in the document." + }, + "TABCHILD_PAINT_TIME": { + "alert_emails": ["mconley@mozilla.com"], + "bug_numbers": [1313686], + "expires_in_version": "56", + "kind": "exponential", + "high": 1000, + "n_buckets": 50, + "description": "Time spent painting the contents of a remote browser (ms).", + "releaseChannelCollection": "opt-out" + }, + "TIME_TO_NON_BLANK_PAINT_MS": { + "alert_emails": ["hkirschner@mozilla.com"], + "expires_in_version": "55", + "kind": "exponential", + "high": 100000, + "n_buckets": 100, + "bug_numbers": [1307242], + "description": "The time between navigation start and the first non-blank paint of a foreground root content document, in milliseconds. This only records documents that were in an active docshell throughout the whole time between navigation start and non-blank paint. The non-blank paint timestamp is taken during display list building and does not include rasterization or compositing of that paint." + }, + "MOZ_BLOB_IN_XHR": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "58", + "kind": "boolean", + "bug_numbers": [1335365], + "releaseChannelCollection": "opt-out", + "description": "XMLHttpRequest.responseType set to moz-blob" + }, + "MOZ_CHUNKED_TEXT_IN_XHR": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "58", + "kind": "boolean", + "bug_numbers": [1335365], + "releaseChannelCollection": "opt-out", + "description": "XMLHttpRequest.responseType set to moz-chunked-text" + }, + "MOZ_CHUNKED_ARRAYBUFFER_IN_XHR": { + "alert_emails": ["amarchesini@mozilla.com"], + "expires_in_version": "58", + "kind": "boolean", + "bug_numbers": [1335365], + "releaseChannelCollection": "opt-out", + "description": "XMLHttpRequest.responseType set to moz-chunked-arraybuffer" + } +} diff --git a/toolkit/components/telemetry/Makefile.in b/toolkit/components/telemetry/Makefile.in new file mode 100644 index 000000000..52016707c --- /dev/null +++ b/toolkit/components/telemetry/Makefile.in @@ -0,0 +1,17 @@ +# +# 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/. + +include $(topsrcdir)/config/rules.mk + +# This is so hacky. Waiting on bug 988938. +addondir = $(srcdir)/tests/addons +testdir = $(topobjdir)/_tests/xpcshell/toolkit/components/telemetry/tests/unit + +misc:: $(call mkdir_deps,$(testdir)) + $(EXIT_ON_ERROR) \ + for dir in $(addondir)/*; do \ + base=`basename $$dir`; \ + (cd $$dir && zip -qr $(testdir)/$$base.xpi *); \ + done diff --git a/toolkit/components/telemetry/ProcessedStack.h b/toolkit/components/telemetry/ProcessedStack.h new file mode 100644 index 000000000..2bda55007 --- /dev/null +++ b/toolkit/components/telemetry/ProcessedStack.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef ProcessedStack_h__ +#define ProcessedStack_h__ + +#include +#include + +namespace mozilla { +namespace Telemetry { + +// This class represents a stack trace and the modules referenced in that trace. +// It is designed to be easy to read and write to disk or network and doesn't +// include any logic on how to collect or read the information it stores. +class ProcessedStack +{ +public: + ProcessedStack(); + size_t GetStackSize() const; + size_t GetNumModules() const; + + struct Frame + { + // The offset of this program counter in its module or an absolute pc. + uintptr_t mOffset; + // The index to pass to GetModule to get the module this program counter + // was in. + uint16_t mModIndex; + }; + struct Module + { + // The file name, /foo/bar/libxul.so for example. + std::string mName; + std::string mBreakpadId; + + bool operator==(const Module& other) const; + }; + + const Frame &GetFrame(unsigned aIndex) const; + void AddFrame(const Frame& aFrame); + const Module &GetModule(unsigned aIndex) const; + void AddModule(const Module& aFrame); + + void Clear(); + +private: + std::vector mModules; + std::vector mStack; +}; + +// Get the current list of loaded modules, filter and pair it to the provided +// stack. We let the caller collect the stack since different callers have +// different needs (current thread X main thread, stopping the thread, etc). +ProcessedStack +GetStackAndModules(const std::vector &aPCs); + +} // namespace Telemetry +} // namespace mozilla + +#endif // ProcessedStack_h__ diff --git a/toolkit/components/telemetry/ScalarInfo.h b/toolkit/components/telemetry/ScalarInfo.h new file mode 100644 index 000000000..6c9d8aade --- /dev/null +++ b/toolkit/components/telemetry/ScalarInfo.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef TelemetryScalarInfo_h__ +#define TelemetryScalarInfo_h__ + +// This module is internal to Telemetry. It defines a structure that holds the +// scalar info. It should only be used by TelemetryScalarData.h automatically +// generated file and TelemetryScalar.cpp. This should not be used anywhere else. +// For the public interface to Telemetry functionality, see Telemetry.h. + +namespace { +struct ScalarInfo { + uint32_t kind; + uint32_t name_offset; + uint32_t expiration_offset; + uint32_t dataset; + bool keyed; + + const char *name() const; + const char *expiration() const; +}; +} // namespace + +#endif // TelemetryScalarInfo_h__ diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml new file mode 100644 index 000000000..e95819879 --- /dev/null +++ b/toolkit/components/telemetry/Scalars.yaml @@ -0,0 +1,298 @@ +# This file contains a definition of the scalar probes that are recorded in Telemetry. +# They are submitted with the "main" pings and can be inspected in about:telemetry. + +# The following section contains the aushelper system add-on scalars. +aushelper: + websense_reg_version: + bug_numbers: + - 1305847 + description: The Websense version from the Windows registry. + expires: "60" + kind: string + notification_emails: + - application-update-telemetry-alerts@mozilla.com + release_channel_collection: opt-out + +# The following section contains the browser engagement scalars. +browser.engagement: + max_concurrent_tab_count: + bug_numbers: + - 1271304 + description: > + The count of maximum number of tabs open during a subsession, + across all windows, including tabs in private windows and restored + at startup. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + tab_open_event_count: + bug_numbers: + - 1271304 + description: > + The count of tab open events per subsession, across all windows, after the + session has been restored. This includes tab open events from private windows + and from manual session restorations (i.e. after crashes and from about:home). + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + max_concurrent_window_count: + bug_numbers: + - 1271304 + description: > + The count of maximum number of browser windows open during a subsession. This + includes private windows and the ones opened when starting the browser. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + window_open_event_count: + bug_numbers: + - 1271304 + description: > + The count of browser window open events per subsession, after the session + has been restored. The count includes private windows and the ones from manual + session restorations (i.e. after crashes and from about:home). + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + total_uri_count: + bug_numbers: + - 1271313 + description: > + The count of the total non-unique http(s) URIs visited in a subsession, including + page reloads, after the session has been restored. This does not include background + page requests and URIs from embedded pages or private browsing. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + unfiltered_uri_count: + bug_numbers: + - 1304647 + description: > + The count of the total non-unique URIs visited in a subsession, not restricted to + a specific protocol, including page reloads and about:* pages (other than initial + pages such as about:blank, ...), after the session has been restored. This does + not include background page requests and URIs from embedded pages or private browsing. + expires: "55" + kind: uint + notification_emails: + - bcolloran@mozilla.com + release_channel_collection: opt-out + + unique_domains_count: + bug_numbers: + - 1271310 + description: > + The count of the unique domains visited in a subsession, after the session + has been restored. Subdomains under eTLD are aggregated after the first level + (i.e. test.example.com and other.example.com are only counted once). + This does not include background page requests and domains from embedded pages + or private browsing. The count is limited to 100 unique domains. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + +# The following section contains the browser engagement scalars. +browser.engagement.navigation: + urlbar: + bug_numbers: + - 1271313 + description: > + The count URI loads triggered in a subsession from the urlbar (awesomebar), + broken down by the originating action. + expires: "55" + kind: uint + keyed: true + notification_emails: + - bcolloran@mozilla.com + release_channel_collection: opt-out + + searchbar: + bug_numbers: + - 1271313 + description: > + The count URI loads triggered in a subsession from the searchbar, + broken down by the originating action. + expires: "55" + kind: uint + keyed: true + notification_emails: + - bcolloran@mozilla.com + release_channel_collection: opt-out + + about_home: + bug_numbers: + - 1271313 + description: > + The count URI loads triggered in a subsession from about:home, + broken down by the originating action. + expires: "55" + kind: uint + keyed: true + notification_emails: + - bcolloran@mozilla.com + release_channel_collection: opt-out + + about_newtab: + bug_numbers: + - 1271313 + description: > + The count URI loads triggered in a subsession from about:newtab, + broken down by the originating action. + expires: "55" + kind: uint + keyed: true + notification_emails: + - bcolloran@mozilla.com + release_channel_collection: opt-out + + contextmenu: + bug_numbers: + - 1271313 + description: > + The count URI loads triggered in a subsession from the contextmenu, + broken down by the originating action. + expires: "55" + kind: uint + keyed: true + notification_emails: + - bcolloran@mozilla.com + release_channel_collection: opt-out + +# The following section is for probes testing the Telemetry system. They will not be +# submitted in pings and are only used for testing. +telemetry.test: + unsigned_int_kind: + bug_numbers: + - 1276190 + description: > + This is a test uint type with a really long description, maybe spanning even multiple + lines, to just prove a point: everything works just fine. + expires: never + kind: uint + notification_emails: + - telemetry-client-dev@mozilla.com + + string_kind: + bug_numbers: + - 1276190 + description: A string test type with a one line comment that works just fine! + expires: never + kind: string + notification_emails: + - telemetry-client-dev@mozilla.com + + boolean_kind: + bug_numbers: + - 1281214 + description: A boolean test type with a one line comment that works just fine! + expires: never + kind: boolean + notification_emails: + - telemetry-client-dev@mozilla.com + + expired: + bug_numbers: + - 1276190 + description: This is an expired testing scalar; not meant to be touched. + expires: 4.0a1 + kind: uint + notification_emails: + - telemetry-client-dev@mozilla.com + + unexpired: + bug_numbers: + - 1276190 + description: This is an unexpired testing scalar; not meant to be touched. + expires: "375.0" + kind: uint + notification_emails: + - telemetry-client-dev@mozilla.com + + release_optin: + bug_numbers: + - 1276190 + description: A testing scalar; not meant to be touched. + expires: never + kind: uint + notification_emails: + - telemetry-client-dev@mozilla.com + release_channel_collection: opt-in + + release_optout: + bug_numbers: + - 1276190 + description: A testing scalar; not meant to be touched. + expires: never + kind: uint + notification_emails: + - telemetry-client-dev@mozilla.com + release_channel_collection: opt-out + + keyed_release_optin: + bug_numbers: + - 1277806 + description: A testing scalar; not meant to be touched. + expires: never + kind: uint + keyed: true + notification_emails: + - telemetry-client-dev@mozilla.com + release_channel_collection: opt-in + + keyed_release_optout: + bug_numbers: + - 1277806 + description: A testing scalar; not meant to be touched. + expires: never + kind: uint + keyed: true + notification_emails: + - telemetry-client-dev@mozilla.com + release_channel_collection: opt-out + + keyed_expired: + bug_numbers: + - 1277806 + description: This is an expired testing scalar; not meant to be touched. + expires: 4.0a1 + kind: uint + keyed: true + notification_emails: + - telemetry-client-dev@mozilla.com + + keyed_unsigned_int: + bug_numbers: + - 1277806 + description: A testing keyed uint scalar; not meant to be touched. + expires: never + kind: uint + keyed: true + notification_emails: + - telemetry-client-dev@mozilla.com + + keyed_boolean_kind: + bug_numbers: + - 1277806 + description: A testing keyed boolean scalar; not meant to be touched. + expires: never + kind: boolean + keyed: true + notification_emails: + - telemetry-client-dev@mozilla.com diff --git a/toolkit/components/telemetry/Telemetry.cpp b/toolkit/components/telemetry/Telemetry.cpp new file mode 100644 index 000000000..ad2263c9b --- /dev/null +++ b/toolkit/components/telemetry/Telemetry.cpp @@ -0,0 +1,3076 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include + +#include + +#include + +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Likely.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Unused.h" + +#include "base/pickle.h" +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsThreadManager.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsXPCOMPrivate.h" +#include "nsIXULAppInfo.h" +#include "nsVersionComparator.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/ModuleUtils.h" +#include "nsIXPConnect.h" +#include "mozilla/Services.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/GCAPI.h" +#include "nsString.h" +#include "nsITelemetry.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIMemoryReporter.h" +#include "nsISeekableStream.h" +#include "Telemetry.h" +#include "TelemetryCommon.h" +#include "TelemetryHistogram.h" +#include "TelemetryScalar.h" +#include "TelemetryEvent.h" +#include "WebrtcTelemetry.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsBaseHashtable.h" +#include "nsClassHashtable.h" +#include "nsXULAppAPI.h" +#include "nsReadableUtils.h" +#include "nsThreadUtils.h" +#if defined(XP_WIN) +#include "nsUnicharUtils.h" +#endif +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsJSUtils.h" +#include "nsReadableUtils.h" +#include "plstr.h" +#include "nsAppDirectoryServiceDefs.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/ThreadHangStats.h" +#include "mozilla/ProcessedStack.h" +#include "mozilla/Mutex.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/PoisonIOInterposer.h" +#include "mozilla/StartupTimeline.h" +#include "mozilla/HangMonitor.h" +#if defined(MOZ_ENABLE_PROFILER_SPS) +#include "shared-libraries.h" +#endif + +namespace { + +using namespace mozilla; +using namespace mozilla::HangMonitor; +using Telemetry::Common::AutoHashtable; + +// The maximum number of chrome hangs stacks that we're keeping. +const size_t kMaxChromeStacksKept = 50; +// The maximum depth of a single chrome hang stack. +const size_t kMaxChromeStackDepth = 50; + +// This class is conceptually a list of ProcessedStack objects, but it represents them +// more efficiently by keeping a single global list of modules. +class CombinedStacks { +public: + CombinedStacks() : mNextIndex(0) {} + typedef std::vector Stack; + const Telemetry::ProcessedStack::Module& GetModule(unsigned aIndex) const; + size_t GetModuleCount() const; + const Stack& GetStack(unsigned aIndex) const; + size_t AddStack(const Telemetry::ProcessedStack& aStack); + size_t GetStackCount() const; + size_t SizeOfExcludingThis() const; +private: + std::vector mModules; + // A circular buffer to hold the stacks. + std::vector mStacks; + // The index of the next buffer element to write to in mStacks. + size_t mNextIndex; +}; + +static JSObject * +CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks); + +size_t +CombinedStacks::GetModuleCount() const { + return mModules.size(); +} + +const Telemetry::ProcessedStack::Module& +CombinedStacks::GetModule(unsigned aIndex) const { + return mModules[aIndex]; +} + +size_t +CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) { + // Advance the indices of the circular queue holding the stacks. + size_t index = mNextIndex++ % kMaxChromeStacksKept; + // Grow the vector up to the maximum size, if needed. + if (mStacks.size() < kMaxChromeStacksKept) { + mStacks.resize(mStacks.size() + 1); + } + // Get a reference to the location holding the new stack. + CombinedStacks::Stack& adjustedStack = mStacks[index]; + // If we're using an old stack to hold aStack, clear it. + adjustedStack.clear(); + + size_t stackSize = aStack.GetStackSize(); + for (size_t i = 0; i < stackSize; ++i) { + const Telemetry::ProcessedStack::Frame& frame = aStack.GetFrame(i); + uint16_t modIndex; + if (frame.mModIndex == std::numeric_limits::max()) { + modIndex = frame.mModIndex; + } else { + const Telemetry::ProcessedStack::Module& module = + aStack.GetModule(frame.mModIndex); + std::vector::iterator modIterator = + std::find(mModules.begin(), mModules.end(), module); + if (modIterator == mModules.end()) { + mModules.push_back(module); + modIndex = mModules.size() - 1; + } else { + modIndex = modIterator - mModules.begin(); + } + } + Telemetry::ProcessedStack::Frame adjustedFrame = { frame.mOffset, modIndex }; + adjustedStack.push_back(adjustedFrame); + } + return index; +} + +const CombinedStacks::Stack& +CombinedStacks::GetStack(unsigned aIndex) const { + return mStacks[aIndex]; +} + +size_t +CombinedStacks::GetStackCount() const { + return mStacks.size(); +} + +size_t +CombinedStacks::SizeOfExcludingThis() const { + // This is a crude approximation. We would like to do something like + // aMallocSizeOf(&mModules[0]), but on linux aMallocSizeOf will call + // malloc_usable_size which is only safe on the pointers returned by malloc. + // While it works on current libstdc++, it is better to be safe and not assume + // that &vec[0] points to one. We could use a custom allocator, but + // it doesn't seem worth it. + size_t n = 0; + n += mModules.capacity() * sizeof(Telemetry::ProcessedStack::Module); + n += mStacks.capacity() * sizeof(Stack); + for (std::vector::const_iterator i = mStacks.begin(), + e = mStacks.end(); i != e; ++i) { + const Stack& s = *i; + n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame); + } + return n; +} + +// This utility function generates a string key that is used to index the annotations +// in a hash map from |HangReports::AddHang|. +nsresult +ComputeAnnotationsKey(const HangAnnotationsPtr& aAnnotations, nsAString& aKeyOut) +{ + UniquePtr annotationsEnum = aAnnotations->GetEnumerator(); + if (!annotationsEnum) { + return NS_ERROR_FAILURE; + } + + // Append all the attributes to the key, to uniquely identify this annotation. + nsAutoString key; + nsAutoString value; + while (annotationsEnum->Next(key, value)) { + aKeyOut.Append(key); + aKeyOut.Append(value); + } + + return NS_OK; +} + +class HangReports { +public: + /** + * This struct encapsulates information for an individual ChromeHang annotation. + * mHangIndex is the index of the corresponding ChromeHang. + */ + struct AnnotationInfo { + AnnotationInfo(uint32_t aHangIndex, + HangAnnotationsPtr aAnnotations) + : mAnnotations(Move(aAnnotations)) + { + mHangIndices.AppendElement(aHangIndex); + } + AnnotationInfo(AnnotationInfo&& aOther) + : mHangIndices(aOther.mHangIndices) + , mAnnotations(Move(aOther.mAnnotations)) + {} + ~AnnotationInfo() {} + AnnotationInfo& operator=(AnnotationInfo&& aOther) + { + mHangIndices = aOther.mHangIndices; + mAnnotations = Move(aOther.mAnnotations); + return *this; + } + // To save memory, a single AnnotationInfo can be associated to multiple chrome + // hangs. The following array holds the index of each related chrome hang. + nsTArray mHangIndices; + HangAnnotationsPtr mAnnotations; + + private: + // Force move constructor + AnnotationInfo(const AnnotationInfo& aOther) = delete; + void operator=(const AnnotationInfo& aOther) = delete; + }; + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + void AddHang(const Telemetry::ProcessedStack& aStack, uint32_t aDuration, + int32_t aSystemUptime, int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations); + void PruneStackReferences(const size_t aRemovedStackIndex); + uint32_t GetDuration(unsigned aIndex) const; + int32_t GetSystemUptime(unsigned aIndex) const; + int32_t GetFirefoxUptime(unsigned aIndex) const; + const nsClassHashtable& GetAnnotationInfo() const; + const CombinedStacks& GetStacks() const; +private: + /** + * This struct encapsulates the data for an individual ChromeHang, excluding + * annotations. + */ + struct HangInfo { + // Hang duration (in seconds) + uint32_t mDuration; + // System uptime (in minutes) at the time of the hang + int32_t mSystemUptime; + // Firefox uptime (in minutes) at the time of the hang + int32_t mFirefoxUptime; + }; + std::vector mHangInfo; + nsClassHashtable mAnnotationInfo; + CombinedStacks mStacks; +}; + +void +HangReports::AddHang(const Telemetry::ProcessedStack& aStack, + uint32_t aDuration, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations) { + // Append the new stack to the stack's circular queue. + size_t hangIndex = mStacks.AddStack(aStack); + // Append the hang info at the same index, in mHangInfo. + HangInfo info = { aDuration, aSystemUptime, aFirefoxUptime }; + if (mHangInfo.size() < kMaxChromeStacksKept) { + mHangInfo.push_back(info); + } else { + mHangInfo[hangIndex] = info; + // Remove any reference to the stack overwritten in the circular queue + // from the annotations. + PruneStackReferences(hangIndex); + } + + if (!aAnnotations) { + return; + } + + nsAutoString annotationsKey; + // Generate a key to index aAnnotations in the hash map. + nsresult rv = ComputeAnnotationsKey(aAnnotations, annotationsKey); + if (NS_FAILED(rv)) { + return; + } + + AnnotationInfo* annotationsEntry = mAnnotationInfo.Get(annotationsKey); + if (annotationsEntry) { + // If the key is already in the hash map, append the index of the chrome hang + // to its indices. + annotationsEntry->mHangIndices.AppendElement(hangIndex); + return; + } + + // If the key was not found, add the annotations to the hash map. + mAnnotationInfo.Put(annotationsKey, new AnnotationInfo(hangIndex, Move(aAnnotations))); +} + +/** + * This function removes links to discarded chrome hangs stacks and prunes unused + * annotations. + */ +void +HangReports::PruneStackReferences(const size_t aRemovedStackIndex) { + // We need to adjust the indices that link annotations to chrome hangs. Since we + // removed a stack, we must remove all references to it and prune annotations + // linked to no stacks. + for (auto iter = mAnnotationInfo.Iter(); !iter.Done(); iter.Next()) { + nsTArray& stackIndices = iter.Data()->mHangIndices; + size_t toRemove = stackIndices.NoIndex; + for (size_t k = 0; k < stackIndices.Length(); k++) { + // Is this index referencing the removed stack? + if (stackIndices[k] == aRemovedStackIndex) { + toRemove = k; + break; + } + } + + // Remove the index referencing the old stack from the annotation. + if (toRemove != stackIndices.NoIndex) { + stackIndices.RemoveElementAt(toRemove); + } + + // If this annotation no longer references any stack, drop it. + if (!stackIndices.Length()) { + iter.Remove(); + } + } +} + +size_t +HangReports::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + n += mStacks.SizeOfExcludingThis(); + // This is a crude approximation. See comment on + // CombinedStacks::SizeOfExcludingThis. + n += mHangInfo.capacity() * sizeof(HangInfo); + n += mAnnotationInfo.ShallowSizeOfExcludingThis(aMallocSizeOf); + n += mAnnotationInfo.Count() * sizeof(AnnotationInfo); + for (auto iter = mAnnotationInfo.ConstIter(); !iter.Done(); iter.Next()) { + n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += iter.Data()->mAnnotations->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +const CombinedStacks& +HangReports::GetStacks() const { + return mStacks; +} + +uint32_t +HangReports::GetDuration(unsigned aIndex) const { + return mHangInfo[aIndex].mDuration; +} + +int32_t +HangReports::GetSystemUptime(unsigned aIndex) const { + return mHangInfo[aIndex].mSystemUptime; +} + +int32_t +HangReports::GetFirefoxUptime(unsigned aIndex) const { + return mHangInfo[aIndex].mFirefoxUptime; +} + +const nsClassHashtable& +HangReports::GetAnnotationInfo() const { + return mAnnotationInfo; +} + +/** + * IOInterposeObserver recording statistics of main-thread I/O during execution, + * aimed at consumption by TelemetryImpl + */ +class TelemetryIOInterposeObserver : public IOInterposeObserver +{ + /** File-level statistics structure */ + struct FileStats { + FileStats() + : creates(0) + , reads(0) + , writes(0) + , fsyncs(0) + , stats(0) + , totalTime(0) + {} + uint32_t creates; /** Number of create/open operations */ + uint32_t reads; /** Number of read operations */ + uint32_t writes; /** Number of write operations */ + uint32_t fsyncs; /** Number of fsync operations */ + uint32_t stats; /** Number of stat operations */ + double totalTime; /** Accumulated duration of all operations */ + }; + + struct SafeDir { + SafeDir(const nsAString& aPath, const nsAString& aSubstName) + : mPath(aPath) + , mSubstName(aSubstName) + {} + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return mPath.SizeOfExcludingThisIfUnshared(aMallocSizeOf) + + mSubstName.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + nsString mPath; /** Path to the directory */ + nsString mSubstName; /** Name to substitute with */ + }; + +public: + explicit TelemetryIOInterposeObserver(nsIFile* aXreDir); + + /** + * An implementation of Observe that records statistics of all + * file IO operations. + */ + void Observe(Observation& aOb); + + /** + * Reflect recorded file IO statistics into Javascript + */ + bool ReflectIntoJS(JSContext *cx, JS::Handle rootObj); + + /** + * Adds a path for inclusion in main thread I/O report. + * @param aPath Directory path + * @param aSubstName Name to substitute for aPath for privacy reasons + */ + void AddPath(const nsAString& aPath, const nsAString& aSubstName); + + /** + * Get size of hash table with file stats + */ + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t size = 0; + size += mFileStats.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto iter = mFileStats.ConstIter(); !iter.Done(); iter.Next()) { + size += iter.Get()->GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + size += mSafeDirs.ShallowSizeOfExcludingThis(aMallocSizeOf); + uint32_t safeDirsLen = mSafeDirs.Length(); + for (uint32_t i = 0; i < safeDirsLen; ++i) { + size += mSafeDirs[i].SizeOfExcludingThis(aMallocSizeOf); + } + return size; + } + +private: + enum Stage + { + STAGE_STARTUP = 0, + STAGE_NORMAL, + STAGE_SHUTDOWN, + NUM_STAGES + }; + static inline Stage NextStage(Stage aStage) + { + switch (aStage) { + case STAGE_STARTUP: + return STAGE_NORMAL; + case STAGE_NORMAL: + return STAGE_SHUTDOWN; + case STAGE_SHUTDOWN: + return STAGE_SHUTDOWN; + default: + return NUM_STAGES; + } + } + + struct FileStatsByStage + { + FileStats mStats[NUM_STAGES]; + }; + typedef nsBaseHashtableET FileIOEntryType; + + // Statistics for each filename + AutoHashtable mFileStats; + // Container for whitelisted directories + nsTArray mSafeDirs; + Stage mCurStage; + + /** + * Reflect a FileIOEntryType object to a Javascript property on obj with + * filename as key containing array: + * [totalTime, creates, reads, writes, fsyncs, stats] + */ + static bool ReflectFileStats(FileIOEntryType* entry, JSContext *cx, + JS::Handle obj); +}; + +TelemetryIOInterposeObserver::TelemetryIOInterposeObserver(nsIFile* aXreDir) + : mCurStage(STAGE_STARTUP) +{ + nsAutoString xreDirPath; + nsresult rv = aXreDir->GetPath(xreDirPath); + if (NS_SUCCEEDED(rv)) { + AddPath(xreDirPath, NS_LITERAL_STRING("{xre}")); + } +} + +void TelemetryIOInterposeObserver::AddPath(const nsAString& aPath, + const nsAString& aSubstName) +{ + mSafeDirs.AppendElement(SafeDir(aPath, aSubstName)); +} + +// Threshold for reporting slow main-thread I/O (50 milliseconds). +const TimeDuration kTelemetryReportThreshold = TimeDuration::FromMilliseconds(50); + +void TelemetryIOInterposeObserver::Observe(Observation& aOb) +{ + // We only report main-thread I/O + if (!IsMainThread()) { + return; + } + + if (aOb.ObservedOperation() == OpNextStage) { + mCurStage = NextStage(mCurStage); + MOZ_ASSERT(mCurStage < NUM_STAGES); + return; + } + + if (aOb.Duration() < kTelemetryReportThreshold) { + return; + } + + // Get the filename + const char16_t* filename = aOb.Filename(); + + // Discard observations without filename + if (!filename) { + return; + } + +#if defined(XP_WIN) + nsCaseInsensitiveStringComparator comparator; +#else + nsDefaultStringComparator comparator; +#endif + nsAutoString processedName; + nsDependentString filenameStr(filename); + uint32_t safeDirsLen = mSafeDirs.Length(); + for (uint32_t i = 0; i < safeDirsLen; ++i) { + if (StringBeginsWith(filenameStr, mSafeDirs[i].mPath, comparator)) { + processedName = mSafeDirs[i].mSubstName; + processedName += Substring(filenameStr, mSafeDirs[i].mPath.Length()); + break; + } + } + + if (processedName.IsEmpty()) { + return; + } + + // Create a new entry or retrieve the existing one + FileIOEntryType* entry = mFileStats.PutEntry(processedName); + if (entry) { + FileStats& stats = entry->mData.mStats[mCurStage]; + // Update the statistics + stats.totalTime += (double) aOb.Duration().ToMilliseconds(); + switch (aOb.ObservedOperation()) { + case OpCreateOrOpen: + stats.creates++; + break; + case OpRead: + stats.reads++; + break; + case OpWrite: + stats.writes++; + break; + case OpFSync: + stats.fsyncs++; + break; + case OpStat: + stats.stats++; + break; + default: + break; + } + } +} + +bool TelemetryIOInterposeObserver::ReflectFileStats(FileIOEntryType* entry, + JSContext *cx, + JS::Handle obj) +{ + JS::AutoValueArray stages(cx); + + FileStatsByStage& statsByStage = entry->mData; + for (int s = STAGE_STARTUP; s < NUM_STAGES; ++s) { + FileStats& fileStats = statsByStage.mStats[s]; + + if (fileStats.totalTime == 0 && fileStats.creates == 0 && + fileStats.reads == 0 && fileStats.writes == 0 && + fileStats.fsyncs == 0 && fileStats.stats == 0) { + // Don't add an array that contains no information + stages[s].setNull(); + continue; + } + + // Array we want to report + JS::AutoValueArray<6> stats(cx); + stats[0].setNumber(fileStats.totalTime); + stats[1].setNumber(fileStats.creates); + stats[2].setNumber(fileStats.reads); + stats[3].setNumber(fileStats.writes); + stats[4].setNumber(fileStats.fsyncs); + stats[5].setNumber(fileStats.stats); + + // Create jsStats as array of elements above + JS::RootedObject jsStats(cx, JS_NewArrayObject(cx, stats)); + if (!jsStats) { + continue; + } + + stages[s].setObject(*jsStats); + } + + JS::Rooted jsEntry(cx, JS_NewArrayObject(cx, stages)); + if (!jsEntry) { + return false; + } + + // Add jsEntry to top-level dictionary + const nsAString& key = entry->GetKey(); + return JS_DefineUCProperty(cx, obj, key.Data(), key.Length(), + jsEntry, JSPROP_ENUMERATE | JSPROP_READONLY); +} + +bool TelemetryIOInterposeObserver::ReflectIntoJS(JSContext *cx, + JS::Handle rootObj) +{ + return mFileStats.ReflectIntoJS(ReflectFileStats, cx, rootObj); +} + +// This is not a member of TelemetryImpl because we want to record I/O during +// startup. +StaticAutoPtr sTelemetryIOObserver; + +void +ClearIOReporting() +{ + if (!sTelemetryIOObserver) { + return; + } + IOInterposer::Unregister(IOInterposeObserver::OpAllWithStaging, + sTelemetryIOObserver); + sTelemetryIOObserver = nullptr; +} + +class TelemetryImpl final + : public nsITelemetry + , public nsIMemoryReporter +{ + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITELEMETRY + NS_DECL_NSIMEMORYREPORTER + +public: + void InitMemoryReporter(); + + static already_AddRefed CreateTelemetryInstance(); + static void ShutdownTelemetry(); + static void RecordSlowStatement(const nsACString &sql, const nsACString &dbName, + uint32_t delay); +#if defined(MOZ_ENABLE_PROFILER_SPS) + static void RecordChromeHang(uint32_t aDuration, + Telemetry::ProcessedStack &aStack, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations); +#endif + static void RecordThreadHangStats(Telemetry::ThreadHangStats& aStats); + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + struct Stat { + uint32_t hitCount; + uint32_t totalTime; + }; + struct StmtStats { + struct Stat mainThread; + struct Stat otherThreads; + }; + typedef nsBaseHashtableET SlowSQLEntryType; + + static void RecordIceCandidates(const uint32_t iceCandidateBitmask, + const bool success); +private: + TelemetryImpl(); + ~TelemetryImpl(); + + static nsCString SanitizeSQL(const nsACString& sql); + + enum SanitizedState { Sanitized, Unsanitized }; + + static void StoreSlowSQL(const nsACString &offender, uint32_t delay, + SanitizedState state); + + static bool ReflectMainThreadSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle obj); + static bool ReflectOtherThreadsSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle obj); + static bool ReflectSQL(const SlowSQLEntryType *entry, const Stat *stat, + JSContext *cx, JS::Handle obj); + + bool AddSQLInfo(JSContext *cx, JS::Handle rootObj, bool mainThread, + bool privateSQL); + bool GetSQLStats(JSContext *cx, JS::MutableHandle ret, + bool includePrivateSql); + + void ReadLateWritesStacks(nsIFile* aProfileDir); + + static TelemetryImpl *sTelemetry; + AutoHashtable mPrivateSQL; + AutoHashtable mSanitizedSQL; + Mutex mHashMutex; + HangReports mHangReports; + Mutex mHangReportsMutex; + // mThreadHangStats stores recorded, inactive thread hang stats + Vector mThreadHangStats; + Mutex mThreadHangStatsMutex; + + CombinedStacks mLateWritesStacks; // This is collected out of the main thread. + bool mCachedTelemetryData; + uint32_t mLastShutdownTime; + uint32_t mFailedLockCount; + nsCOMArray mCallbacks; + friend class nsFetchTelemetryData; + + WebrtcTelemetry mWebrtcTelemetry; +}; + +TelemetryImpl* TelemetryImpl::sTelemetry = nullptr; + +MOZ_DEFINE_MALLOC_SIZE_OF(TelemetryMallocSizeOf) + +NS_IMETHODIMP +TelemetryImpl::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) +{ + MOZ_COLLECT_REPORT( + "explicit/telemetry", KIND_HEAP, UNITS_BYTES, + SizeOfIncludingThis(TelemetryMallocSizeOf), + "Memory used by the telemetry system."); + + return NS_OK; +} + +void +InitHistogramRecordingEnabled() +{ + TelemetryHistogram::InitHistogramRecordingEnabled(); +} + +static uint32_t +ReadLastShutdownDuration(const char *filename) { + FILE *f = fopen(filename, "r"); + if (!f) { + return 0; + } + + int shutdownTime; + int r = fscanf(f, "%d\n", &shutdownTime); + fclose(f); + if (r != 1) { + return 0; + } + + return shutdownTime; +} + +const int32_t kMaxFailedProfileLockFileSize = 10; + +bool +GetFailedLockCount(nsIInputStream* inStream, uint32_t aCount, + unsigned int& result) +{ + nsAutoCString bufStr; + nsresult rv; + rv = NS_ReadInputStreamToString(inStream, bufStr, aCount); + NS_ENSURE_SUCCESS(rv, false); + result = bufStr.ToInteger(&rv); + return NS_SUCCEEDED(rv) && result > 0; +} + +nsresult +GetFailedProfileLockFile(nsIFile* *aFile, nsIFile* aProfileDir) +{ + NS_ENSURE_ARG_POINTER(aProfileDir); + + nsresult rv = aProfileDir->Clone(aFile); + NS_ENSURE_SUCCESS(rv, rv); + + (*aFile)->AppendNative(NS_LITERAL_CSTRING("Telemetry.FailedProfileLocks.txt")); + return NS_OK; +} + +class nsFetchTelemetryData : public Runnable +{ +public: + nsFetchTelemetryData(const char* aShutdownTimeFilename, + nsIFile* aFailedProfileLockFile, + nsIFile* aProfileDir) + : mShutdownTimeFilename(aShutdownTimeFilename), + mFailedProfileLockFile(aFailedProfileLockFile), + mTelemetry(TelemetryImpl::sTelemetry), + mProfileDir(aProfileDir) + { + } + +private: + const char* mShutdownTimeFilename; + nsCOMPtr mFailedProfileLockFile; + RefPtr mTelemetry; + nsCOMPtr mProfileDir; + +public: + void MainThread() { + mTelemetry->mCachedTelemetryData = true; + for (unsigned int i = 0, n = mTelemetry->mCallbacks.Count(); i < n; ++i) { + mTelemetry->mCallbacks[i]->Complete(); + } + mTelemetry->mCallbacks.Clear(); + } + + NS_IMETHOD Run() override { + LoadFailedLockCount(mTelemetry->mFailedLockCount); + mTelemetry->mLastShutdownTime = + ReadLastShutdownDuration(mShutdownTimeFilename); + mTelemetry->ReadLateWritesStacks(mProfileDir); + nsCOMPtr e = + NewRunnableMethod(this, &nsFetchTelemetryData::MainThread); + NS_ENSURE_STATE(e); + NS_DispatchToMainThread(e); + return NS_OK; + } + +private: + nsresult + LoadFailedLockCount(uint32_t& failedLockCount) + { + failedLockCount = 0; + int64_t fileSize = 0; + nsresult rv = mFailedProfileLockFile->GetFileSize(&fileSize); + if (NS_FAILED(rv)) { + return rv; + } + NS_ENSURE_TRUE(fileSize <= kMaxFailedProfileLockFileSize, + NS_ERROR_UNEXPECTED); + nsCOMPtr inStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream), + mFailedProfileLockFile, + PR_RDONLY); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(GetFailedLockCount(inStream, fileSize, failedLockCount), + NS_ERROR_UNEXPECTED); + inStream->Close(); + + mFailedProfileLockFile->Remove(false); + return NS_OK; + } +}; + +static TimeStamp gRecordedShutdownStartTime; +static bool gAlreadyFreedShutdownTimeFileName = false; +static char *gRecordedShutdownTimeFileName = nullptr; + +static char * +GetShutdownTimeFileName() +{ + if (gAlreadyFreedShutdownTimeFileName) { + return nullptr; + } + + if (!gRecordedShutdownTimeFileName) { + nsCOMPtr mozFile; + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mozFile)); + if (!mozFile) + return nullptr; + + mozFile->AppendNative(NS_LITERAL_CSTRING("Telemetry.ShutdownTime.txt")); + nsAutoCString nativePath; + nsresult rv = mozFile->GetNativePath(nativePath); + if (!NS_SUCCEEDED(rv)) + return nullptr; + + gRecordedShutdownTimeFileName = PL_strdup(nativePath.get()); + } + + return gRecordedShutdownTimeFileName; +} + +NS_IMETHODIMP +TelemetryImpl::GetLastShutdownDuration(uint32_t *aResult) +{ + // The user must call AsyncFetchTelemetryData first. We return zero instead of + // reporting a failure so that the rest of telemetry can uniformly handle + // the read not being available yet. + if (!mCachedTelemetryData) { + *aResult = 0; + return NS_OK; + } + + *aResult = mLastShutdownTime; + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::GetFailedProfileLockCount(uint32_t* aResult) +{ + // The user must call AsyncFetchTelemetryData first. We return zero instead of + // reporting a failure so that the rest of telemetry can uniformly handle + // the read not being available yet. + if (!mCachedTelemetryData) { + *aResult = 0; + return NS_OK; + } + + *aResult = mFailedLockCount; + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::AsyncFetchTelemetryData(nsIFetchTelemetryDataCallback *aCallback) +{ + // We have finished reading the data already, just call the callback. + if (mCachedTelemetryData) { + aCallback->Complete(); + return NS_OK; + } + + // We already have a read request running, just remember the callback. + if (mCallbacks.Count() != 0) { + mCallbacks.AppendObject(aCallback); + return NS_OK; + } + + // We make this check so that GetShutdownTimeFileName() doesn't get + // called; calling that function without telemetry enabled violates + // assumptions that the write-the-shutdown-timestamp machinery makes. + if (!Telemetry::CanRecordExtended()) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + // Send the read to a background thread provided by the stream transport + // service to avoid a read in the main thread. + nsCOMPtr targetThread = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + if (!targetThread) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + // We have to get the filename from the main thread. + const char *shutdownTimeFilename = GetShutdownTimeFileName(); + if (!shutdownTimeFilename) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + nsCOMPtr profileDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + if (NS_FAILED(rv)) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + nsCOMPtr failedProfileLockFile; + rv = GetFailedProfileLockFile(getter_AddRefs(failedProfileLockFile), + profileDir); + if (NS_FAILED(rv)) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + mCallbacks.AppendObject(aCallback); + + nsCOMPtr event = new nsFetchTelemetryData(shutdownTimeFilename, + failedProfileLockFile, + profileDir); + + targetThread->Dispatch(event, NS_DISPATCH_NORMAL); + return NS_OK; +} + +TelemetryImpl::TelemetryImpl() + : mHashMutex("Telemetry::mHashMutex") + , mHangReportsMutex("Telemetry::mHangReportsMutex") + , mThreadHangStatsMutex("Telemetry::mThreadHangStatsMutex") + , mCachedTelemetryData(false) + , mLastShutdownTime(0) + , mFailedLockCount(0) +{ + // We expect TelemetryHistogram::InitializeGlobalState() to have been + // called before we get to this point. + MOZ_ASSERT(TelemetryHistogram::GlobalStateHasBeenInitialized()); +} + +TelemetryImpl::~TelemetryImpl() { + UnregisterWeakMemoryReporter(this); +} + +void +TelemetryImpl::InitMemoryReporter() { + RegisterWeakMemoryReporter(this); +} + +bool +TelemetryImpl::ReflectSQL(const SlowSQLEntryType *entry, + const Stat *stat, + JSContext *cx, + JS::Handle obj) +{ + if (stat->hitCount == 0) + return true; + + const nsACString &sql = entry->GetKey(); + + JS::Rooted arrayObj(cx, JS_NewArrayObject(cx, 0)); + if (!arrayObj) { + return false; + } + return (JS_DefineElement(cx, arrayObj, 0, stat->hitCount, JSPROP_ENUMERATE) + && JS_DefineElement(cx, arrayObj, 1, stat->totalTime, JSPROP_ENUMERATE) + && JS_DefineProperty(cx, obj, sql.BeginReading(), arrayObj, + JSPROP_ENUMERATE)); +} + +bool +TelemetryImpl::ReflectMainThreadSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle obj) +{ + return ReflectSQL(entry, &entry->mData.mainThread, cx, obj); +} + +bool +TelemetryImpl::ReflectOtherThreadsSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle obj) +{ + return ReflectSQL(entry, &entry->mData.otherThreads, cx, obj); +} + +bool +TelemetryImpl::AddSQLInfo(JSContext *cx, JS::Handle rootObj, bool mainThread, + bool privateSQL) +{ + JS::Rooted statsObj(cx, JS_NewPlainObject(cx)); + if (!statsObj) + return false; + + AutoHashtable& sqlMap = (privateSQL ? mPrivateSQL : mSanitizedSQL); + AutoHashtable::ReflectEntryFunc reflectFunction = + (mainThread ? ReflectMainThreadSQL : ReflectOtherThreadsSQL); + if (!sqlMap.ReflectIntoJS(reflectFunction, cx, statsObj)) { + return false; + } + + return JS_DefineProperty(cx, rootObj, + mainThread ? "mainThread" : "otherThreads", + statsObj, JSPROP_ENUMERATE); +} + +NS_IMETHODIMP +TelemetryImpl::RegisterAddonHistogram(const nsACString &id, + const nsACString &name, + uint32_t histogramType, + uint32_t min, uint32_t max, + uint32_t bucketCount, + uint8_t optArgCount) +{ + return TelemetryHistogram::RegisterAddonHistogram + (id, name, histogramType, min, max, bucketCount, optArgCount); +} + +NS_IMETHODIMP +TelemetryImpl::GetAddonHistogram(const nsACString &id, const nsACString &name, + JSContext *cx, JS::MutableHandle ret) +{ + return TelemetryHistogram::GetAddonHistogram(id, name, cx, ret); +} + +NS_IMETHODIMP +TelemetryImpl::UnregisterAddonHistograms(const nsACString &id) +{ + return TelemetryHistogram::UnregisterAddonHistograms(id); +} + +NS_IMETHODIMP +TelemetryImpl::SetHistogramRecordingEnabled(const nsACString &id, bool aEnabled) +{ + return TelemetryHistogram::SetHistogramRecordingEnabled(id, aEnabled); +} + +NS_IMETHODIMP +TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle ret) +{ + return TelemetryHistogram::CreateHistogramSnapshots(cx, ret, false, false); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotSubsessionHistograms(bool clearSubsession, + JSContext *cx, + JS::MutableHandle ret) +{ +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + return TelemetryHistogram::CreateHistogramSnapshots(cx, ret, true, + clearSubsession); +#else + return NS_OK; +#endif +} + +NS_IMETHODIMP +TelemetryImpl::GetAddonHistogramSnapshots(JSContext *cx, JS::MutableHandle ret) +{ + return TelemetryHistogram::GetAddonHistogramSnapshots(cx, ret); +} + +NS_IMETHODIMP +TelemetryImpl::GetKeyedHistogramSnapshots(JSContext *cx, JS::MutableHandle ret) +{ + return TelemetryHistogram::GetKeyedHistogramSnapshots(cx, ret); +} + +bool +TelemetryImpl::GetSQLStats(JSContext *cx, JS::MutableHandle ret, bool includePrivateSql) +{ + JS::Rooted root_obj(cx, JS_NewPlainObject(cx)); + if (!root_obj) + return false; + ret.setObject(*root_obj); + + MutexAutoLock hashMutex(mHashMutex); + // Add info about slow SQL queries on the main thread + if (!AddSQLInfo(cx, root_obj, true, includePrivateSql)) + return false; + // Add info about slow SQL queries on other threads + if (!AddSQLInfo(cx, root_obj, false, includePrivateSql)) + return false; + + return true; +} + +NS_IMETHODIMP +TelemetryImpl::GetSlowSQL(JSContext *cx, JS::MutableHandle ret) +{ + if (GetSQLStats(cx, ret, false)) + return NS_OK; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TelemetryImpl::GetDebugSlowSQL(JSContext *cx, JS::MutableHandle ret) +{ + bool revealPrivateSql = + Preferences::GetBool("toolkit.telemetry.debugSlowSql", false); + if (GetSQLStats(cx, ret, revealPrivateSql)) + return NS_OK; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TelemetryImpl::GetWebrtcStats(JSContext *cx, JS::MutableHandle ret) +{ + if (mWebrtcTelemetry.GetWebrtcStats(cx, ret)) + return NS_OK; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TelemetryImpl::GetMaximalNumberOfConcurrentThreads(uint32_t *ret) +{ + *ret = nsThreadManager::get().GetHighestNumberOfThreads(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::GetChromeHangs(JSContext *cx, JS::MutableHandle ret) +{ + MutexAutoLock hangReportMutex(mHangReportsMutex); + + const CombinedStacks& stacks = mHangReports.GetStacks(); + JS::Rooted fullReportObj(cx, CreateJSStackObject(cx, stacks)); + if (!fullReportObj) { + return NS_ERROR_FAILURE; + } + + ret.setObject(*fullReportObj); + + JS::Rooted durationArray(cx, JS_NewArrayObject(cx, 0)); + JS::Rooted systemUptimeArray(cx, JS_NewArrayObject(cx, 0)); + JS::Rooted firefoxUptimeArray(cx, JS_NewArrayObject(cx, 0)); + JS::Rooted annotationsArray(cx, JS_NewArrayObject(cx, 0)); + if (!durationArray || !systemUptimeArray || !firefoxUptimeArray || + !annotationsArray) { + return NS_ERROR_FAILURE; + } + + bool ok = JS_DefineProperty(cx, fullReportObj, "durations", + durationArray, JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + ok = JS_DefineProperty(cx, fullReportObj, "systemUptime", + systemUptimeArray, JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + ok = JS_DefineProperty(cx, fullReportObj, "firefoxUptime", + firefoxUptimeArray, JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + ok = JS_DefineProperty(cx, fullReportObj, "annotations", annotationsArray, + JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + + const size_t length = stacks.GetStackCount(); + for (size_t i = 0; i < length; ++i) { + if (!JS_DefineElement(cx, durationArray, i, mHangReports.GetDuration(i), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineElement(cx, systemUptimeArray, i, mHangReports.GetSystemUptime(i), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineElement(cx, firefoxUptimeArray, i, mHangReports.GetFirefoxUptime(i), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + size_t annotationIndex = 0; + const nsClassHashtable& annotationInfo = + mHangReports.GetAnnotationInfo(); + + for (auto iter = annotationInfo.ConstIter(); !iter.Done(); iter.Next()) { + const HangReports::AnnotationInfo* info = iter.Data(); + + JS::Rooted keyValueArray(cx, JS_NewArrayObject(cx, 0)); + if (!keyValueArray) { + return NS_ERROR_FAILURE; + } + + // Create an array containing all the indices of the chrome hangs relative to this + // annotation. + JS::Rooted indicesArray(cx); + if (!mozilla::dom::ToJSValue(cx, info->mHangIndices, &indicesArray)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // We're saving the annotation as [[indices], {annotation-data}], so add the indices + // array as the first element of that structure. + if (!JS_DefineElement(cx, keyValueArray, 0, indicesArray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + // Create the annotations object... + JS::Rooted jsAnnotation(cx, JS_NewPlainObject(cx)); + if (!jsAnnotation) { + return NS_ERROR_FAILURE; + } + UniquePtr annotationsEnum = + info->mAnnotations->GetEnumerator(); + if (!annotationsEnum) { + return NS_ERROR_FAILURE; + } + + // ... fill it with key:value pairs... + nsAutoString key; + nsAutoString value; + while (annotationsEnum->Next(key, value)) { + JS::RootedValue jsValue(cx); + jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length())); + if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(), + jsValue, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + // ... and append it after the indices array. + if (!JS_DefineElement(cx, keyValueArray, 1, jsAnnotation, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineElement(cx, annotationsArray, annotationIndex++, + keyValueArray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + } + + return NS_OK; +} + +static JSObject * +CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks) { + JS::Rooted ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + + JS::Rooted moduleArray(cx, JS_NewArrayObject(cx, 0)); + if (!moduleArray) { + return nullptr; + } + bool ok = JS_DefineProperty(cx, ret, "memoryMap", moduleArray, + JSPROP_ENUMERATE); + if (!ok) { + return nullptr; + } + + const size_t moduleCount = stacks.GetModuleCount(); + for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) { + // Current module + const Telemetry::ProcessedStack::Module& module = + stacks.GetModule(moduleIndex); + + JS::Rooted moduleInfoArray(cx, JS_NewArrayObject(cx, 0)); + if (!moduleInfoArray) { + return nullptr; + } + if (!JS_DefineElement(cx, moduleArray, moduleIndex, moduleInfoArray, + JSPROP_ENUMERATE)) { + return nullptr; + } + + unsigned index = 0; + + // Module name + JS::Rooted str(cx, JS_NewStringCopyZ(cx, module.mName.c_str())); + if (!str) { + return nullptr; + } + if (!JS_DefineElement(cx, moduleInfoArray, index++, str, JSPROP_ENUMERATE)) { + return nullptr; + } + + // Module breakpad identifier + JS::Rooted id(cx, JS_NewStringCopyZ(cx, module.mBreakpadId.c_str())); + if (!id) { + return nullptr; + } + if (!JS_DefineElement(cx, moduleInfoArray, index++, id, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + JS::Rooted reportArray(cx, JS_NewArrayObject(cx, 0)); + if (!reportArray) { + return nullptr; + } + ok = JS_DefineProperty(cx, ret, "stacks", reportArray, JSPROP_ENUMERATE); + if (!ok) { + return nullptr; + } + + const size_t length = stacks.GetStackCount(); + for (size_t i = 0; i < length; ++i) { + // Represent call stack PCs as (module index, offset) pairs. + JS::Rooted pcArray(cx, JS_NewArrayObject(cx, 0)); + if (!pcArray) { + return nullptr; + } + + if (!JS_DefineElement(cx, reportArray, i, pcArray, JSPROP_ENUMERATE)) { + return nullptr; + } + + const CombinedStacks::Stack& stack = stacks.GetStack(i); + const uint32_t pcCount = stack.size(); + for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) { + const Telemetry::ProcessedStack::Frame& frame = stack[pcIndex]; + JS::Rooted framePair(cx, JS_NewArrayObject(cx, 0)); + if (!framePair) { + return nullptr; + } + int modIndex = (std::numeric_limits::max() == frame.mModIndex) ? + -1 : frame.mModIndex; + if (!JS_DefineElement(cx, framePair, 0, modIndex, JSPROP_ENUMERATE)) { + return nullptr; + } + if (!JS_DefineElement(cx, framePair, 1, static_cast(frame.mOffset), + JSPROP_ENUMERATE)) { + return nullptr; + } + if (!JS_DefineElement(cx, pcArray, pcIndex, framePair, JSPROP_ENUMERATE)) { + return nullptr; + } + } + } + + return ret; +} + +static bool +IsValidBreakpadId(const std::string &breakpadId) { + if (breakpadId.size() < 33) { + return false; + } + for (unsigned i = 0, n = breakpadId.size(); i < n; ++i) { + char c = breakpadId[i]; + if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) { + return false; + } + } + return true; +} + +// Read a stack from the given file name. In case of any error, aStack is +// unchanged. +static void +ReadStack(const char *aFileName, Telemetry::ProcessedStack &aStack) +{ + std::ifstream file(aFileName); + + size_t numModules; + file >> numModules; + if (file.fail()) { + return; + } + + char newline = file.get(); + if (file.fail() || newline != '\n') { + return; + } + + Telemetry::ProcessedStack stack; + for (size_t i = 0; i < numModules; ++i) { + std::string breakpadId; + file >> breakpadId; + if (file.fail() || !IsValidBreakpadId(breakpadId)) { + return; + } + + char space = file.get(); + if (file.fail() || space != ' ') { + return; + } + + std::string moduleName; + getline(file, moduleName); + if (file.fail() || moduleName[0] == ' ') { + return; + } + + Telemetry::ProcessedStack::Module module = { + moduleName, + breakpadId + }; + stack.AddModule(module); + } + + size_t numFrames; + file >> numFrames; + if (file.fail()) { + return; + } + + newline = file.get(); + if (file.fail() || newline != '\n') { + return; + } + + for (size_t i = 0; i < numFrames; ++i) { + uint16_t index; + file >> index; + uintptr_t offset; + file >> std::hex >> offset >> std::dec; + if (file.fail()) { + return; + } + + Telemetry::ProcessedStack::Frame frame = { + offset, + index + }; + stack.AddFrame(frame); + } + + aStack = stack; +} + +static JSObject* +CreateJSTimeHistogram(JSContext* cx, const Telemetry::TimeHistogram& time) +{ + /* Create JS representation of TimeHistogram, + in the format of Chromium-style histograms. */ + JS::RootedObject ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + + if (!JS_DefineProperty(cx, ret, "min", time.GetBucketMin(0), + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "max", + time.GetBucketMax(ArrayLength(time) - 1), + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "histogram_type", + nsITelemetry::HISTOGRAM_EXPONENTIAL, + JSPROP_ENUMERATE)) { + return nullptr; + } + // TODO: calculate "sum" + if (!JS_DefineProperty(cx, ret, "sum", 0, JSPROP_ENUMERATE)) { + return nullptr; + } + + JS::RootedObject ranges( + cx, JS_NewArrayObject(cx, ArrayLength(time) + 1)); + JS::RootedObject counts( + cx, JS_NewArrayObject(cx, ArrayLength(time) + 1)); + if (!ranges || !counts) { + return nullptr; + } + /* In a Chromium-style histogram, the first bucket is an "under" bucket + that represents all values below the histogram's range. */ + if (!JS_DefineElement(cx, ranges, 0, time.GetBucketMin(0), JSPROP_ENUMERATE) || + !JS_DefineElement(cx, counts, 0, 0, JSPROP_ENUMERATE)) { + return nullptr; + } + for (size_t i = 0; i < ArrayLength(time); i++) { + if (!JS_DefineElement(cx, ranges, i + 1, time.GetBucketMax(i), + JSPROP_ENUMERATE) || + !JS_DefineElement(cx, counts, i + 1, time[i], JSPROP_ENUMERATE)) { + return nullptr; + } + } + if (!JS_DefineProperty(cx, ret, "ranges", ranges, JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "counts", counts, JSPROP_ENUMERATE)) { + return nullptr; + } + return ret; +} + +static JSObject* +CreateJSHangStack(JSContext* cx, const Telemetry::HangStack& stack) +{ + JS::RootedObject ret(cx, JS_NewArrayObject(cx, stack.length())); + if (!ret) { + return nullptr; + } + for (size_t i = 0; i < stack.length(); i++) { + JS::RootedString string(cx, JS_NewStringCopyZ(cx, stack[i])); + if (!JS_DefineElement(cx, ret, i, string, JSPROP_ENUMERATE)) { + return nullptr; + } + } + return ret; +} + +static void +CreateJSHangAnnotations(JSContext* cx, const HangAnnotationsVector& annotations, + JS::MutableHandleObject returnedObject) +{ + JS::RootedObject annotationsArray(cx, JS_NewArrayObject(cx, 0)); + if (!annotationsArray) { + returnedObject.set(nullptr); + return; + } + // We keep track of the annotations we reported in this hash set, so we can + // discard duplicated ones. + nsTHashtable reportedAnnotations; + size_t annotationIndex = 0; + for (const HangAnnotationsPtr *i = annotations.begin(), *e = annotations.end(); + i != e; ++i) { + JS::RootedObject jsAnnotation(cx, JS_NewPlainObject(cx)); + if (!jsAnnotation) { + continue; + } + const HangAnnotationsPtr& curAnnotations = *i; + // Build a key to index the current annotations in our hash set. + nsAutoString annotationsKey; + nsresult rv = ComputeAnnotationsKey(curAnnotations, annotationsKey); + if (NS_FAILED(rv)) { + continue; + } + // Check if the annotations are in the set. If that's the case, don't double report. + if (reportedAnnotations.GetEntry(annotationsKey)) { + continue; + } + // If not, report them. + reportedAnnotations.PutEntry(annotationsKey); + UniquePtr annotationsEnum = + curAnnotations->GetEnumerator(); + if (!annotationsEnum) { + continue; + } + nsAutoString key; + nsAutoString value; + while (annotationsEnum->Next(key, value)) { + JS::RootedValue jsValue(cx); + jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length())); + if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(), + jsValue, JSPROP_ENUMERATE)) { + returnedObject.set(nullptr); + return; + } + } + if (!JS_SetElement(cx, annotationsArray, annotationIndex, jsAnnotation)) { + continue; + } + ++annotationIndex; + } + // Return the array using a |MutableHandleObject| to avoid triggering a false + // positive rooting issue in the hazard analysis build. + returnedObject.set(annotationsArray); +} + +static JSObject* +CreateJSHangHistogram(JSContext* cx, const Telemetry::HangHistogram& hang) +{ + JS::RootedObject ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + + JS::RootedObject stack(cx, CreateJSHangStack(cx, hang.GetStack())); + JS::RootedObject time(cx, CreateJSTimeHistogram(cx, hang)); + auto& hangAnnotations = hang.GetAnnotations(); + JS::RootedObject annotations(cx); + CreateJSHangAnnotations(cx, hangAnnotations, &annotations); + + if (!stack || + !time || + !annotations || + !JS_DefineProperty(cx, ret, "stack", stack, JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "histogram", time, JSPROP_ENUMERATE) || + (!hangAnnotations.empty() && // <-- Only define annotations when nonempty + !JS_DefineProperty(cx, ret, "annotations", annotations, JSPROP_ENUMERATE))) { + return nullptr; + } + + if (!hang.GetNativeStack().empty()) { + JS::RootedObject native(cx, CreateJSHangStack(cx, hang.GetNativeStack())); + if (!native || + !JS_DefineProperty(cx, ret, "nativeStack", native, JSPROP_ENUMERATE)) { + return nullptr; + } + } + return ret; +} + +static JSObject* +CreateJSThreadHangStats(JSContext* cx, const Telemetry::ThreadHangStats& thread) +{ + JS::RootedObject ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + JS::RootedString name(cx, JS_NewStringCopyZ(cx, thread.GetName())); + if (!name || + !JS_DefineProperty(cx, ret, "name", name, JSPROP_ENUMERATE)) { + return nullptr; + } + + JS::RootedObject activity(cx, CreateJSTimeHistogram(cx, thread.mActivity)); + if (!activity || + !JS_DefineProperty(cx, ret, "activity", activity, JSPROP_ENUMERATE)) { + return nullptr; + } + + JS::RootedObject hangs(cx, JS_NewArrayObject(cx, 0)); + if (!hangs) { + return nullptr; + } + for (size_t i = 0; i < thread.mHangs.length(); i++) { + JS::RootedObject obj(cx, CreateJSHangHistogram(cx, thread.mHangs[i])); + if (!JS_DefineElement(cx, hangs, i, obj, JSPROP_ENUMERATE)) { + return nullptr; + } + } + if (!JS_DefineProperty(cx, ret, "hangs", hangs, JSPROP_ENUMERATE)) { + return nullptr; + } + + return ret; +} + +NS_IMETHODIMP +TelemetryImpl::GetThreadHangStats(JSContext* cx, JS::MutableHandle ret) +{ + JS::RootedObject retObj(cx, JS_NewArrayObject(cx, 0)); + if (!retObj) { + return NS_ERROR_FAILURE; + } + size_t threadIndex = 0; + + if (!BackgroundHangMonitor::IsDisabled()) { + /* First add active threads; we need to hold |iter| (and its lock) + throughout this method to avoid a race condition where a thread can + be recorded twice if the thread is destroyed while this method is + running */ + BackgroundHangMonitor::ThreadHangStatsIterator iter; + for (Telemetry::ThreadHangStats* histogram = iter.GetNext(); + histogram; histogram = iter.GetNext()) { + JS::RootedObject obj(cx, CreateJSThreadHangStats(cx, *histogram)); + if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + } + + // Add saved threads next + MutexAutoLock autoLock(mThreadHangStatsMutex); + for (size_t i = 0; i < mThreadHangStats.length(); i++) { + JS::RootedObject obj(cx, + CreateJSThreadHangStats(cx, mThreadHangStats[i])); + if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + ret.setObject(*retObj); + return NS_OK; +} + +void +TelemetryImpl::ReadLateWritesStacks(nsIFile* aProfileDir) +{ + nsAutoCString nativePath; + nsresult rv = aProfileDir->GetNativePath(nativePath); + if (NS_FAILED(rv)) { + return; + } + + const char *name = nativePath.get(); + PRDir *dir = PR_OpenDir(name); + if (!dir) { + return; + } + + PRDirEntry *ent; + const char *prefix = "Telemetry.LateWriteFinal-"; + unsigned int prefixLen = strlen(prefix); + while ((ent = PR_ReadDir(dir, PR_SKIP_NONE))) { + if (strncmp(prefix, ent->name, prefixLen) != 0) { + continue; + } + + nsAutoCString stackNativePath = nativePath; + stackNativePath += XPCOM_FILE_PATH_SEPARATOR; + stackNativePath += nsDependentCString(ent->name); + + Telemetry::ProcessedStack stack; + ReadStack(stackNativePath.get(), stack); + if (stack.GetStackSize() != 0) { + mLateWritesStacks.AddStack(stack); + } + // Delete the file so that we don't report it again on the next run. + PR_Delete(stackNativePath.get()); + } + PR_CloseDir(dir); +} + +NS_IMETHODIMP +TelemetryImpl::GetLateWrites(JSContext *cx, JS::MutableHandle ret) +{ + // The user must call AsyncReadTelemetryData first. We return an empty list + // instead of reporting a failure so that the rest of telemetry can uniformly + // handle the read not being available yet. + + // FIXME: we allocate the js object again and again in the getter. We should + // figure out a way to cache it. In order to do that we have to call + // JS_AddNamedObjectRoot. A natural place to do so is in the TelemetryImpl + // constructor, but it is not clear how to get a JSContext in there. + // Another option would be to call it in here when we first call + // CreateJSStackObject, but we would still need to figure out where to call + // JS_RemoveObjectRoot. Would it be ok to never call JS_RemoveObjectRoot + // and just set the pointer to nullptr is the telemetry destructor? + + JSObject *report; + if (!mCachedTelemetryData) { + CombinedStacks empty; + report = CreateJSStackObject(cx, empty); + } else { + report = CreateJSStackObject(cx, mLateWritesStacks); + } + + if (report == nullptr) { + return NS_ERROR_FAILURE; + } + + ret.setObject(*report); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::RegisteredHistograms(uint32_t aDataset, uint32_t *aCount, + char*** aHistograms) +{ + return + TelemetryHistogram::RegisteredHistograms(aDataset, aCount, aHistograms); +} + +NS_IMETHODIMP +TelemetryImpl::RegisteredKeyedHistograms(uint32_t aDataset, uint32_t *aCount, + char*** aHistograms) +{ + return + TelemetryHistogram::RegisteredKeyedHistograms(aDataset, aCount, + aHistograms); +} + +NS_IMETHODIMP +TelemetryImpl::GetHistogramById(const nsACString &name, JSContext *cx, + JS::MutableHandle ret) +{ + return TelemetryHistogram::GetHistogramById(name, cx, ret); +} + +NS_IMETHODIMP +TelemetryImpl::GetKeyedHistogramById(const nsACString &name, JSContext *cx, + JS::MutableHandle ret) +{ + return TelemetryHistogram::GetKeyedHistogramById(name, cx, ret); +} + +/** + * Indicates if Telemetry can record base data (FHR data). This is true if the + * FHR data reporting service or self-support are enabled. + * + * In the unlikely event that adding a new base probe is needed, please check the data + * collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection and talk to the + * Telemetry team. + */ +NS_IMETHODIMP +TelemetryImpl::GetCanRecordBase(bool *ret) { + *ret = TelemetryHistogram::CanRecordBase(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::SetCanRecordBase(bool canRecord) { + TelemetryHistogram::SetCanRecordBase(canRecord); + TelemetryScalar::SetCanRecordBase(canRecord); + TelemetryEvent::SetCanRecordBase(canRecord); + return NS_OK; +} + +/** + * Indicates if Telemetry is allowed to record extended data. Returns false if the user + * hasn't opted into "extended Telemetry" on the Release channel, when the user has + * explicitly opted out of Telemetry on Nightly/Aurora/Beta or if manually set to false + * during tests. + * If the returned value is false, gathering of extended telemetry statistics is disabled. + */ +NS_IMETHODIMP +TelemetryImpl::GetCanRecordExtended(bool *ret) { + *ret = TelemetryHistogram::CanRecordExtended(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::SetCanRecordExtended(bool canRecord) { + TelemetryHistogram::SetCanRecordExtended(canRecord); + TelemetryScalar::SetCanRecordExtended(canRecord); + TelemetryEvent::SetCanRecordExtended(canRecord); + return NS_OK; +} + + +NS_IMETHODIMP +TelemetryImpl::GetIsOfficialTelemetry(bool *ret) { +#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && !defined(DEBUG) + *ret = true; +#else + *ret = false; +#endif + return NS_OK; +} + +already_AddRefed +TelemetryImpl::CreateTelemetryInstance() +{ + MOZ_ASSERT(sTelemetry == nullptr, "CreateTelemetryInstance may only be called once, via GetService()"); + + bool useTelemetry = false; + if (XRE_IsParentProcess() || + XRE_IsContentProcess() || + XRE_IsGPUProcess()) + { + useTelemetry = true; + } + + // First, initialize the TelemetryHistogram and TelemetryScalar global states. + TelemetryHistogram::InitializeGlobalState(useTelemetry, useTelemetry); + + // Only record scalars from the parent process. + TelemetryScalar::InitializeGlobalState(XRE_IsParentProcess(), XRE_IsParentProcess()); + + // Only record events from the parent process. + TelemetryEvent::InitializeGlobalState(XRE_IsParentProcess(), XRE_IsParentProcess()); + + // Now, create and initialize the Telemetry global state. + sTelemetry = new TelemetryImpl(); + + // AddRef for the local reference + NS_ADDREF(sTelemetry); + // AddRef for the caller + nsCOMPtr ret = sTelemetry; + + sTelemetry->InitMemoryReporter(); + InitHistogramRecordingEnabled(); // requires sTelemetry to exist + + return ret.forget(); +} + +void +TelemetryImpl::ShutdownTelemetry() +{ + // No point in collecting IO beyond this point + ClearIOReporting(); + NS_IF_RELEASE(sTelemetry); + + // Lastly, de-initialise the TelemetryHistogram and TelemetryScalar global states, + // so as to release any heap storage that would otherwise be kept alive by it. + TelemetryHistogram::DeInitializeGlobalState(); + TelemetryScalar::DeInitializeGlobalState(); + TelemetryEvent::DeInitializeGlobalState(); +} + +void +TelemetryImpl::StoreSlowSQL(const nsACString &sql, uint32_t delay, + SanitizedState state) +{ + AutoHashtable* slowSQLMap = nullptr; + if (state == Sanitized) + slowSQLMap = &(sTelemetry->mSanitizedSQL); + else + slowSQLMap = &(sTelemetry->mPrivateSQL); + + MutexAutoLock hashMutex(sTelemetry->mHashMutex); + + SlowSQLEntryType *entry = slowSQLMap->GetEntry(sql); + if (!entry) { + entry = slowSQLMap->PutEntry(sql); + if (MOZ_UNLIKELY(!entry)) + return; + entry->mData.mainThread.hitCount = 0; + entry->mData.mainThread.totalTime = 0; + entry->mData.otherThreads.hitCount = 0; + entry->mData.otherThreads.totalTime = 0; + } + + if (NS_IsMainThread()) { + entry->mData.mainThread.hitCount++; + entry->mData.mainThread.totalTime += delay; + } else { + entry->mData.otherThreads.hitCount++; + entry->mData.otherThreads.totalTime += delay; + } +} + +/** + * This method replaces string literals in SQL strings with the word :private + * + * States used in this state machine: + * + * NORMAL: + * - This is the active state when not iterating over a string literal or + * comment + * + * SINGLE_QUOTE: + * - Defined here: http://www.sqlite.org/lang_expr.html + * - This state represents iterating over a string literal opened with + * a single quote. + * - A single quote within the string can be encoded by putting 2 single quotes + * in a row, e.g. 'This literal contains an escaped quote ''' + * - Any double quotes found within a single-quoted literal are ignored + * - This state covers BLOB literals, e.g. X'ABC123' + * - The string literal and the enclosing quotes will be replaced with + * the text :private + * + * DOUBLE_QUOTE: + * - Same rules as the SINGLE_QUOTE state. + * - According to http://www.sqlite.org/lang_keywords.html, + * SQLite interprets text in double quotes as an identifier unless it's used in + * a context where it cannot be resolved to an identifier and a string literal + * is allowed. This method removes text in double-quotes for safety. + * + * DASH_COMMENT: + * - http://www.sqlite.org/lang_comment.html + * - A dash comment starts with two dashes in a row, + * e.g. DROP TABLE foo -- a comment + * - Any text following two dashes in a row is interpreted as a comment until + * end of input or a newline character + * - Any quotes found within the comment are ignored and no replacements made + * + * C_STYLE_COMMENT: + * - http://www.sqlite.org/lang_comment.html + * - A C-style comment starts with a forward slash and an asterisk, and ends + * with an asterisk and a forward slash + * - Any text following comment start is interpreted as a comment up to end of + * input or comment end + * - Any quotes found within the comment are ignored and no replacements made + */ +nsCString +TelemetryImpl::SanitizeSQL(const nsACString &sql) { + nsCString output; + int length = sql.Length(); + + typedef enum { + NORMAL, + SINGLE_QUOTE, + DOUBLE_QUOTE, + DASH_COMMENT, + C_STYLE_COMMENT, + } State; + + State state = NORMAL; + int fragmentStart = 0; + for (int i = 0; i < length; i++) { + char character = sql[i]; + char nextCharacter = (i + 1 < length) ? sql[i + 1] : '\0'; + + switch (character) { + case '\'': + case '"': + if (state == NORMAL) { + state = (character == '\'') ? SINGLE_QUOTE : DOUBLE_QUOTE; + output += nsDependentCSubstring(sql, fragmentStart, i - fragmentStart); + output += ":private"; + fragmentStart = -1; + } else if ((state == SINGLE_QUOTE && character == '\'') || + (state == DOUBLE_QUOTE && character == '"')) { + if (nextCharacter == character) { + // Two consecutive quotes within a string literal are a single escaped quote + i++; + } else { + state = NORMAL; + fragmentStart = i + 1; + } + } + break; + case '-': + if (state == NORMAL) { + if (nextCharacter == '-') { + state = DASH_COMMENT; + i++; + } + } + break; + case '\n': + if (state == DASH_COMMENT) { + state = NORMAL; + } + break; + case '/': + if (state == NORMAL) { + if (nextCharacter == '*') { + state = C_STYLE_COMMENT; + i++; + } + } + break; + case '*': + if (state == C_STYLE_COMMENT) { + if (nextCharacter == '/') { + state = NORMAL; + } + } + break; + default: + continue; + } + } + + if ((fragmentStart >= 0) && fragmentStart < length) + output += nsDependentCSubstring(sql, fragmentStart, length - fragmentStart); + + return output; +} + +// A whitelist mechanism to prevent Telemetry reporting on Addon & Thunderbird +// DBs. +struct TrackedDBEntry +{ + const char* mName; + const uint32_t mNameLength; + + // This struct isn't meant to be used beyond the static arrays below. + constexpr + TrackedDBEntry(const char* aName, uint32_t aNameLength) + : mName(aName) + , mNameLength(aNameLength) + { } + + TrackedDBEntry() = delete; + TrackedDBEntry(TrackedDBEntry&) = delete; +}; + +#define TRACKEDDB_ENTRY(_name) { _name, (sizeof(_name) - 1) } + +// A whitelist of database names. If the database name exactly matches one of +// these then its SQL statements will always be recorded. +static constexpr TrackedDBEntry kTrackedDBs[] = { + // IndexedDB for about:home, see aboutHome.js + TRACKEDDB_ENTRY("818200132aebmoouht.sqlite"), + TRACKEDDB_ENTRY("addons.sqlite"), + TRACKEDDB_ENTRY("content-prefs.sqlite"), + TRACKEDDB_ENTRY("cookies.sqlite"), + TRACKEDDB_ENTRY("downloads.sqlite"), + TRACKEDDB_ENTRY("extensions.sqlite"), + TRACKEDDB_ENTRY("formhistory.sqlite"), + TRACKEDDB_ENTRY("index.sqlite"), + TRACKEDDB_ENTRY("netpredictions.sqlite"), + TRACKEDDB_ENTRY("permissions.sqlite"), + TRACKEDDB_ENTRY("places.sqlite"), + TRACKEDDB_ENTRY("reading-list.sqlite"), + TRACKEDDB_ENTRY("search.sqlite"), + TRACKEDDB_ENTRY("signons.sqlite"), + TRACKEDDB_ENTRY("urlclassifier3.sqlite"), + TRACKEDDB_ENTRY("webappsstore.sqlite") +}; + +// A whitelist of database name prefixes. If the database name begins with +// one of these prefixes then its SQL statements will always be recorded. +static const TrackedDBEntry kTrackedDBPrefixes[] = { + TRACKEDDB_ENTRY("indexedDB-") +}; + +#undef TRACKEDDB_ENTRY + +// Slow SQL statements will be automatically +// trimmed to kMaxSlowStatementLength characters. +// This limit doesn't include the ellipsis and DB name, +// that are appended at the end of the stored statement. +const uint32_t kMaxSlowStatementLength = 1000; + +void +TelemetryImpl::RecordSlowStatement(const nsACString &sql, + const nsACString &dbName, + uint32_t delay) +{ + MOZ_ASSERT(!sql.IsEmpty()); + MOZ_ASSERT(!dbName.IsEmpty()); + + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + bool recordStatement = false; + + for (const TrackedDBEntry& nameEntry : kTrackedDBs) { + MOZ_ASSERT(nameEntry.mNameLength); + const nsDependentCString name(nameEntry.mName, nameEntry.mNameLength); + if (dbName == name) { + recordStatement = true; + break; + } + } + + if (!recordStatement) { + for (const TrackedDBEntry& prefixEntry : kTrackedDBPrefixes) { + MOZ_ASSERT(prefixEntry.mNameLength); + const nsDependentCString prefix(prefixEntry.mName, + prefixEntry.mNameLength); + if (StringBeginsWith(dbName, prefix)) { + recordStatement = true; + break; + } + } + } + + if (recordStatement) { + nsAutoCString sanitizedSQL(SanitizeSQL(sql)); + if (sanitizedSQL.Length() > kMaxSlowStatementLength) { + sanitizedSQL.SetLength(kMaxSlowStatementLength); + sanitizedSQL += "..."; + } + sanitizedSQL.AppendPrintf(" /* %s */", nsPromiseFlatCString(dbName).get()); + StoreSlowSQL(sanitizedSQL, delay, Sanitized); + } else { + // Report aggregate DB-level statistics for addon DBs + nsAutoCString aggregate; + aggregate.AppendPrintf("Untracked SQL for %s", + nsPromiseFlatCString(dbName).get()); + StoreSlowSQL(aggregate, delay, Sanitized); + } + + nsAutoCString fullSQL; + fullSQL.AppendPrintf("%s /* %s */", + nsPromiseFlatCString(sql).get(), + nsPromiseFlatCString(dbName).get()); + StoreSlowSQL(fullSQL, delay, Unsanitized); +} + +void +TelemetryImpl::RecordIceCandidates(const uint32_t iceCandidateBitmask, + const bool success) +{ + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + sTelemetry->mWebrtcTelemetry.RecordIceCandidateMask(iceCandidateBitmask, success); +} + +#if defined(MOZ_ENABLE_PROFILER_SPS) +void +TelemetryImpl::RecordChromeHang(uint32_t aDuration, + Telemetry::ProcessedStack &aStack, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations) +{ + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + HangAnnotationsPtr annotations; + // We only pass aAnnotations if it is not empty. + if (aAnnotations && !aAnnotations->IsEmpty()) { + annotations = Move(aAnnotations); + } + + MutexAutoLock hangReportMutex(sTelemetry->mHangReportsMutex); + + sTelemetry->mHangReports.AddHang(aStack, aDuration, + aSystemUptime, aFirefoxUptime, + Move(annotations)); +} +#endif + +void +TelemetryImpl::RecordThreadHangStats(Telemetry::ThreadHangStats& aStats) +{ + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + MutexAutoLock autoLock(sTelemetry->mThreadHangStatsMutex); + + // Ignore OOM. + mozilla::Unused << sTelemetry->mThreadHangStats.append(Move(aStats)); +} + +NS_IMPL_ISUPPORTS(TelemetryImpl, nsITelemetry, nsIMemoryReporter) +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsITelemetry, TelemetryImpl::CreateTelemetryInstance) + +#define NS_TELEMETRY_CID \ + {0xaea477f2, 0xb3a2, 0x469c, {0xaa, 0x29, 0x0a, 0x82, 0xd1, 0x32, 0xb8, 0x29}} +NS_DEFINE_NAMED_CID(NS_TELEMETRY_CID); + +const Module::CIDEntry kTelemetryCIDs[] = { + { &kNS_TELEMETRY_CID, false, nullptr, nsITelemetryConstructor, Module::ALLOW_IN_GPU_PROCESS }, + { nullptr } +}; + +const Module::ContractIDEntry kTelemetryContracts[] = { + { "@mozilla.org/base/telemetry;1", &kNS_TELEMETRY_CID, Module::ALLOW_IN_GPU_PROCESS }, + { nullptr } +}; + +const Module kTelemetryModule = { + Module::kVersion, + kTelemetryCIDs, + kTelemetryContracts, + nullptr, + nullptr, + nullptr, + TelemetryImpl::ShutdownTelemetry, + Module::ALLOW_IN_GPU_PROCESS +}; + +NS_IMETHODIMP +TelemetryImpl::GetFileIOReports(JSContext *cx, JS::MutableHandleValue ret) +{ + if (sTelemetryIOObserver) { + JS::Rooted obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return NS_ERROR_FAILURE; + } + + if (!sTelemetryIOObserver->ReflectIntoJS(cx, obj)) { + return NS_ERROR_FAILURE; + } + ret.setObject(*obj); + return NS_OK; + } + ret.setNull(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::MsSinceProcessStart(double* aResult) +{ + return Telemetry::Common::MsSinceProcessStart(aResult); +} + +// Telemetry Scalars IDL Implementation + +NS_IMETHODIMP +TelemetryImpl::ScalarAdd(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Add(aName, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::ScalarSet(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Set(aName, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::ScalarSetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::SetMaximum(aName, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + return TelemetryScalar::CreateSnapshots(aDataset, aClearScalars, aCx, optional_argc, aResult); +} + +NS_IMETHODIMP +TelemetryImpl::KeyedScalarAdd(const nsACString& aName, const nsAString& aKey, + JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Add(aName, aKey, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::KeyedScalarSet(const nsACString& aName, const nsAString& aKey, + JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Set(aName, aKey, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::KeyedScalarSetMaximum(const nsACString& aName, const nsAString& aKey, + JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::SetMaximum(aName, aKey, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotKeyedScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + return TelemetryScalar::CreateKeyedSnapshots(aDataset, aClearScalars, aCx, optional_argc, + aResult); +} + +NS_IMETHODIMP +TelemetryImpl::ClearScalars() +{ + TelemetryScalar::ClearScalars(); + return NS_OK; +} + +// Telemetry Event IDL implementation. + +NS_IMETHODIMP +TelemetryImpl::RecordEvent(const nsACString & aCategory, const nsACString & aMethod, + const nsACString & aObject, JS::HandleValue aValue, + JS::HandleValue aExtra, JSContext* aCx, uint8_t optional_argc) +{ + return TelemetryEvent::RecordEvent(aCategory, aMethod, aObject, aValue, aExtra, aCx, optional_argc); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotBuiltinEvents(uint32_t aDataset, bool aClear, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + return TelemetryEvent::CreateSnapshots(aDataset, aClear, aCx, optional_argc, aResult); +} + +NS_IMETHODIMP +TelemetryImpl::ClearEvents() +{ + TelemetryEvent::ClearEvents(); + return NS_OK; +} + + +NS_IMETHODIMP +TelemetryImpl::FlushBatchedChildTelemetry() +{ + TelemetryHistogram::IPCTimerFired(nullptr, nullptr); + return NS_OK; +} + +size_t +TelemetryImpl::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) +{ + size_t n = aMallocSizeOf(this); + + // Ignore the hashtables in mAddonMap; they are not significant. + n += TelemetryHistogram::GetMapShallowSizesOfExcludingThis(aMallocSizeOf); + n += TelemetryScalar::GetMapShallowSizesOfExcludingThis(aMallocSizeOf); + n += mWebrtcTelemetry.SizeOfExcludingThis(aMallocSizeOf); + { // Scope for mHashMutex lock + MutexAutoLock lock(mHashMutex); + n += mPrivateSQL.SizeOfExcludingThis(aMallocSizeOf); + n += mSanitizedSQL.SizeOfExcludingThis(aMallocSizeOf); + } + { // Scope for mHangReportsMutex lock + MutexAutoLock lock(mHangReportsMutex); + n += mHangReports.SizeOfExcludingThis(aMallocSizeOf); + } + { // Scope for mThreadHangStatsMutex lock + MutexAutoLock lock(mThreadHangStatsMutex); + n += mThreadHangStats.sizeOfExcludingThis(aMallocSizeOf); + } + + // It's a bit gross that we measure this other stuff that lives outside of + // TelemetryImpl... oh well. + if (sTelemetryIOObserver) { + n += sTelemetryIOObserver->SizeOfIncludingThis(aMallocSizeOf); + } + + n += TelemetryHistogram::GetHistogramSizesofIncludingThis(aMallocSizeOf); + n += TelemetryScalar::GetScalarSizesOfIncludingThis(aMallocSizeOf); + n += TelemetryEvent::SizeOfIncludingThis(aMallocSizeOf); + + return n; +} + +struct StackFrame +{ + uintptr_t mPC; // The program counter at this position in the call stack. + uint16_t mIndex; // The number of this frame in the call stack. + uint16_t mModIndex; // The index of module that has this program counter. +}; + +#ifdef MOZ_ENABLE_PROFILER_SPS +static bool CompareByPC(const StackFrame &a, const StackFrame &b) +{ + return a.mPC < b.mPC; +} + +static bool CompareByIndex(const StackFrame &a, const StackFrame &b) +{ + return a.mIndex < b.mIndex; +} +#endif + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in no name space +// These are NOT listed in Telemetry.h + +NSMODULE_DEFN(nsTelemetryModule) = &kTelemetryModule; + +/** + * The XRE_TelemetryAdd function is to be used by embedding applications + * that can't use mozilla::Telemetry::Accumulate() directly. + */ +void +XRE_TelemetryAccumulate(int aID, uint32_t aSample) +{ + mozilla::Telemetry::Accumulate((mozilla::Telemetry::ID) aID, aSample); +} + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in mozilla:: +// These are NOT listed in Telemetry.h + +namespace mozilla { + +void +RecordShutdownStartTimeStamp() { +#ifdef DEBUG + // FIXME: this function should only be called once, since it should be called + // at the earliest point we *know* we are shutting down. Unfortunately + // this assert has been firing. Given that if we are called multiple times + // we just keep the last timestamp, the assert is commented for now. + static bool recorded = false; + // MOZ_ASSERT(!recorded); + (void)recorded; // Silence unused-var warnings (remove when assert re-enabled) + recorded = true; +#endif + + if (!Telemetry::CanRecordExtended()) + return; + + gRecordedShutdownStartTime = TimeStamp::Now(); + + GetShutdownTimeFileName(); +} + +void +RecordShutdownEndTimeStamp() { + if (!gRecordedShutdownTimeFileName || gAlreadyFreedShutdownTimeFileName) + return; + + nsCString name(gRecordedShutdownTimeFileName); + PL_strfree(gRecordedShutdownTimeFileName); + gRecordedShutdownTimeFileName = nullptr; + gAlreadyFreedShutdownTimeFileName = true; + + if (gRecordedShutdownStartTime.IsNull()) { + // If |CanRecordExtended()| is true before |AsyncFetchTelemetryData| is called and + // then disabled before shutdown, |RecordShutdownStartTimeStamp| will bail out and + // we will end up with a null |gRecordedShutdownStartTime| here. This can happen + // during tests. + return; + } + + nsCString tmpName = name; + tmpName += ".tmp"; + FILE *f = fopen(tmpName.get(), "w"); + if (!f) + return; + // On a normal release build this should be called just before + // calling _exit, but on a debug build or when the user forces a full + // shutdown this is called as late as possible, so we have to + // white list this write as write poisoning will be enabled. + MozillaRegisterDebugFILE(f); + + TimeStamp now = TimeStamp::Now(); + MOZ_ASSERT(now >= gRecordedShutdownStartTime); + TimeDuration diff = now - gRecordedShutdownStartTime; + uint32_t diff2 = diff.ToMilliseconds(); + int written = fprintf(f, "%d\n", diff2); + MozillaUnRegisterDebugFILE(f); + int rv = fclose(f); + if (written < 0 || rv != 0) { + PR_Delete(tmpName.get()); + return; + } + PR_Delete(name.get()); + PR_Rename(tmpName.get(), name.get()); +} + +} // namespace mozilla + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry:: +// These are NOT listed in Telemetry.h + +namespace mozilla { +namespace Telemetry { + +ProcessedStack::ProcessedStack() +{ +} + +size_t ProcessedStack::GetStackSize() const +{ + return mStack.size(); +} + +size_t ProcessedStack::GetNumModules() const +{ + return mModules.size(); +} + +bool ProcessedStack::Module::operator==(const Module& aOther) const { + return mName == aOther.mName && + mBreakpadId == aOther.mBreakpadId; +} + +const ProcessedStack::Frame &ProcessedStack::GetFrame(unsigned aIndex) const +{ + MOZ_ASSERT(aIndex < mStack.size()); + return mStack[aIndex]; +} + +void ProcessedStack::AddFrame(const Frame &aFrame) +{ + mStack.push_back(aFrame); +} + +const ProcessedStack::Module &ProcessedStack::GetModule(unsigned aIndex) const +{ + MOZ_ASSERT(aIndex < mModules.size()); + return mModules[aIndex]; +} + +void ProcessedStack::AddModule(const Module &aModule) +{ + mModules.push_back(aModule); +} + +void ProcessedStack::Clear() { + mModules.clear(); + mStack.clear(); +} + +ProcessedStack +GetStackAndModules(const std::vector& aPCs) +{ + std::vector rawStack; + auto stackEnd = aPCs.begin() + std::min(aPCs.size(), kMaxChromeStackDepth); + for (auto i = aPCs.begin(); i != stackEnd; ++i) { + uintptr_t aPC = *i; + StackFrame Frame = {aPC, static_cast(rawStack.size()), + std::numeric_limits::max()}; + rawStack.push_back(Frame); + } + +#ifdef MOZ_ENABLE_PROFILER_SPS + // Remove all modules not referenced by a PC on the stack + std::sort(rawStack.begin(), rawStack.end(), CompareByPC); + + size_t moduleIndex = 0; + size_t stackIndex = 0; + size_t stackSize = rawStack.size(); + + SharedLibraryInfo rawModules = SharedLibraryInfo::GetInfoForSelf(); + rawModules.SortByAddress(); + + while (moduleIndex < rawModules.GetSize()) { + const SharedLibrary& module = rawModules.GetEntry(moduleIndex); + uintptr_t moduleStart = module.GetStart(); + uintptr_t moduleEnd = module.GetEnd() - 1; + // the interval is [moduleStart, moduleEnd) + + bool moduleReferenced = false; + for (;stackIndex < stackSize; ++stackIndex) { + uintptr_t pc = rawStack[stackIndex].mPC; + if (pc >= moduleEnd) + break; + + if (pc >= moduleStart) { + // If the current PC is within the current module, mark + // module as used + moduleReferenced = true; + rawStack[stackIndex].mPC -= moduleStart; + rawStack[stackIndex].mModIndex = moduleIndex; + } else { + // PC does not belong to any module. It is probably from + // the JIT. Use a fixed mPC so that we don't get different + // stacks on different runs. + rawStack[stackIndex].mPC = + std::numeric_limits::max(); + } + } + + if (moduleReferenced) { + ++moduleIndex; + } else { + // Remove module if no PCs within its address range + rawModules.RemoveEntries(moduleIndex, moduleIndex + 1); + } + } + + for (;stackIndex < stackSize; ++stackIndex) { + // These PCs are past the last module. + rawStack[stackIndex].mPC = std::numeric_limits::max(); + } + + std::sort(rawStack.begin(), rawStack.end(), CompareByIndex); +#endif + + // Copy the information to the return value. + ProcessedStack Ret; + for (std::vector::iterator i = rawStack.begin(), + e = rawStack.end(); i != e; ++i) { + const StackFrame &rawFrame = *i; + mozilla::Telemetry::ProcessedStack::Frame frame = { rawFrame.mPC, rawFrame.mModIndex }; + Ret.AddFrame(frame); + } + +#ifdef MOZ_ENABLE_PROFILER_SPS + for (unsigned i = 0, n = rawModules.GetSize(); i != n; ++i) { + const SharedLibrary &info = rawModules.GetEntry(i); + const std::string &name = info.GetName(); + std::string basename = name; +#ifdef XP_MACOSX + // FIXME: We want to use just the basename as the libname, but the + // current profiler addon needs the full path name, so we compute the + // basename in here. + size_t pos = name.rfind('/'); + if (pos != std::string::npos) { + basename = name.substr(pos + 1); + } +#endif + mozilla::Telemetry::ProcessedStack::Module module = { + basename, + info.GetBreakpadId() + }; + Ret.AddModule(module); + } +#endif + + return Ret; +} + +void +TimeHistogram::Add(PRIntervalTime aTime) +{ + uint32_t timeMs = PR_IntervalToMilliseconds(aTime); + size_t index = mozilla::FloorLog2(timeMs); + operator[](index)++; +} + +const char* +HangStack::InfallibleAppendViaBuffer(const char* aText, size_t aLength) +{ + MOZ_ASSERT(this->canAppendWithoutRealloc(1)); + // Include null-terminator in length count. + MOZ_ASSERT(mBuffer.canAppendWithoutRealloc(aLength + 1)); + + const char* const entry = mBuffer.end(); + mBuffer.infallibleAppend(aText, aLength); + mBuffer.infallibleAppend('\0'); // Explicitly append null-terminator + this->infallibleAppend(entry); + return entry; +} + +const char* +HangStack::AppendViaBuffer(const char* aText, size_t aLength) +{ + if (!this->reserve(this->length() + 1)) { + return nullptr; + } + + // Keep track of the previous buffer in case we need to adjust pointers later. + const char* const prevStart = mBuffer.begin(); + const char* const prevEnd = mBuffer.end(); + + // Include null-terminator in length count. + if (!mBuffer.reserve(mBuffer.length() + aLength + 1)) { + return nullptr; + } + + if (prevStart != mBuffer.begin()) { + // The buffer has moved; we have to adjust pointers in the stack. + for (const char** entry = this->begin(); entry != this->end(); entry++) { + if (*entry >= prevStart && *entry < prevEnd) { + // Move from old buffer to new buffer. + *entry += mBuffer.begin() - prevStart; + } + } + } + + return InfallibleAppendViaBuffer(aText, aLength); +} + +uint32_t +HangHistogram::GetHash(const HangStack& aStack) +{ + uint32_t hash = 0; + for (const char* const* label = aStack.begin(); + label != aStack.end(); label++) { + /* If the string is within our buffer, we need to hash its content. + Otherwise, the string is statically allocated, and we only need + to hash the pointer instead of the content. */ + if (aStack.IsInBuffer(*label)) { + hash = AddToHash(hash, HashString(*label)); + } else { + hash = AddToHash(hash, *label); + } + } + return hash; +} + +bool +HangHistogram::operator==(const HangHistogram& aOther) const +{ + if (mHash != aOther.mHash) { + return false; + } + if (mStack.length() != aOther.mStack.length()) { + return false; + } + return mStack == aOther.mStack; +} + +} // namespace Telemetry +} // namespace mozilla + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry:: +// These are listed in Telemetry.h + +namespace mozilla { +namespace Telemetry { + +// The external API for controlling recording state +void +SetHistogramRecordingEnabled(ID aID, bool aEnabled) +{ + TelemetryHistogram::SetHistogramRecordingEnabled(aID, aEnabled); +} + +void +Accumulate(ID aHistogram, uint32_t aSample) +{ + TelemetryHistogram::Accumulate(aHistogram, aSample); +} + +void +Accumulate(ID aID, const nsCString& aKey, uint32_t aSample) +{ + TelemetryHistogram::Accumulate(aID, aKey, aSample); +} + +void +Accumulate(const char* name, uint32_t sample) +{ + TelemetryHistogram::Accumulate(name, sample); +} + +void +Accumulate(const char *name, const nsCString& key, uint32_t sample) +{ + TelemetryHistogram::Accumulate(name, key, sample); +} + +void +AccumulateCategorical(ID id, const nsCString& label) +{ + TelemetryHistogram::AccumulateCategorical(id, label); +} + +void +AccumulateTimeDelta(ID aHistogram, TimeStamp start, TimeStamp end) +{ + Accumulate(aHistogram, + static_cast((end - start).ToMilliseconds())); +} + +void +AccumulateChild(GeckoProcessType aProcessType, + const nsTArray& aAccumulations) +{ + TelemetryHistogram::AccumulateChild(aProcessType, aAccumulations); +} + +void +AccumulateChildKeyed(GeckoProcessType aProcessType, + const nsTArray& aAccumulations) +{ + TelemetryHistogram::AccumulateChildKeyed(aProcessType, aAccumulations); +} + +const char* +GetHistogramName(ID id) +{ + return TelemetryHistogram::GetHistogramName(id); +} + +bool +CanRecordBase() +{ + return TelemetryHistogram::CanRecordBase(); +} + +bool +CanRecordExtended() +{ + return TelemetryHistogram::CanRecordExtended(); +} + +void +RecordSlowSQLStatement(const nsACString &statement, + const nsACString &dbName, + uint32_t delay) +{ + TelemetryImpl::RecordSlowStatement(statement, dbName, delay); +} + +void +RecordWebrtcIceCandidates(const uint32_t iceCandidateBitmask, + const bool success) +{ + TelemetryImpl::RecordIceCandidates(iceCandidateBitmask, success); +} + +void Init() +{ + // Make the service manager hold a long-lived reference to the service + nsCOMPtr telemetryService = + do_GetService("@mozilla.org/base/telemetry;1"); + MOZ_ASSERT(telemetryService); +} + +#if defined(MOZ_ENABLE_PROFILER_SPS) +void RecordChromeHang(uint32_t duration, + ProcessedStack &aStack, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations) +{ + TelemetryImpl::RecordChromeHang(duration, aStack, + aSystemUptime, aFirefoxUptime, + Move(aAnnotations)); +} +#endif + +void RecordThreadHangStats(ThreadHangStats& aStats) +{ + TelemetryImpl::RecordThreadHangStats(aStats); +} + + +void +WriteFailedProfileLock(nsIFile* aProfileDir) +{ + nsCOMPtr file; + nsresult rv = GetFailedProfileLockFile(getter_AddRefs(file), aProfileDir); + NS_ENSURE_SUCCESS_VOID(rv); + int64_t fileSize = 0; + rv = file->GetFileSize(&fileSize); + // It's expected that the file might not exist yet + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) { + return; + } + nsCOMPtr fileStream; + rv = NS_NewLocalFileStream(getter_AddRefs(fileStream), file, + PR_RDWR | PR_CREATE_FILE, 0640); + NS_ENSURE_SUCCESS_VOID(rv); + NS_ENSURE_TRUE_VOID(fileSize <= kMaxFailedProfileLockFileSize); + unsigned int failedLockCount = 0; + if (fileSize > 0) { + nsCOMPtr inStream = do_QueryInterface(fileStream); + NS_ENSURE_TRUE_VOID(inStream); + if (!GetFailedLockCount(inStream, fileSize, failedLockCount)) { + failedLockCount = 0; + } + } + ++failedLockCount; + nsAutoCString bufStr; + bufStr.AppendInt(static_cast(failedLockCount)); + nsCOMPtr seekStream = do_QueryInterface(fileStream); + NS_ENSURE_TRUE_VOID(seekStream); + // If we read in an existing failed lock count, we need to reset the file ptr + if (fileSize > 0) { + rv = seekStream->Seek(nsISeekableStream::NS_SEEK_SET, 0); + NS_ENSURE_SUCCESS_VOID(rv); + } + nsCOMPtr outStream = do_QueryInterface(fileStream); + uint32_t bytesLeft = bufStr.Length(); + const char* bytes = bufStr.get(); + do { + uint32_t written = 0; + rv = outStream->Write(bytes, bytesLeft, &written); + if (NS_FAILED(rv)) { + break; + } + bytes += written; + bytesLeft -= written; + } while (bytesLeft > 0); + seekStream->SetEOF(); +} + +void +InitIOReporting(nsIFile* aXreDir) +{ + // Never initialize twice + if (sTelemetryIOObserver) { + return; + } + + sTelemetryIOObserver = new TelemetryIOInterposeObserver(aXreDir); + IOInterposer::Register(IOInterposeObserver::OpAllWithStaging, + sTelemetryIOObserver); +} + +void +SetProfileDir(nsIFile* aProfD) +{ + if (!sTelemetryIOObserver || !aProfD) { + return; + } + nsAutoString profDirPath; + nsresult rv = aProfD->GetPath(profDirPath); + if (NS_FAILED(rv)) { + return; + } + sTelemetryIOObserver->AddPath(profDirPath, NS_LITERAL_STRING("{profile}")); +} + +void CreateStatisticsRecorder() +{ + TelemetryHistogram::CreateStatisticsRecorder(); +} + +void DestroyStatisticsRecorder() +{ + TelemetryHistogram::DestroyStatisticsRecorder(); +} + +// Scalar API C++ Endpoints + +void +ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aVal) +{ + TelemetryScalar::Add(aId, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aVal) +{ + TelemetryScalar::Set(aId, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, bool aVal) +{ + TelemetryScalar::Set(aId, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aVal) +{ + TelemetryScalar::Set(aId, aVal); +} + +void +ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aVal) +{ + TelemetryScalar::SetMaximum(aId, aVal); +} + +void +ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal) +{ + TelemetryScalar::Add(aId, aKey, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal) +{ + TelemetryScalar::Set(aId, aKey, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aVal) +{ + TelemetryScalar::Set(aId, aKey, aVal); +} + +void +ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal) +{ + TelemetryScalar::SetMaximum(aId, aKey, aVal); +} + +} // namespace Telemetry +} // namespace mozilla diff --git a/toolkit/components/telemetry/Telemetry.h b/toolkit/components/telemetry/Telemetry.h new file mode 100644 index 000000000..64f50013a --- /dev/null +++ b/toolkit/components/telemetry/Telemetry.h @@ -0,0 +1,436 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef Telemetry_h__ +#define Telemetry_h__ + +#include "mozilla/GuardObjects.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/StartupTimeline.h" +#include "nsTArray.h" +#include "nsStringGlue.h" +#include "nsXULAppAPI.h" + +#include "mozilla/TelemetryHistogramEnums.h" +#include "mozilla/TelemetryScalarEnums.h" + +/****************************************************************************** + * This implements the Telemetry system. + * It allows recording into histograms as well some more specialized data + * points and gives access to the data. + * + * For documentation on how to add and use new Telemetry probes, see: + * https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Adding_a_new_Telemetry_probe + * + * For more general information on Telemetry see: + * https://wiki.mozilla.org/Telemetry + *****************************************************************************/ + +namespace mozilla { +namespace HangMonitor { + class HangAnnotations; +} // namespace HangMonitor +namespace Telemetry { + +struct Accumulation; +struct KeyedAccumulation; + +enum TimerResolution { + Millisecond, + Microsecond +}; + +/** + * Create and destroy the underlying base::StatisticsRecorder singleton. + * Creation has to be done very early in the startup sequence. + */ +void CreateStatisticsRecorder(); +void DestroyStatisticsRecorder(); + +/** + * Initialize the Telemetry service on the main thread at startup. + */ +void Init(); + +/** + * Adds sample to a histogram defined in TelemetryHistogramEnums.h + * + * @param id - histogram id + * @param sample - value to record. + */ +void Accumulate(ID id, uint32_t sample); + +/** + * Adds sample to a keyed histogram defined in TelemetryHistogramEnums.h + * + * @param id - keyed histogram id + * @param key - the string key + * @param sample - (optional) value to record, defaults to 1. + */ +void Accumulate(ID id, const nsCString& key, uint32_t sample = 1); + +/** + * Adds a sample to a histogram defined in TelemetryHistogramEnums.h. + * This function is here to support telemetry measurements from Java, + * where we have only names and not numeric IDs. You should almost + * certainly be using the by-enum-id version instead of this one. + * + * @param name - histogram name + * @param sample - value to record + */ +void Accumulate(const char* name, uint32_t sample); + +/** + * Adds a sample to a histogram defined in TelemetryHistogramEnums.h. + * This function is here to support telemetry measurements from Java, + * where we have only names and not numeric IDs. You should almost + * certainly be using the by-enum-id version instead of this one. + * + * @param name - histogram name + * @param key - the string key + * @param sample - sample - (optional) value to record, defaults to 1. + */ +void Accumulate(const char *name, const nsCString& key, uint32_t sample = 1); + +/** + * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h + * This is the typesafe - and preferred - way to use the categorical histograms + * by passing values from the corresponding Telemetry::LABELS_* enum. + * + * @param enumValue - Label value from one of the Telemetry::LABELS_* enums. + */ +template +void AccumulateCategorical(E enumValue) { + static_assert(IsCategoricalLabelEnum::value, + "Only categorical label enum types are supported."); + Accumulate(static_cast(CategoricalLabelId::value), + static_cast(enumValue)); +}; + +/** + * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h + * This string will be matched against the labels defined in Histograms.json. + * If the string does not match a label defined for the histogram, nothing will + * be recorded. + * + * @param id - The histogram id. + * @param label - A string label value that is defined in Histograms.json for this histogram. + */ +void AccumulateCategorical(ID id, const nsCString& label); + +/** + * Adds time delta in milliseconds to a histogram defined in TelemetryHistogramEnums.h + * + * @param id - histogram id + * @param start - start time + * @param end - end time + */ +void AccumulateTimeDelta(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()); + +/** + * Accumulate child process data into histograms for the given process type. + * + * @param aAccumulations - accumulation actions to perform + */ +void AccumulateChild(GeckoProcessType aProcessType, const nsTArray& aAccumulations); + +/** + * Accumulate child process data into keyed histograms for the given process type. + * + * @param aAccumulations - accumulation actions to perform + */ +void AccumulateChildKeyed(GeckoProcessType aProcessType, const nsTArray& aAccumulations); + +/** + * Enable/disable recording for this histogram at runtime. + * Recording is enabled by default, unless listed at kRecordingInitiallyDisabledIDs[]. + * id must be a valid telemetry enum, otherwise an assertion is triggered. + * + * @param id - histogram id + * @param enabled - whether or not to enable recording from now on. + */ +void SetHistogramRecordingEnabled(ID id, bool enabled); + +const char* GetHistogramName(ID id); + +/** + * Those wrappers are needed because the VS versions we use do not support free + * functions with default template arguments. + */ +template +struct AccumulateDelta_impl +{ + static void compute(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()); + static void compute(ID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now()); +}; + +template<> +struct AccumulateDelta_impl +{ + static void compute(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()) { + Accumulate(id, static_cast((end - start).ToMilliseconds())); + } + static void compute(ID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now()) { + Accumulate(id, key, static_cast((end - start).ToMilliseconds())); + } +}; + +template<> +struct AccumulateDelta_impl +{ + static void compute(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()) { + Accumulate(id, static_cast((end - start).ToMicroseconds())); + } + static void compute(ID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now()) { + Accumulate(id, key, static_cast((end - start).ToMicroseconds())); + } +}; + + +template +class MOZ_RAII AutoTimer { +public: + explicit AutoTimer(TimeStamp aStart = TimeStamp::Now() MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : start(aStart) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + } + + explicit AutoTimer(const nsCString& aKey, TimeStamp aStart = TimeStamp::Now() MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : start(aStart) + , key(aKey) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + } + + ~AutoTimer() { + if (key.IsEmpty()) { + AccumulateDelta_impl::compute(id, start); + } else { + AccumulateDelta_impl::compute(id, key, start); + } + } + +private: + const TimeStamp start; + const nsCString key; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +template +class MOZ_RAII AutoCounter { +public: + explicit AutoCounter(uint32_t counterStart = 0 MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + : counter(counterStart) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + } + + ~AutoCounter() { + Accumulate(id, counter); + } + + // Prefix increment only, to encourage good habits. + void operator++() { + ++counter; + } + + // Chaining doesn't make any sense, don't return anything. + void operator+=(int increment) { + counter += increment; + } + +private: + uint32_t counter; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +/** + * Indicates whether Telemetry base data recording is turned on. Added for future uses. + */ +bool CanRecordBase(); + +/** + * Indicates whether Telemetry extended data recording is turned on. This is intended + * to guard calls to Accumulate when the statistic being recorded is expensive to compute. + */ +bool CanRecordExtended(); + +/** + * Records slow SQL statements for Telemetry reporting. + * + * @param statement - offending SQL statement to record + * @param dbName - DB filename + * @param delay - execution time in milliseconds + */ +void RecordSlowSQLStatement(const nsACString &statement, + const nsACString &dbName, + uint32_t delay); + +/** + * Record Webrtc ICE candidate type combinations in a 17bit bitmask + * + * @param iceCandidateBitmask - the bitmask representing local and remote ICE + * candidate types present for the connection + * @param success - did the peer connection connected + */ +void +RecordWebrtcIceCandidates(const uint32_t iceCandidateBitmask, + const bool success); +/** + * Initialize I/O Reporting + * Initially this only records I/O for files in the binary directory. + * + * @param aXreDir - XRE directory + */ +void InitIOReporting(nsIFile* aXreDir); + +/** + * Set the profile directory. Once called, files in the profile directory will + * be included in I/O reporting. We can't use the directory + * service to obtain this information because it isn't running yet. + */ +void SetProfileDir(nsIFile* aProfD); + +/** + * Called to inform Telemetry that startup has completed. + */ +void LeavingStartupStage(); + +/** + * Called to inform Telemetry that shutdown is commencing. + */ +void EnteringShutdownStage(); + +/** + * Thresholds for a statement to be considered slow, in milliseconds + */ +const uint32_t kSlowSQLThresholdForMainThread = 50; +const uint32_t kSlowSQLThresholdForHelperThreads = 100; + +class ProcessedStack; + +/** + * Record the main thread's call stack after it hangs. + * + * @param aDuration - Approximate duration of main thread hang, in seconds + * @param aStack - Array of PCs from the hung call stack + * @param aSystemUptime - System uptime at the time of the hang, in minutes + * @param aFirefoxUptime - Firefox uptime at the time of the hang, in minutes + * @param aAnnotations - Any annotations to be added to the report + */ +#if defined(MOZ_ENABLE_PROFILER_SPS) +void RecordChromeHang(uint32_t aDuration, + ProcessedStack &aStack, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + mozilla::UniquePtr + aAnnotations); +#endif + +class ThreadHangStats; + +/** + * Move a ThreadHangStats to Telemetry storage. Normally Telemetry queries + * for active ThreadHangStats through BackgroundHangMonitor, but once a + * thread exits, the thread's copy of ThreadHangStats needs to be moved to + * inside Telemetry using this function. + * + * @param aStats ThreadHangStats to save; the data inside aStats + * will be moved and aStats should be treated as + * invalid after this function returns + */ +void RecordThreadHangStats(ThreadHangStats& aStats); + +/** + * Record a failed attempt at locking the user's profile. + * + * @param aProfileDir The profile directory whose lock attempt failed + */ +void WriteFailedProfileLock(nsIFile* aProfileDir); + +/** + * Adds the value to the given scalar. + * + * @param aId The scalar enum id. + * @param aValue The value to add to the scalar. + */ +void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue); + +/** + * Sets the scalar to the given value. + * + * @param aId The scalar enum id. + * @param aValue The value to set the scalar to. + */ +void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue); + +/** + * Sets the scalar to the given value. + * + * @param aId The scalar enum id. + * @param aValue The value to set the scalar to. + */ +void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue); + +/** + * Sets the scalar to the given value. + * + * @param aId The scalar enum id. + * @param aValue The value to set the scalar to, truncated to + * 50 characters if exceeding that length. + */ +void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue); + +/** + * Sets the scalar to the maximum of the current and the passed value. + * + * @param aId The scalar enum id. + * @param aValue The value the scalar is set to if its greater + * than the current value. + */ +void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue); + +/** + * Adds the value to the given scalar. + * + * @param aId The scalar enum id. + * @param aKey The scalar key. + * @param aValue The value to add to the scalar. + */ +void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); + +/** + * Sets the scalar to the given value. + * + * @param aId The scalar enum id. + * @param aKey The scalar key. + * @param aValue The value to set the scalar to. + */ +void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); + +/** + * Sets the scalar to the given value. + * + * @param aId The scalar enum id. + * @param aKey The scalar key. + * @param aValue The value to set the scalar to. + */ +void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue); + +/** + * Sets the scalar to the maximum of the current and the passed value. + * + * @param aId The scalar enum id. + * @param aKey The scalar key. + * @param aValue The value the scalar is set to if its greater + * than the current value. + */ +void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); + +} // namespace Telemetry +} // namespace mozilla + +#endif // Telemetry_h__ diff --git a/toolkit/components/telemetry/TelemetryArchive.jsm b/toolkit/components/telemetry/TelemetryArchive.jsm new file mode 100644 index 000000000..c5d251ab7 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryArchive.jsm @@ -0,0 +1,125 @@ +/* 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 = [ + "TelemetryArchive" +]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryArchive::"; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled"; + +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage", + "resource://gre/modules/TelemetryStorage.jsm"); + +this.TelemetryArchive = { + /** + * Get a list of the archived pings, sorted by the creation date. + * Note that scanning the archived pings on disk is delayed on startup, + * use promizeInitialized() to access this after scanning. + * + * @return {Promise>} + * A list of the archived ping info in the form: + * { id: , + * timestampCreated: , + * type: } + */ + promiseArchivedPingList: function() { + return TelemetryArchiveImpl.promiseArchivedPingList(); + }, + + /** + * Load an archived ping from disk by id, asynchronously. + * + * @param id {String} The pings UUID. + * @return {Promise} A promise resolved with the pings data on success. + */ + promiseArchivedPingById: function(id) { + return TelemetryArchiveImpl.promiseArchivedPingById(id); + }, + + /** + * Archive a ping and persist it to disk. + * + * @param {object} ping The ping data to archive. + * @return {promise} Promise that is resolved when the ping is successfully archived. + */ + promiseArchivePing: function(ping) { + return TelemetryArchiveImpl.promiseArchivePing(ping); + }, +}; + +/** + * Checks if pings can be archived. Some products (e.g. Thunderbird) might not want + * to do that. + * @return {Boolean} True if pings should be archived, false otherwise. + */ +function shouldArchivePings() { + return Preferences.get(PREF_ARCHIVE_ENABLED, false); +} + +var TelemetryArchiveImpl = { + _logger: null, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + + return this._logger; + }, + + promiseArchivePing: function(ping) { + if (!shouldArchivePings()) { + this._log.trace("promiseArchivePing - archiving is disabled"); + return Promise.resolve(); + } + + for (let field of ["creationDate", "id", "type"]) { + if (!(field in ping)) { + this._log.warn("promiseArchivePing - missing field " + field) + return Promise.reject(new Error("missing field " + field)); + } + } + + return TelemetryStorage.saveArchivedPing(ping); + }, + + _buildArchivedPingList: function(archivedPingsMap) { + let list = Array.from(archivedPingsMap, p => ({ + id: p[0], + timestampCreated: p[1].timestampCreated, + type: p[1].type, + })); + + list.sort((a, b) => a.timestampCreated - b.timestampCreated); + + return list; + }, + + promiseArchivedPingList: function() { + this._log.trace("promiseArchivedPingList"); + + return TelemetryStorage.loadArchivedPingList().then(loadedInfo => { + return this._buildArchivedPingList(loadedInfo); + }); + }, + + promiseArchivedPingById: function(id) { + this._log.trace("promiseArchivedPingById - id: " + id); + return TelemetryStorage.loadArchivedPing(id); + }, +}; diff --git a/toolkit/components/telemetry/TelemetryCommon.cpp b/toolkit/components/telemetry/TelemetryCommon.cpp new file mode 100644 index 000000000..db9341ab5 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryCommon.cpp @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsITelemetry.h" +#include "nsVersionComparator.h" +#include "mozilla/TimeStamp.h" +#include "nsIConsoleService.h" +#include "nsThreadUtils.h" + +#include "TelemetryCommon.h" + +#include + +namespace mozilla { +namespace Telemetry { +namespace Common { + +bool +IsExpiredVersion(const char* aExpiration) +{ + MOZ_ASSERT(aExpiration); + // Note: We intentionally don't construct a static Version object here as we + // saw odd crashes around this (see bug 1334105). + return strcmp(aExpiration, "never") && strcmp(aExpiration, "default") && + (mozilla::Version(aExpiration) <= MOZ_APP_VERSION); +} + +bool +IsInDataset(uint32_t aDataset, uint32_t aContainingDataset) +{ + if (aDataset == aContainingDataset) { + return true; + } + + // The "optin on release channel" dataset is a superset of the + // "optout on release channel one". + if (aContainingDataset == nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN && + aDataset == nsITelemetry::DATASET_RELEASE_CHANNEL_OPTOUT) { + return true; + } + + return false; +} + +bool +CanRecordDataset(uint32_t aDataset, bool aCanRecordBase, bool aCanRecordExtended) +{ + // If we are extended telemetry is enabled, we are allowed to record + // regardless of the dataset. + if (aCanRecordExtended) { + return true; + } + + // If base telemetry data is enabled and we're trying to record base + // telemetry, allow it. + if (aCanRecordBase && + IsInDataset(aDataset, nsITelemetry::DATASET_RELEASE_CHANNEL_OPTOUT)) { + return true; + } + + // We're not recording extended telemetry or this is not the base + // dataset. Bail out. + return false; +} + +nsresult +MsSinceProcessStart(double* aResult) +{ + bool error; + *aResult = (TimeStamp::NowLoRes() - + TimeStamp::ProcessCreation(error)).ToMilliseconds(); + if (error) { + return NS_ERROR_NOT_AVAILABLE; + } + return NS_OK; +} + +void +LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg) +{ + if (!NS_IsMainThread()) { + nsString msg(aMsg); + nsCOMPtr task = + NS_NewRunnableFunction([aLogLevel, msg]() { LogToBrowserConsole(aLogLevel, msg); }); + NS_DispatchToMainThread(task.forget(), NS_DISPATCH_NORMAL); + return; + } + + nsCOMPtr console(do_GetService("@mozilla.org/consoleservice;1")); + if (!console) { + NS_WARNING("Failed to log message to console."); + return; + } + + nsCOMPtr error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); + error->Init(aMsg, EmptyString(), EmptyString(), 0, 0, aLogLevel, "chrome javascript"); + console->LogMessage(error); +} + +} // namespace Common +} // namespace Telemetry +} // namespace mozilla diff --git a/toolkit/components/telemetry/TelemetryCommon.h b/toolkit/components/telemetry/TelemetryCommon.h new file mode 100644 index 000000000..3beefd673 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryCommon.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef TelemetryCommon_h__ +#define TelemetryCommon_h__ + +#include "nsTHashtable.h" +#include "jsapi.h" +#include "nsIScriptError.h" + +namespace mozilla { +namespace Telemetry { +namespace Common { + +template +class AutoHashtable : public nsTHashtable +{ +public: + explicit AutoHashtable(uint32_t initLength = + PLDHashTable::kDefaultInitialLength); + typedef bool (*ReflectEntryFunc)(EntryType *entry, JSContext *cx, JS::Handle obj); + bool ReflectIntoJS(ReflectEntryFunc entryFunc, JSContext *cx, JS::Handle obj); +}; + +template +AutoHashtable::AutoHashtable(uint32_t initLength) + : nsTHashtable(initLength) +{ +} + +/** + * Reflect the individual entries of table into JS, usually by defining + * some property and value of obj. entryFunc is called for each entry. + */ +template +bool +AutoHashtable::ReflectIntoJS(ReflectEntryFunc entryFunc, + JSContext *cx, JS::Handle obj) +{ + for (auto iter = this->Iter(); !iter.Done(); iter.Next()) { + if (!entryFunc(iter.Get(), cx, obj)) { + return false; + } + } + return true; +} + +bool IsExpiredVersion(const char* aExpiration); +bool IsInDataset(uint32_t aDataset, uint32_t aContainingDataset); +bool CanRecordDataset(uint32_t aDataset, bool aCanRecordBase, bool aCanRecordExtended); + +/** + * Return the number of milliseconds since process start using monotonic + * timestamps (unaffected by system clock changes). + * + * @return NS_OK on success, NS_ERROR_NOT_AVAILABLE if TimeStamp doesn't have the data. + */ +nsresult MsSinceProcessStart(double* aResult); + +/** + * Dumps a log message to the Browser Console using the provided level. + * + * @param aLogLevel The level to use when displaying the message in the browser console + * (e.g. nsIScriptError::warningFlag, ...). + * @param aMsg The text message to print to the console. + */ +void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg); + +} // namespace Common +} // namespace Telemetry +} // namespace mozilla + +#endif // TelemetryCommon_h__ diff --git a/toolkit/components/telemetry/TelemetryComms.h b/toolkit/components/telemetry/TelemetryComms.h new file mode 100644 index 000000000..0f2d888e3 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryComms.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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 + */ + +#ifndef Telemetry_Comms_h__ +#define Telemetry_Comms_h__ + +#include "ipc/IPCMessageUtils.h" + +namespace mozilla { +namespace Telemetry { + +enum ID : uint32_t; + +struct Accumulation +{ + mozilla::Telemetry::ID mId; + uint32_t mSample; +}; + +struct KeyedAccumulation +{ + mozilla::Telemetry::ID mId; + uint32_t mSample; + nsCString mKey; +}; + +} // namespace Telemetry +} // namespace mozilla + +namespace IPC { + +template<> +struct +ParamTraits +{ + typedef mozilla::Telemetry::Accumulation paramType; + + static void Write(Message* aMsg, const paramType& aParam) + { + aMsg->WriteUInt32(aParam.mId); + WriteParam(aMsg, aParam.mSample); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult) + { + if (!aMsg->ReadUInt32(aIter, reinterpret_cast(&(aResult->mId))) || + !ReadParam(aMsg, aIter, &(aResult->mSample))) { + return false; + } + + return true; + } +}; + +template<> +struct +ParamTraits +{ + typedef mozilla::Telemetry::KeyedAccumulation paramType; + + static void Write(Message* aMsg, const paramType& aParam) + { + aMsg->WriteUInt32(aParam.mId); + WriteParam(aMsg, aParam.mSample); + WriteParam(aMsg, aParam.mKey); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult) + { + if (!aMsg->ReadUInt32(aIter, reinterpret_cast(&(aResult->mId))) || + !ReadParam(aMsg, aIter, &(aResult->mSample)) || + !ReadParam(aMsg, aIter, &(aResult->mKey))) { + return false; + } + + return true; + } +}; + +} // namespace IPC + +#endif // Telemetry_Comms_h__ diff --git a/toolkit/components/telemetry/TelemetryController.jsm b/toolkit/components/telemetry/TelemetryController.jsm new file mode 100644 index 000000000..b8de776da --- /dev/null +++ b/toolkit/components/telemetry/TelemetryController.jsm @@ -0,0 +1,954 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +const myScope = this; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/debug.js", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/PromiseUtils.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/DeferredTask.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const Utils = TelemetryUtils; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryController::"; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_BRANCH_LOG = PREF_BRANCH + "log."; +const PREF_SERVER = PREF_BRANCH + "server"; +const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level"; +const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump"; +const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; +const PREF_SESSIONS_BRANCH = "datareporting.sessions."; +const PREF_UNIFIED = PREF_BRANCH + "unified"; + +// Whether the FHR/Telemetry unification features are enabled. +// Changing this pref requires a restart. +const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false); + +const PING_FORMAT_VERSION = 4; + +// Delay before intializing telemetry (ms) +const TELEMETRY_DELAY = Preferences.get("toolkit.telemetry.initDelay", 60) * 1000; +// Delay before initializing telemetry if we're testing (ms) +const TELEMETRY_TEST_DELAY = 1; + +// Ping types. +const PING_TYPE_MAIN = "main"; +const PING_TYPE_DELETION = "deletion"; + +// Session ping reasons. +const REASON_GATHER_PAYLOAD = "gather-payload"; +const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload"; + +XPCOMUtils.defineLazyModuleGetter(this, "ClientID", + "resource://gre/modules/ClientID.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", + "@mozilla.org/base/telemetry;1", + "nsITelemetry"); +XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", + "resource://gre/modules/AsyncShutdown.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage", + "resource://gre/modules/TelemetryStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ThirdPartyCookieProbe", + "resource://gre/modules/ThirdPartyCookieProbe.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment", + "resource://gre/modules/TelemetryEnvironment.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionRecorder", + "resource://gre/modules/SessionRecorder.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive", + "resource://gre/modules/TelemetryArchive.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySession", + "resource://gre/modules/TelemetrySession.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySend", + "resource://gre/modules/TelemetrySend.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryReportingPolicy", + "resource://gre/modules/TelemetryReportingPolicy.jsm"); + +/** + * Setup Telemetry logging. This function also gets called when loggin related + * preferences change. + */ +var gLogger = null; +var gLogAppenderDump = null; +function configureLogging() { + if (!gLogger) { + gLogger = Log.repository.getLogger(LOGGER_NAME); + + // Log messages need to go to the browser console. + let consoleAppender = new Log.ConsoleAppender(new Log.BasicFormatter()); + gLogger.addAppender(consoleAppender); + + Preferences.observe(PREF_BRANCH_LOG, configureLogging); + } + + // Make sure the logger keeps up with the logging level preference. + gLogger.level = Log.Level[Preferences.get(PREF_LOG_LEVEL, "Warn")]; + + // If enabled in the preferences, add a dump appender. + let logDumping = Preferences.get(PREF_LOG_DUMP, false); + if (logDumping != !!gLogAppenderDump) { + if (logDumping) { + gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter()); + gLogger.addAppender(gLogAppenderDump); + } else { + gLogger.removeAppender(gLogAppenderDump); + gLogAppenderDump = null; + } + } +} + +/** + * This is a policy object used to override behavior for testing. + */ +var Policy = { + now: () => new Date(), + generatePingId: () => Utils.generateUUID(), + getCachedClientID: () => ClientID.getCachedClientID(), +} + +this.EXPORTED_SYMBOLS = ["TelemetryController"]; + +this.TelemetryController = Object.freeze({ + Constants: Object.freeze({ + PREF_LOG_LEVEL: PREF_LOG_LEVEL, + PREF_LOG_DUMP: PREF_LOG_DUMP, + PREF_SERVER: PREF_SERVER, + }), + + /** + * Used only for testing purposes. + */ + testInitLogging: function() { + configureLogging(); + }, + + /** + * Used only for testing purposes. + */ + testReset: function() { + return Impl.reset(); + }, + + /** + * Used only for testing purposes. + */ + testSetup: function() { + return Impl.setupTelemetry(true); + }, + + /** + * Used only for testing purposes. + */ + testShutdown: function() { + return Impl.shutdown(); + }, + + /** + * Used only for testing purposes. + */ + testSetupContent: function() { + return Impl.setupContentTelemetry(true); + }, + + /** + * Send a notification. + */ + observe: function (aSubject, aTopic, aData) { + return Impl.observe(aSubject, aTopic, aData); + }, + + /** + * Submit ping payloads to Telemetry. This will assemble a complete ping, adding + * environment data, client id and some general info. + * Depending on configuration, the ping will be sent to the server (immediately or later) + * and archived locally. + * + * To identify the different pings and to be able to query them pings have a type. + * A type is a string identifier that should be unique to the type ping that is being submitted, + * it should only contain alphanumeric characters and '-' for separation, i.e. satisfy: + * /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {Object} [aOptions] Options object. + * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client + * id, false otherwise. + * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the + * environment data. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * @returns {Promise} Test-only - a promise that resolves with the ping id once the ping is stored or sent. + */ + submitExternalPing: function(aType, aPayload, aOptions = {}) { + aOptions.addClientId = aOptions.addClientId || false; + aOptions.addEnvironment = aOptions.addEnvironment || false; + + return Impl.submitExternalPing(aType, aPayload, aOptions); + }, + + /** + * Get the current session ping data as it would be sent out or stored. + * + * @param {bool} aSubsession Whether to get subsession data. Optional, defaults to false. + * @return {object} The current ping data if Telemetry is enabled, null otherwise. + */ + getCurrentPingData: function(aSubsession = false) { + return Impl.getCurrentPingData(aSubsession); + }, + + /** + * Save a ping to disk. + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {Object} [aOptions] Options object. + * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client + * id, false otherwise. + * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the + * environment data. + * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name, + * if found. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * + * @returns {Promise} A promise that resolves with the ping id when the ping is saved to + * disk. + */ + addPendingPing: function(aType, aPayload, aOptions = {}) { + let options = aOptions; + options.addClientId = aOptions.addClientId || false; + options.addEnvironment = aOptions.addEnvironment || false; + options.overwrite = aOptions.overwrite || false; + + return Impl.addPendingPing(aType, aPayload, options); + }, + + /** + * Check if we have an aborted-session ping from a previous session. + * If so, submit and then remove it. + * + * @return {Promise} Promise that is resolved when the ping is saved. + */ + checkAbortedSessionPing: function() { + return Impl.checkAbortedSessionPing(); + }, + + /** + * Save an aborted-session ping to disk without adding it to the pending pings. + * + * @param {Object} aPayload The ping payload data. + * @return {Promise} Promise that is resolved when the ping is saved. + */ + saveAbortedSessionPing: function(aPayload) { + return Impl.saveAbortedSessionPing(aPayload); + }, + + /** + * Remove the aborted-session ping if any exists. + * + * @return {Promise} Promise that is resolved when the ping was removed. + */ + removeAbortedSessionPing: function() { + return Impl.removeAbortedSessionPing(); + }, + + /** + * Write a ping to a specified location on the disk. Does not add the ping to the + * pending pings. + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {String} aFilePath The path to save the ping to. + * @param {Object} [aOptions] Options object. + * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client + * id, false otherwise. + * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the + * environment data. + * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name, + * if found. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * + * @returns {Promise} A promise that resolves with the ping id when the ping is saved to + * disk. + */ + savePing: function(aType, aPayload, aFilePath, aOptions = {}) { + let options = aOptions; + options.addClientId = aOptions.addClientId || false; + options.addEnvironment = aOptions.addEnvironment || false; + options.overwrite = aOptions.overwrite || false; + + return Impl.savePing(aType, aPayload, aFilePath, options); + }, + + /** + * The session recorder instance managed by Telemetry. + * @return {Object} The active SessionRecorder instance or null if not available. + */ + getSessionRecorder: function() { + return Impl._sessionRecorder; + }, + + /** + * Allows waiting for TelemetryControllers delayed initialization to complete. + * The returned promise is guaranteed to resolve before TelemetryController is shutting down. + * @return {Promise} Resolved when delayed TelemetryController initialization completed. + */ + promiseInitialized: function() { + return Impl.promiseInitialized(); + }, +}); + +var Impl = { + _initialized: false, + _initStarted: false, // Whether we started setting up TelemetryController. + _logger: null, + _prevValues: {}, + // The previous build ID, if this is the first run with a new build. + // Undefined if this is not the first run, or the previous build ID is unknown. + _previousBuildID: undefined, + _clientID: null, + // A task performing delayed initialization + _delayedInitTask: null, + // The deferred promise resolved when the initialization task completes. + _delayedInitTaskDeferred: null, + + // The session recorder, shared with FHR and the Data Reporting Service. + _sessionRecorder: null, + // This is a public barrier Telemetry clients can use to add blockers to the shutdown + // of TelemetryController. + // After this barrier, clients can not submit Telemetry pings anymore. + _shutdownBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for clients."), + // This is a private barrier blocked by pending async ping activity (sending & saving). + _connectionsBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for pending ping activity"), + // This is true when running in the test infrastructure. + _testMode: false, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + + return this._logger; + }, + + /** + * Get the data for the "application" section of the ping. + */ + _getApplicationSection: function() { + // Querying architecture and update channel can throw. Make sure to recover and null + // those fields. + let arch = null; + try { + arch = Services.sysinfo.get("arch"); + } catch (e) { + this._log.trace("_getApplicationSection - Unable to get system architecture.", e); + } + + let updateChannel = null; + try { + updateChannel = UpdateUtils.getUpdateChannel(false); + } catch (e) { + this._log.trace("_getApplicationSection - Unable to get update channel.", e); + } + + return { + architecture: arch, + buildId: Services.appinfo.appBuildID, + name: Services.appinfo.name, + version: Services.appinfo.version, + displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY, + vendor: Services.appinfo.vendor, + platformVersion: Services.appinfo.platformVersion, + xpcomAbi: Services.appinfo.XPCOMABI, + channel: updateChannel, + }; + }, + + /** + * Assemble a complete ping following the common ping format specification. + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {Object} aOptions Options object. + * @param {Boolean} aOptions.addClientId true if the ping should contain the client + * id, false otherwise. + * @param {Boolean} aOptions.addEnvironment true if the ping should contain the + * environment data. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * + * @returns {Object} An object that contains the assembled ping data. + */ + assemblePing: function assemblePing(aType, aPayload, aOptions = {}) { + this._log.trace("assemblePing - Type " + aType + ", aOptions " + JSON.stringify(aOptions)); + + // Clone the payload data so we don't race against unexpected changes in subobjects that are + // still referenced by other code. + // We can't trust all callers to do this properly on their own. + let payload = Cu.cloneInto(aPayload, myScope); + + // Fill the common ping fields. + let pingData = { + type: aType, + id: Policy.generatePingId(), + creationDate: (Policy.now()).toISOString(), + version: PING_FORMAT_VERSION, + application: this._getApplicationSection(), + payload: payload, + }; + + if (aOptions.addClientId) { + pingData.clientId = this._clientID; + } + + if (aOptions.addEnvironment) { + pingData.environment = aOptions.overrideEnvironment || TelemetryEnvironment.currentEnvironment; + } + + return pingData; + }, + + /** + * Track any pending ping send and save tasks through the promise passed here. + * This is needed to block shutdown on any outstanding ping activity. + */ + _trackPendingPingTask: function (aPromise) { + this._connectionsBarrier.client.addBlocker("Waiting for ping task", aPromise); + }, + + /** + * Internal function to assemble a complete ping, adding environment data, client id + * and some general info. This waits on the client id to be loaded/generated if it's + * not yet available. Note that this function is synchronous unless we need to load + * the client id. + * Depending on configuration, the ping will be sent to the server (immediately or later) + * and archived locally. + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {Object} [aOptions] Options object. + * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client + * id, false otherwise. + * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the + * environment data. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent. + */ + _submitPingLogic: Task.async(function* (aType, aPayload, aOptions) { + // Make sure to have a clientId if we need one. This cover the case of submitting + // a ping early during startup, before Telemetry is initialized, if no client id was + // cached. + if (!this._clientID && aOptions.addClientId) { + Telemetry.getHistogramById("TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID").add(); + // We can safely call |getClientID| here and during initialization: we would still + // spawn and return one single loading task. + this._clientID = yield ClientID.getClientID(); + } + + const pingData = this.assemblePing(aType, aPayload, aOptions); + this._log.trace("submitExternalPing - ping assembled, id: " + pingData.id); + + // Always persist the pings if we are allowed to. We should not yield on any of the + // following operations to keep this function synchronous for the majority of the calls. + let archivePromise = TelemetryArchive.promiseArchivePing(pingData) + .catch(e => this._log.error("submitExternalPing - Failed to archive ping " + pingData.id, e)); + let p = [ archivePromise ]; + + p.push(TelemetrySend.submitPing(pingData)); + + return Promise.all(p).then(() => pingData.id); + }), + + /** + * Submit ping payloads to Telemetry. + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {Object} [aOptions] Options object. + * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client + * id, false otherwise. + * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the + * environment data. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent. + */ + submitExternalPing: function send(aType, aPayload, aOptions) { + this._log.trace("submitExternalPing - type: " + aType + ", aOptions: " + JSON.stringify(aOptions)); + + // Enforce the type string to only contain sane characters. + const typeUuid = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i; + if (!typeUuid.test(aType)) { + this._log.error("submitExternalPing - invalid ping type: " + aType); + let histogram = Telemetry.getKeyedHistogramById("TELEMETRY_INVALID_PING_TYPE_SUBMITTED"); + histogram.add(aType, 1); + return Promise.reject(new Error("Invalid type string submitted.")); + } + // Enforce that the payload is an object. + if (aPayload === null || typeof aPayload !== 'object' || Array.isArray(aPayload)) { + this._log.error("submitExternalPing - invalid payload type: " + typeof aPayload); + let histogram = Telemetry.getHistogramById("TELEMETRY_INVALID_PAYLOAD_SUBMITTED"); + histogram.add(1); + return Promise.reject(new Error("Invalid payload type submitted.")); + } + + let promise = this._submitPingLogic(aType, aPayload, aOptions); + this._trackPendingPingTask(promise); + return promise; + }, + + /** + * Save a ping to disk. + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {Object} aOptions Options object. + * @param {Boolean} aOptions.addClientId true if the ping should contain the client id, + * false otherwise. + * @param {Boolean} aOptions.addEnvironment true if the ping should contain the + * environment data. + * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * + * @returns {Promise} A promise that resolves with the ping id when the ping is saved to + * disk. + */ + addPendingPing: function addPendingPing(aType, aPayload, aOptions) { + this._log.trace("addPendingPing - Type " + aType + ", aOptions " + JSON.stringify(aOptions)); + + let pingData = this.assemblePing(aType, aPayload, aOptions); + + let savePromise = TelemetryStorage.savePendingPing(pingData); + let archivePromise = TelemetryArchive.promiseArchivePing(pingData).catch(e => { + this._log.error("addPendingPing - Failed to archive ping " + pingData.id, e); + }); + + // Wait for both the archiving and ping persistence to complete. + let promises = [ + savePromise, + archivePromise, + ]; + return Promise.all(promises).then(() => pingData.id); + }, + + /** + * Write a ping to a specified location on the disk. Does not add the ping to the + * pending pings. + * + * @param {String} aType The type of the ping. + * @param {Object} aPayload The actual data payload for the ping. + * @param {String} aFilePath The path to save the ping to. + * @param {Object} aOptions Options object. + * @param {Boolean} aOptions.addClientId true if the ping should contain the client id, + * false otherwise. + * @param {Boolean} aOptions.addEnvironment true if the ping should contain the + * environment data. + * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found. + * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data. + * + * @returns {Promise} A promise that resolves with the ping id when the ping is saved to + * disk. + */ + savePing: function savePing(aType, aPayload, aFilePath, aOptions) { + this._log.trace("savePing - Type " + aType + ", File Path " + aFilePath + + ", aOptions " + JSON.stringify(aOptions)); + let pingData = this.assemblePing(aType, aPayload, aOptions); + return TelemetryStorage.savePingToFile(pingData, aFilePath, aOptions.overwrite) + .then(() => pingData.id); + }, + + /** + * Check whether we have an aborted-session ping. If so add it to the pending pings and archive it. + * + * @return {Promise} Promise that is resolved when the ping is submitted and archived. + */ + checkAbortedSessionPing: Task.async(function*() { + let ping = yield TelemetryStorage.loadAbortedSessionPing(); + this._log.trace("checkAbortedSessionPing - found aborted-session ping: " + !!ping); + if (!ping) { + return; + } + + try { + yield TelemetryStorage.addPendingPing(ping); + yield TelemetryArchive.promiseArchivePing(ping); + } catch (e) { + this._log.error("checkAbortedSessionPing - Unable to add the pending ping", e); + } finally { + yield TelemetryStorage.removeAbortedSessionPing(); + } + }), + + /** + * Save an aborted-session ping to disk without adding it to the pending pings. + * + * @param {Object} aPayload The ping payload data. + * @return {Promise} Promise that is resolved when the ping is saved. + */ + saveAbortedSessionPing: function(aPayload) { + this._log.trace("saveAbortedSessionPing"); + const options = {addClientId: true, addEnvironment: true}; + const pingData = this.assemblePing(PING_TYPE_MAIN, aPayload, options); + return TelemetryStorage.saveAbortedSessionPing(pingData); + }, + + removeAbortedSessionPing: function() { + return TelemetryStorage.removeAbortedSessionPing(); + }, + + /** + * Perform telemetry initialization for either chrome or content process. + * @return {Boolean} True if Telemetry is allowed to record at least base (FHR) data, + * false otherwise. + */ + enableTelemetryRecording: function enableTelemetryRecording() { + // The thumbnail service also runs in a content process, even with e10s off. + // We need to check if e10s is on so we don't submit child payloads for it. + // We still need xpcshell child tests to work, so we skip this if test mode is enabled. + if (Utils.isContentProcess && !this._testMode && !Services.appinfo.browserTabsRemoteAutostart) { + this._log.config("enableTelemetryRecording - not enabling Telemetry for non-e10s child process"); + Telemetry.canRecordBase = false; + Telemetry.canRecordExtended = false; + return false; + } + + // Configure base Telemetry recording. + // Unified Telemetry makes it opt-out. If extended Telemetry is enabled, base recording + // is always on as well. + const enabled = Utils.isTelemetryEnabled; + Telemetry.canRecordBase = enabled || IS_UNIFIED_TELEMETRY; + Telemetry.canRecordExtended = enabled; + + this._log.config("enableTelemetryRecording - canRecordBase:" + Telemetry.canRecordBase + + ", canRecordExtended: " + Telemetry.canRecordExtended); + + return Telemetry.canRecordBase; + }, + + /** + * This triggers basic telemetry initialization and schedules a full initialized for later + * for performance reasons. + * + * This delayed initialization means TelemetryController init can be in the following states: + * 1) setupTelemetry was never called + * or it was called and + * 2) _delayedInitTask was scheduled, but didn't run yet. + * 3) _delayedInitTask is currently running. + * 4) _delayedInitTask finished running and is nulled out. + * + * @return {Promise} Resolved when TelemetryController and TelemetrySession are fully + * initialized. This is only used in tests. + */ + setupTelemetry: function setupTelemetry(testing) { + this._initStarted = true; + this._testMode = testing; + + this._log.trace("setupTelemetry"); + + if (this._delayedInitTask) { + this._log.error("setupTelemetry - init task already running"); + return this._delayedInitTaskDeferred.promise; + } + + if (this._initialized && !this._testMode) { + this._log.error("setupTelemetry - already initialized"); + return Promise.resolve(); + } + + // This will trigger displaying the datachoices infobar. + TelemetryReportingPolicy.setup(); + + if (!this.enableTelemetryRecording()) { + this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup."); + return Promise.resolve(); + } + + // Initialize the session recorder. + if (!this._sessionRecorder) { + this._sessionRecorder = new SessionRecorder(PREF_SESSIONS_BRANCH); + this._sessionRecorder.onStartup(); + } + + this._attachObservers(); + + // Perform a lightweight, early initialization for the component, just registering + // a few observers and initializing the session. + TelemetrySession.earlyInit(this._testMode); + + // For very short session durations, we may never load the client + // id from disk. + // We try to cache it in prefs to avoid this, even though this may + // lead to some stale client ids. + this._clientID = ClientID.getCachedClientID(); + + // Delay full telemetry initialization to give the browser time to + // run various late initializers. Otherwise our gathered memory + // footprint and other numbers would be too optimistic. + this._delayedInitTaskDeferred = Promise.defer(); + this._delayedInitTask = new DeferredTask(function* () { + try { + // TODO: This should probably happen after all the delayed init here. + this._initialized = true; + TelemetryEnvironment.delayedInit(); + + yield TelemetrySend.setup(this._testMode); + + // Load the ClientID. + this._clientID = yield ClientID.getClientID(); + + // Perform TelemetrySession delayed init. + yield TelemetrySession.delayedInit(); + // Purge the pings archive by removing outdated pings. We don't wait for + // this task to complete, but TelemetryStorage blocks on it during + // shutdown. + TelemetryStorage.runCleanPingArchiveTask(); + + // Now that FHR/healthreporter is gone, make sure to remove FHR's DB from + // the profile directory. This is a temporary measure that we should drop + // in the future. + TelemetryStorage.removeFHRDatabase(); + + this._delayedInitTaskDeferred.resolve(); + } catch (e) { + this._delayedInitTaskDeferred.reject(e); + } finally { + this._delayedInitTask = null; + } + }.bind(this), this._testMode ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY); + + AsyncShutdown.sendTelemetry.addBlocker("TelemetryController: shutting down", + () => this.shutdown(), + () => this._getState()); + + this._delayedInitTask.arm(); + return this._delayedInitTaskDeferred.promise; + }, + + /** + * This triggers basic telemetry initialization for content processes. + * @param {Boolean} [testing=false] True if we are in test mode, false otherwise. + */ + setupContentTelemetry: function (testing = false) { + this._testMode = testing; + + // We call |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags + // are in sync between chrome and content processes. + if (!this.enableTelemetryRecording()) { + this._log.trace("setupContentTelemetry - Content process recording disabled."); + return; + } + TelemetrySession.setupContent(testing); + }, + + // Do proper shutdown waiting and cleanup. + _cleanupOnShutdown: Task.async(function*() { + if (!this._initialized) { + return; + } + + Preferences.ignore(PREF_BRANCH_LOG, configureLogging); + this._detachObservers(); + + // Now do an orderly shutdown. + try { + // Stop the datachoices infobar display. + TelemetryReportingPolicy.shutdown(); + TelemetryEnvironment.shutdown(); + + // Stop any ping sending. + yield TelemetrySend.shutdown(); + + yield TelemetrySession.shutdown(); + + // First wait for clients processing shutdown. + yield this._shutdownBarrier.wait(); + + // ... and wait for any outstanding async ping activity. + yield this._connectionsBarrier.wait(); + + // Perform final shutdown operations. + yield TelemetryStorage.shutdown(); + } finally { + // Reset state. + this._initialized = false; + this._initStarted = false; + } + }), + + shutdown: function() { + this._log.trace("shutdown"); + + // We can be in one the following states here: + // 1) setupTelemetry was never called + // or it was called and + // 2) _delayedInitTask was scheduled, but didn't run yet. + // 3) _delayedInitTask is running now. + // 4) _delayedInitTask finished running already. + + // This handles 1). + if (!this._initStarted) { + return Promise.resolve(); + } + + // This handles 4). + if (!this._delayedInitTask) { + // We already ran the delayed initialization. + return this._cleanupOnShutdown(); + } + + // This handles 2) and 3). + return this._delayedInitTask.finalize().then(() => this._cleanupOnShutdown()); + }, + + /** + * This observer drives telemetry. + */ + observe: function (aSubject, aTopic, aData) { + // The logger might still be not available at this point. + if (aTopic == "profile-after-change" || aTopic == "app-startup") { + // If we don't have a logger, we need to make sure |Log.repository.getLogger()| is + // called before |getLoggerWithMessagePrefix|. Otherwise logging won't work. + configureLogging(); + } + + this._log.trace("observe - " + aTopic + " notified."); + + switch (aTopic) { + case "profile-after-change": + // profile-after-change is only registered for chrome processes. + return this.setupTelemetry(); + case "app-startup": + // app-startup is only registered for content processes. + return this.setupContentTelemetry(); + } + return undefined; + }, + + /** + * Get an object describing the current state of this module for AsyncShutdown diagnostics. + */ + _getState: function() { + return { + initialized: this._initialized, + initStarted: this._initStarted, + haveDelayedInitTask: !!this._delayedInitTask, + shutdownBarrier: this._shutdownBarrier.state, + connectionsBarrier: this._connectionsBarrier.state, + sendModule: TelemetrySend.getShutdownState(), + }; + }, + + /** + * Called whenever the FHR Upload preference changes (e.g. when user disables FHR from + * the preferences panel), this triggers sending the deletion ping. + */ + _onUploadPrefChange: function() { + const uploadEnabled = Preferences.get(PREF_FHR_UPLOAD_ENABLED, false); + if (uploadEnabled) { + // There's nothing we should do if we are enabling upload. + return; + } + + let p = Task.spawn(function*() { + try { + // Clear the current pings. + yield TelemetrySend.clearCurrentPings(); + + // Remove all the pending pings, but not the deletion ping. + yield TelemetryStorage.runRemovePendingPingsTask(); + } catch (e) { + this._log.error("_onUploadPrefChange - error clearing pending pings", e); + } finally { + // Always send the deletion ping. + this._log.trace("_onUploadPrefChange - Sending deletion ping."); + this.submitExternalPing(PING_TYPE_DELETION, {}, { addClientId: true }); + } + }.bind(this)); + + this._shutdownBarrier.client.addBlocker( + "TelemetryController: removing pending pings after data upload was disabled", p); + }, + + _attachObservers: function() { + if (IS_UNIFIED_TELEMETRY) { + // Watch the FHR upload setting to trigger deletion pings. + Preferences.observe(PREF_FHR_UPLOAD_ENABLED, this._onUploadPrefChange, this); + } + }, + + /** + * Remove the preference observer to avoid leaks. + */ + _detachObservers: function() { + if (IS_UNIFIED_TELEMETRY) { + Preferences.ignore(PREF_FHR_UPLOAD_ENABLED, this._onUploadPrefChange, this); + } + }, + + /** + * Allows waiting for TelemetryControllers delayed initialization to complete. + * This will complete before TelemetryController is shutting down. + * @return {Promise} Resolved when delayed TelemetryController initialization completed. + */ + promiseInitialized: function() { + return this._delayedInitTaskDeferred.promise; + }, + + getCurrentPingData: function(aSubsession) { + this._log.trace("getCurrentPingData - subsession: " + aSubsession) + + // Telemetry is disabled, don't gather any data. + if (!Telemetry.canRecordBase) { + return null; + } + + const reason = aSubsession ? REASON_GATHER_SUBSESSION_PAYLOAD : REASON_GATHER_PAYLOAD; + const type = PING_TYPE_MAIN; + const payload = TelemetrySession.getPayload(reason); + const options = { addClientId: true, addEnvironment: true }; + const ping = this.assemblePing(type, payload, options); + + return ping; + }, + + reset: Task.async(function*() { + this._clientID = null; + this._detachObservers(); + + yield TelemetrySession.testReset(); + + this._connectionsBarrier = new AsyncShutdown.Barrier( + "TelemetryController: Waiting for pending ping activity" + ); + this._shutdownBarrier = new AsyncShutdown.Barrier( + "TelemetryController: Waiting for clients." + ); + + // We need to kick of the controller setup first for tests that check the + // cached client id. + let controllerSetup = this.setupTelemetry(true); + + yield TelemetrySend.reset(); + yield TelemetryStorage.reset(); + yield TelemetryEnvironment.testReset(); + + yield controllerSetup; + }), +}; diff --git a/toolkit/components/telemetry/TelemetryEnvironment.jsm b/toolkit/components/telemetry/TelemetryEnvironment.jsm new file mode 100644 index 000000000..e2453649c --- /dev/null +++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm @@ -0,0 +1,1459 @@ +/* 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 = [ + "TelemetryEnvironment", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +const myScope = this; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/PromiseUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); +Cu.import("resource://gre/modules/ObjectUtils.jsm"); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const Utils = TelemetryUtils; + +XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode", + "resource:///modules/AttributionCode.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ctypes", + "resource://gre/modules/ctypes.jsm"); +if (AppConstants.platform !== "gonk") { + Cu.import("resource://gre/modules/AddonManager.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", + "resource://gre/modules/LightweightThemeManager.jsm"); +} +XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge", + "resource://gre/modules/ProfileAge.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", + "resource://gre/modules/WindowsRegistry.jsm"); + +// The maximum length of a string (e.g. description) in the addons section. +const MAX_ADDON_STRING_LENGTH = 100; +// The maximum length of a string value in the settings.attribution object. +const MAX_ATTRIBUTION_STRING_LENGTH = 100; + +/** + * This is a policy object used to override behavior for testing. + */ +var Policy = { + now: () => new Date(), +}; + +var gGlobalEnvironment; +function getGlobal() { + if (!gGlobalEnvironment) { + gGlobalEnvironment = new EnvironmentCache(); + } + return gGlobalEnvironment; +} + +this.TelemetryEnvironment = { + get currentEnvironment() { + return getGlobal().currentEnvironment; + }, + + onInitialized: function() { + return getGlobal().onInitialized(); + }, + + delayedInit: function() { + return getGlobal().delayedInit(); + }, + + registerChangeListener: function(name, listener) { + return getGlobal().registerChangeListener(name, listener); + }, + + unregisterChangeListener: function(name) { + return getGlobal().unregisterChangeListener(name); + }, + + shutdown: function() { + return getGlobal().shutdown(); + }, + + // Policy to use when saving preferences. Exported for using them in tests. + RECORD_PREF_STATE: 1, // Don't record the preference value + RECORD_PREF_VALUE: 2, // We only record user-set prefs. + + // Testing method + testWatchPreferences: function(prefMap) { + return getGlobal()._watchPreferences(prefMap); + }, + + /** + * Intended for use in tests only. + * + * In multiple tests we need a way to shut and re-start telemetry together + * with TelemetryEnvironment. This is problematic due to the fact that + * TelemetryEnvironment is a singleton. We, therefore, need this helper + * method to be able to re-set TelemetryEnvironment. + */ + testReset: function() { + return getGlobal().reset(); + }, + + /** + * Intended for use in tests only. + */ + testCleanRestart: function() { + getGlobal().shutdown(); + gGlobalEnvironment = null; + return getGlobal(); + }, +}; + +const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE; +const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE; +const DEFAULT_ENVIRONMENT_PREFS = new Map([ + ["app.feedback.baseURL", {what: RECORD_PREF_VALUE}], + ["app.support.baseURL", {what: RECORD_PREF_VALUE}], + ["accessibility.browsewithcaret", {what: RECORD_PREF_VALUE}], + ["accessibility.force_disabled", {what: RECORD_PREF_VALUE}], + ["app.update.auto", {what: RECORD_PREF_VALUE}], + ["app.update.enabled", {what: RECORD_PREF_VALUE}], + ["app.update.interval", {what: RECORD_PREF_VALUE}], + ["app.update.service.enabled", {what: RECORD_PREF_VALUE}], + ["app.update.silent", {what: RECORD_PREF_VALUE}], + ["app.update.url", {what: RECORD_PREF_VALUE}], + ["browser.cache.disk.enable", {what: RECORD_PREF_VALUE}], + ["browser.cache.disk.capacity", {what: RECORD_PREF_VALUE}], + ["browser.cache.memory.enable", {what: RECORD_PREF_VALUE}], + ["browser.cache.offline.enable", {what: RECORD_PREF_VALUE}], + ["browser.formfill.enable", {what: RECORD_PREF_VALUE}], + ["browser.newtab.url", {what: RECORD_PREF_STATE}], + ["browser.newtabpage.enabled", {what: RECORD_PREF_VALUE}], + ["browser.newtabpage.enhanced", {what: RECORD_PREF_VALUE}], + ["browser.shell.checkDefaultBrowser", {what: RECORD_PREF_VALUE}], + ["browser.search.suggest.enabled", {what: RECORD_PREF_VALUE}], + ["browser.startup.homepage", {what: RECORD_PREF_STATE}], + ["browser.startup.page", {what: RECORD_PREF_VALUE}], + ["browser.tabs.animate", {what: RECORD_PREF_VALUE}], + ["browser.urlbar.suggest.searches", {what: RECORD_PREF_VALUE}], + ["browser.urlbar.userMadeSearchSuggestionsChoice", {what: RECORD_PREF_VALUE}], + // Record "Zoom Text Only" pref in Firefox 50 to 52 (Bug 979323). + ["browser.zoom.full", {what: RECORD_PREF_VALUE}], + ["devtools.chrome.enabled", {what: RECORD_PREF_VALUE}], + ["devtools.debugger.enabled", {what: RECORD_PREF_VALUE}], + ["devtools.debugger.remote-enabled", {what: RECORD_PREF_VALUE}], + ["dom.ipc.plugins.asyncInit.enabled", {what: RECORD_PREF_VALUE}], + ["dom.ipc.plugins.enabled", {what: RECORD_PREF_VALUE}], + ["dom.ipc.processCount", {what: RECORD_PREF_VALUE, requiresRestart: true}], + ["dom.max_script_run_time", {what: RECORD_PREF_VALUE}], + ["experiments.manifest.uri", {what: RECORD_PREF_VALUE}], + ["extensions.autoDisableScopes", {what: RECORD_PREF_VALUE}], + ["extensions.enabledScopes", {what: RECORD_PREF_VALUE}], + ["extensions.blocklist.enabled", {what: RECORD_PREF_VALUE}], + ["extensions.blocklist.url", {what: RECORD_PREF_VALUE}], + ["extensions.strictCompatibility", {what: RECORD_PREF_VALUE}], + ["extensions.update.enabled", {what: RECORD_PREF_VALUE}], + ["extensions.update.url", {what: RECORD_PREF_VALUE}], + ["extensions.update.background.url", {what: RECORD_PREF_VALUE}], + ["general.smoothScroll", {what: RECORD_PREF_VALUE}], + ["gfx.direct2d.disabled", {what: RECORD_PREF_VALUE}], + ["gfx.direct2d.force-enabled", {what: RECORD_PREF_VALUE}], + ["gfx.direct2d.use1_1", {what: RECORD_PREF_VALUE}], + ["layers.acceleration.disabled", {what: RECORD_PREF_VALUE}], + ["layers.acceleration.force-enabled", {what: RECORD_PREF_VALUE}], + ["layers.async-pan-zoom.enabled", {what: RECORD_PREF_VALUE}], + ["layers.async-video-oop.enabled", {what: RECORD_PREF_VALUE}], + ["layers.async-video.enabled", {what: RECORD_PREF_VALUE}], + ["layers.componentalpha.enabled", {what: RECORD_PREF_VALUE}], + ["layers.d3d11.disable-warp", {what: RECORD_PREF_VALUE}], + ["layers.d3d11.force-warp", {what: RECORD_PREF_VALUE}], + ["layers.offmainthreadcomposition.force-disabled", {what: RECORD_PREF_VALUE}], + ["layers.prefer-d3d9", {what: RECORD_PREF_VALUE}], + ["layers.prefer-opengl", {what: RECORD_PREF_VALUE}], + ["layout.css.devPixelsPerPx", {what: RECORD_PREF_VALUE}], + ["network.proxy.autoconfig_url", {what: RECORD_PREF_STATE}], + ["network.proxy.http", {what: RECORD_PREF_STATE}], + ["network.proxy.ssl", {what: RECORD_PREF_STATE}], + ["pdfjs.disabled", {what: RECORD_PREF_VALUE}], + ["places.history.enabled", {what: RECORD_PREF_VALUE}], + ["privacy.trackingprotection.enabled", {what: RECORD_PREF_VALUE}], + ["privacy.donottrackheader.enabled", {what: RECORD_PREF_VALUE}], + ["services.sync.serverURL", {what: RECORD_PREF_STATE}], + ["security.mixed_content.block_active_content", {what: RECORD_PREF_VALUE}], + ["security.mixed_content.block_display_content", {what: RECORD_PREF_VALUE}], + ["security.sandbox.content.level", {what: RECORD_PREF_VALUE}], + ["xpinstall.signatures.required", {what: RECORD_PREF_VALUE}], +]); + +const LOGGER_NAME = "Toolkit.Telemetry"; + +const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled"; +const PREF_DISTRIBUTION_ID = "distribution.id"; +const PREF_DISTRIBUTION_VERSION = "distribution.version"; +const PREF_DISTRIBUTOR = "app.distributor"; +const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel"; +const PREF_HOTFIX_LASTVERSION = "extensions.hotfix.lastVersion"; +const PREF_APP_PARTNER_BRANCH = "app.partner."; +const PREF_PARTNER_ID = "mozilla.partner.id"; +const PREF_UPDATE_ENABLED = "app.update.enabled"; +const PREF_UPDATE_AUTODOWNLOAD = "app.update.auto"; +const PREF_SEARCH_COHORT = "browser.search.cohort"; +const PREF_E10S_COHORT = "e10s.rollout.cohort"; + +const COMPOSITOR_CREATED_TOPIC = "compositor:created"; +const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = "distribution-customization-complete"; +const EXPERIMENTS_CHANGED_TOPIC = "experiments-changed"; +const GFX_FEATURES_READY_TOPIC = "gfx-features-ready"; +const SEARCH_ENGINE_MODIFIED_TOPIC = "browser-search-engine-modified"; +const SEARCH_SERVICE_TOPIC = "browser-search-service"; + +/** + * Enforces the parameter to a boolean value. + * @param aValue The input value. + * @return {Boolean|Object} If aValue is a boolean or a number, returns its truthfulness + * value. Otherwise, return null. + */ +function enforceBoolean(aValue) { + if (typeof(aValue) !== "number" && typeof(aValue) !== "boolean") { + return null; + } + return (new Boolean(aValue)).valueOf(); +} + +/** + * Get the current browser. + * @return a string with the locale or null on failure. + */ +function getBrowserLocale() { + try { + return Cc["@mozilla.org/chrome/chrome-registry;1"]. + getService(Ci.nsIXULChromeRegistry). + getSelectedLocale('global'); + } catch (e) { + return null; + } +} + +/** + * Get the current OS locale. + * @return a string with the OS locale or null on failure. + */ +function getSystemLocale() { + try { + return Services.locale.getLocaleComponentForUserAgent(); + } catch (e) { + return null; + } +} + +/** + * Asynchronously get a list of addons of the specified type from the AddonManager. + * @param aTypes An array containing the types of addons to request. + * @return Promise resolved when AddonManager has finished, returning an + * array of addons. + */ +function promiseGetAddonsByTypes(aTypes) { + return new Promise((resolve) => + AddonManager.getAddonsByTypes(aTypes, (addons) => resolve(addons))); +} + +/** + * Safely get a sysinfo property and return its value. If the property is not + * available, return aDefault. + * + * @param aPropertyName the property name to get. + * @param aDefault the value to return if aPropertyName is not available. + * @return The property value, if available, or aDefault. + */ +function getSysinfoProperty(aPropertyName, aDefault) { + try { + // |getProperty| may throw if |aPropertyName| does not exist. + return Services.sysinfo.getProperty(aPropertyName); + } catch (e) {} + + return aDefault; +} + +/** + * Safely get a gfxInfo field and return its value. If the field is not available, return + * aDefault. + * + * @param aPropertyName the property name to get. + * @param aDefault the value to return if aPropertyName is not available. + * @return The property value, if available, or aDefault. + */ +function getGfxField(aPropertyName, aDefault) { + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + try { + // Accessing the field may throw if |aPropertyName| does not exist. + let gfxProp = gfxInfo[aPropertyName]; + if (gfxProp !== undefined && gfxProp !== "") { + return gfxProp; + } + } catch (e) {} + + return aDefault; +} + +/** + * Returns a substring of the input string. + * + * @param {String} aString The input string. + * @param {Integer} aMaxLength The maximum length of the returned substring. If this is + * greater than the length of the input string, we return the whole input string. + * @return {String} The substring or null if the input string is null. + */ +function limitStringToLength(aString, aMaxLength) { + if (typeof(aString) !== "string") { + return null; + } + return aString.substring(0, aMaxLength); +} + +/** + * Force a value to be a string. + * Only if the value is null, null is returned instead. + */ +function forceToStringOrNull(aValue) { + if (aValue === null) { + return null; + } + + return String(aValue); +} + +/** + * Get the information about a graphic adapter. + * + * @param aSuffix A suffix to add to the properties names. + * @return An object containing the adapter properties. + */ +function getGfxAdapter(aSuffix = "") { + // Note that gfxInfo, and so getGfxField, might return "Unknown" for the RAM on failures, + // not null. + let memoryMB = parseInt(getGfxField("adapterRAM" + aSuffix, null), 10); + if (Number.isNaN(memoryMB)) { + memoryMB = null; + } + + return { + description: getGfxField("adapterDescription" + aSuffix, null), + vendorID: getGfxField("adapterVendorID" + aSuffix, null), + deviceID: getGfxField("adapterDeviceID" + aSuffix, null), + subsysID: getGfxField("adapterSubsysID" + aSuffix, null), + RAM: memoryMB, + driver: getGfxField("adapterDriver" + aSuffix, null), + driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null), + driverDate: getGfxField("adapterDriverDate" + aSuffix, null), + }; +} + +/** + * Gets the service pack and build information on Windows platforms. The initial version + * was copied from nsUpdateService.js. + * + * @return An object containing the service pack major and minor versions, along with the + * build number. + */ +function getWindowsVersionInfo() { + const UNKNOWN_VERSION_INFO = {servicePackMajor: null, servicePackMinor: null, buildNumber: null}; + + if (AppConstants.platform !== "win") { + return UNKNOWN_VERSION_INFO; + } + + const BYTE = ctypes.uint8_t; + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + const WCHAR = ctypes.char16_t; + const BOOL = ctypes.int; + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx + const SZCSDVERSIONLENGTH = 128; + const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', + [ + {dwOSVersionInfoSize: DWORD}, + {dwMajorVersion: DWORD}, + {dwMinorVersion: DWORD}, + {dwBuildNumber: DWORD}, + {dwPlatformId: DWORD}, + {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, + {wServicePackMajor: WORD}, + {wServicePackMinor: WORD}, + {wSuiteMask: WORD}, + {wProductType: BYTE}, + {wReserved: BYTE} + ]); + + let kernel32 = ctypes.open("kernel32"); + try { + let GetVersionEx = kernel32.declare("GetVersionExW", + ctypes.default_abi, + BOOL, + OSVERSIONINFOEXW.ptr); + let winVer = OSVERSIONINFOEXW(); + winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; + + if (0 === GetVersionEx(winVer.address())) { + throw ("Failure in GetVersionEx (returned 0)"); + } + + return { + servicePackMajor: winVer.wServicePackMajor, + servicePackMinor: winVer.wServicePackMinor, + buildNumber: winVer.dwBuildNumber, + }; + } catch (e) { + return UNKNOWN_VERSION_INFO; + } finally { + kernel32.close(); + } +} + +/** + * Encapsulates the asynchronous magic interfacing with the addon manager. The builder + * is owned by a parent environment object and is an addon listener. + */ +function EnvironmentAddonBuilder(environment) { + this._environment = environment; + + // The pending task blocks addon manager shutdown. It can either be the initial load + // or a change load. + this._pendingTask = null; + + // Set to true once initial load is complete and we're watching for changes. + this._loaded = false; +} +EnvironmentAddonBuilder.prototype = { + /** + * Get the initial set of addons. + * @returns Promise when the initial load is complete. + */ + init: function() { + // Some tests don't initialize the addon manager. This accounts for the + // unfortunate reality of life. + try { + AddonManager.shutdown.addBlocker("EnvironmentAddonBuilder", + () => this._shutdownBlocker()); + } catch (err) { + return Promise.reject(err); + } + + this._pendingTask = this._updateAddons().then( + () => { this._pendingTask = null; }, + (err) => { + this._environment._log.error("init - Exception in _updateAddons", err); + this._pendingTask = null; + } + ); + + return this._pendingTask; + }, + + /** + * Register an addon listener and watch for changes. + */ + watchForChanges: function() { + this._loaded = true; + AddonManager.addAddonListener(this); + Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false); + }, + + // AddonListener + onEnabled: function() { + this._onAddonChange(); + }, + onDisabled: function() { + this._onAddonChange(); + }, + onInstalled: function() { + this._onAddonChange(); + }, + onUninstalling: function() { + this._onAddonChange(); + }, + + _onAddonChange: function() { + this._environment._log.trace("_onAddonChange"); + this._checkForChanges("addons-changed"); + }, + + // nsIObserver + observe: function (aSubject, aTopic, aData) { + this._environment._log.trace("observe - Topic " + aTopic); + this._checkForChanges("experiment-changed"); + }, + + _checkForChanges: function(changeReason) { + if (this._pendingTask) { + this._environment._log.trace("_checkForChanges - task already pending, dropping change with reason " + changeReason); + return; + } + + this._pendingTask = this._updateAddons().then( + (result) => { + this._pendingTask = null; + if (result.changed) { + this._environment._onEnvironmentChange(changeReason, result.oldEnvironment); + } + }, + (err) => { + this._pendingTask = null; + this._environment._log.error("_checkForChanges: Error collecting addons", err); + }); + }, + + _shutdownBlocker: function() { + if (this._loaded) { + AddonManager.removeAddonListener(this); + Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC); + } + return this._pendingTask; + }, + + /** + * Collect the addon data for the environment. + * + * This should only be called from _pendingTask; otherwise we risk + * running this during addon manager shutdown. + * + * @returns Promise This returns a Promise resolved with a status object with the following members: + * changed - Whether the environment changed. + * oldEnvironment - Only set if a change occured, contains the environment data before the change. + */ + _updateAddons: Task.async(function* () { + this._environment._log.trace("_updateAddons"); + let personaId = null; + if (AppConstants.platform !== "gonk") { + let theme = LightweightThemeManager.currentTheme; + if (theme) { + personaId = theme.id; + } + } + + let addons = { + activeAddons: yield this._getActiveAddons(), + theme: yield this._getActiveTheme(), + activePlugins: this._getActivePlugins(), + activeGMPlugins: yield this._getActiveGMPlugins(), + activeExperiment: this._getActiveExperiment(), + persona: personaId, + }; + + let result = { + changed: !this._environment._currentEnvironment.addons || + !ObjectUtils.deepEqual(addons, this._environment._currentEnvironment.addons), + }; + + if (result.changed) { + this._environment._log.trace("_updateAddons: addons differ"); + result.oldEnvironment = Cu.cloneInto(this._environment._currentEnvironment, myScope); + this._environment._currentEnvironment.addons = addons; + } + + return result; + }), + + /** + * Get the addon data in object form. + * @return Promise containing the addon data. + */ + _getActiveAddons: Task.async(function* () { + // Request addons, asynchronously. + let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]); + + let activeAddons = {}; + for (let addon of allAddons) { + // Skip addons which are not active. + if (!addon.isActive) { + continue; + } + + // Weird addon data in the wild can lead to exceptions while collecting + // the data. + try { + // Make sure to have valid dates. + let installDate = new Date(Math.max(0, addon.installDate)); + let updateDate = new Date(Math.max(0, addon.updateDate)); + + activeAddons[addon.id] = { + blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED), + description: limitStringToLength(addon.description, MAX_ADDON_STRING_LENGTH), + name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH), + userDisabled: enforceBoolean(addon.userDisabled), + appDisabled: addon.appDisabled, + version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH), + scope: addon.scope, + type: addon.type, + foreignInstall: enforceBoolean(addon.foreignInstall), + hasBinaryComponents: addon.hasBinaryComponents, + installDay: Utils.millisecondsToDays(installDate.getTime()), + updateDay: Utils.millisecondsToDays(updateDate.getTime()), + signedState: addon.signedState, + isSystem: addon.isSystem, + }; + + if (addon.signedState !== undefined) + activeAddons[addon.id].signedState = addon.signedState; + + } catch (ex) { + this._environment._log.error("_getActiveAddons - An addon was discarded due to an error", ex); + continue; + } + } + + return activeAddons; + }), + + /** + * Get the currently active theme data in object form. + * @return Promise containing the active theme data. + */ + _getActiveTheme: Task.async(function* () { + // Request themes, asynchronously. + let themes = yield promiseGetAddonsByTypes(["theme"]); + + let activeTheme = {}; + // We only store information about the active theme. + let theme = themes.find(theme => theme.isActive); + if (theme) { + // Make sure to have valid dates. + let installDate = new Date(Math.max(0, theme.installDate)); + let updateDate = new Date(Math.max(0, theme.updateDate)); + + activeTheme = { + id: theme.id, + blocklisted: (theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED), + description: limitStringToLength(theme.description, MAX_ADDON_STRING_LENGTH), + name: limitStringToLength(theme.name, MAX_ADDON_STRING_LENGTH), + userDisabled: enforceBoolean(theme.userDisabled), + appDisabled: theme.appDisabled, + version: limitStringToLength(theme.version, MAX_ADDON_STRING_LENGTH), + scope: theme.scope, + foreignInstall: enforceBoolean(theme.foreignInstall), + hasBinaryComponents: theme.hasBinaryComponents, + installDay: Utils.millisecondsToDays(installDate.getTime()), + updateDay: Utils.millisecondsToDays(updateDate.getTime()), + }; + } + + return activeTheme; + }), + + /** + * Get the plugins data in object form. + * @return Object containing the plugins data. + */ + _getActivePlugins: function () { + let pluginTags = + Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost).getPluginTags({}); + + let activePlugins = []; + for (let tag of pluginTags) { + // Skip plugins which are not active. + if (tag.disabled) { + continue; + } + + try { + // Make sure to have a valid date. + let updateDate = new Date(Math.max(0, tag.lastModifiedTime)); + + activePlugins.push({ + name: limitStringToLength(tag.name, MAX_ADDON_STRING_LENGTH), + version: limitStringToLength(tag.version, MAX_ADDON_STRING_LENGTH), + description: limitStringToLength(tag.description, MAX_ADDON_STRING_LENGTH), + blocklisted: tag.blocklisted, + disabled: tag.disabled, + clicktoplay: tag.clicktoplay, + mimeTypes: tag.getMimeTypes({}), + updateDay: Utils.millisecondsToDays(updateDate.getTime()), + }); + } catch (ex) { + this._environment._log.error("_getActivePlugins - A plugin was discarded due to an error", ex); + continue; + } + } + + return activePlugins; + }, + + /** + * Get the GMPlugins data in object form. + * @return Object containing the GMPlugins data. + * + * This should only be called from _pendingTask; otherwise we risk + * running this during addon manager shutdown. + */ + _getActiveGMPlugins: Task.async(function* () { + // Request plugins, asynchronously. + let allPlugins = yield promiseGetAddonsByTypes(["plugin"]); + + let activeGMPlugins = {}; + for (let plugin of allPlugins) { + // Only get info for active GMplugins. + if (!plugin.isGMPlugin || !plugin.isActive) { + continue; + } + + try { + activeGMPlugins[plugin.id] = { + version: plugin.version, + userDisabled: enforceBoolean(plugin.userDisabled), + applyBackgroundUpdates: plugin.applyBackgroundUpdates, + }; + } catch (ex) { + this._environment._log.error("_getActiveGMPlugins - A GMPlugin was discarded due to an error", ex); + continue; + } + } + + return activeGMPlugins; + }), + + /** + * Get the active experiment data in object form. + * @return Object containing the active experiment data. + */ + _getActiveExperiment: function () { + let experimentInfo = {}; + try { + let scope = {}; + Cu.import("resource:///modules/experiments/Experiments.jsm", scope); + let experiments = scope.Experiments.instance(); + let activeExperiment = experiments.getActiveExperimentID(); + if (activeExperiment) { + experimentInfo.id = activeExperiment; + experimentInfo.branch = experiments.getActiveExperimentBranch(); + } + } catch (e) { + // If this is not Firefox, the import will fail. + } + + return experimentInfo; + }, +}; + +function EnvironmentCache() { + this._log = Log.repository.getLoggerWithMessagePrefix( + LOGGER_NAME, "TelemetryEnvironment::"); + this._log.trace("constructor"); + + this._shutdown = false; + this._delayedInitFinished = false; + + // A map of listeners that will be called on environment changes. + this._changeListeners = new Map(); + + // A map of watched preferences which trigger an Environment change when + // modified. Every entry contains a recording policy (RECORD_PREF_*). + this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS; + + this._currentEnvironment = { + build: this._getBuild(), + partner: this._getPartner(), + system: this._getSystem(), + }; + + this._updateSettings(); + // Fill in the default search engine, if the search provider is already initialized. + this._updateSearchEngine(); + this._addObservers(); + + // Build the remaining asynchronous parts of the environment. Don't register change listeners + // until the initial environment has been built. + + let p = []; + if (AppConstants.platform === "gonk") { + this._addonBuilder = { + watchForChanges: function() {} + }; + } else { + this._addonBuilder = new EnvironmentAddonBuilder(this); + p = [ this._addonBuilder.init() ]; + } + + this._currentEnvironment.profile = {}; + p.push(this._updateProfile()); + if (AppConstants.MOZ_BUILD_APP == "browser") { + p.push(this._updateAttribution()); + } + + let setup = () => { + this._initTask = null; + this._startWatchingPrefs(); + this._addonBuilder.watchForChanges(); + this._updateGraphicsFeatures(); + return this.currentEnvironment; + }; + + this._initTask = Promise.all(p) + .then( + () => setup(), + (err) => { + // log errors but eat them for consumers + this._log.error("EnvironmentCache - error while initializing", err); + return setup(); + }); +} +EnvironmentCache.prototype = { + /** + * The current environment data. The returned data is cloned to avoid + * unexpected sharing or mutation. + * @returns object + */ + get currentEnvironment() { + return Cu.cloneInto(this._currentEnvironment, myScope); + }, + + /** + * Wait for the current enviroment to be fully initialized. + * @returns Promise + */ + onInitialized: function() { + if (this._initTask) { + return this._initTask; + } + return Promise.resolve(this.currentEnvironment); + }, + + /** + * This gets called when the delayed init completes. + */ + delayedInit: function() { + this._delayedInitFinished = true; + }, + + /** + * Register a listener for environment changes. + * @param name The name of the listener. If a new listener is registered + * with the same name, the old listener will be replaced. + * @param listener function(reason, oldEnvironment) - Will receive a reason for + the change and the environment data before the change. + */ + registerChangeListener: function (name, listener) { + this._log.trace("registerChangeListener for " + name); + if (this._shutdown) { + this._log.warn("registerChangeListener - already shutdown"); + return; + } + this._changeListeners.set(name, listener); + }, + + /** + * Unregister from listening to environment changes. + * It's fine to call this on an unitialized TelemetryEnvironment. + * @param name The name of the listener to remove. + */ + unregisterChangeListener: function (name) { + this._log.trace("unregisterChangeListener for " + name); + if (this._shutdown) { + this._log.warn("registerChangeListener - already shutdown"); + return; + } + this._changeListeners.delete(name); + }, + + shutdown: function() { + this._log.trace("shutdown"); + this._shutdown = true; + }, + + /** + * Only used in tests, set the preferences to watch. + * @param aPreferences A map of preferences names and their recording policy. + */ + _watchPreferences: function (aPreferences) { + this._stopWatchingPrefs(); + this._watchedPrefs = aPreferences; + this._updateSettings(); + this._startWatchingPrefs(); + }, + + /** + * Get an object containing the values for the watched preferences. Depending on the + * policy, the value for a preference or whether it was changed by user is reported. + * + * @return An object containing the preferences values. + */ + _getPrefData: function () { + let prefData = {}; + for (let [pref, policy] of this._watchedPrefs.entries()) { + // Only record preferences if they are non-default + if (!Preferences.isSet(pref)) { + continue; + } + + // Check the policy for the preference and decide if we need to store its value + // or whether it changed from the default value. + let prefValue = undefined; + if (policy.what == TelemetryEnvironment.RECORD_PREF_STATE) { + prefValue = ""; + } else { + prefValue = Preferences.get(pref, null); + } + prefData[pref] = prefValue; + } + return prefData; + }, + + /** + * Start watching the preferences. + */ + _startWatchingPrefs: function () { + this._log.trace("_startWatchingPrefs - " + this._watchedPrefs); + + for (let [pref, options] of this._watchedPrefs) { + if (!("requiresRestart" in options) || !options.requiresRestart) { + Preferences.observe(pref, this._onPrefChanged, this); + } + } + }, + + _onPrefChanged: function() { + this._log.trace("_onPrefChanged"); + let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope); + this._updateSettings(); + this._onEnvironmentChange("pref-changed", oldEnvironment); + }, + + /** + * Do not receive any more change notifications for the preferences. + */ + _stopWatchingPrefs: function () { + this._log.trace("_stopWatchingPrefs"); + + for (let [pref, options] of this._watchedPrefs) { + if (!("requiresRestart" in options) || !options.requiresRestart) { + Preferences.ignore(pref, this._onPrefChanged, this); + } + } + }, + + _addObservers: function () { + // Watch the search engine change and service topics. + Services.obs.addObserver(this, COMPOSITOR_CREATED_TOPIC, false); + Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC, false); + Services.obs.addObserver(this, GFX_FEATURES_READY_TOPIC, false); + Services.obs.addObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC, false); + Services.obs.addObserver(this, SEARCH_SERVICE_TOPIC, false); + }, + + _removeObservers: function () { + Services.obs.removeObserver(this, COMPOSITOR_CREATED_TOPIC); + try { + Services.obs.removeObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC); + } catch (ex) {} + Services.obs.removeObserver(this, GFX_FEATURES_READY_TOPIC); + Services.obs.removeObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC); + Services.obs.removeObserver(this, SEARCH_SERVICE_TOPIC); + }, + + observe: function (aSubject, aTopic, aData) { + this._log.trace("observe - aTopic: " + aTopic + ", aData: " + aData); + switch (aTopic) { + case SEARCH_ENGINE_MODIFIED_TOPIC: + if (aData != "engine-current") { + return; + } + // Record the new default search choice and send the change notification. + this._onSearchEngineChange(); + break; + case SEARCH_SERVICE_TOPIC: + if (aData != "init-complete") { + return; + } + // Now that the search engine init is complete, record the default search choice. + this._updateSearchEngine(); + break; + case GFX_FEATURES_READY_TOPIC: + case COMPOSITOR_CREATED_TOPIC: + // Full graphics information is not available until we have created at + // least one off-main-thread-composited window. Thus we wait for the + // first compositor to be created and then query nsIGfxInfo again. + this._updateGraphicsFeatures(); + break; + case DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC: + // Distribution customizations are applied after final-ui-startup. query + // partner prefs again when they are ready. + this._updatePartner(); + Services.obs.removeObserver(this, aTopic); + break; + } + }, + + /** + * Get the default search engine. + * @return {String} Returns the search engine identifier, "NONE" if no default search + * engine is defined or "UNDEFINED" if no engine identifier or name can be found. + */ + _getDefaultSearchEngine: function () { + let engine; + try { + engine = Services.search.defaultEngine; + } catch (e) {} + + let name; + if (!engine) { + name = "NONE"; + } else if (engine.identifier) { + name = engine.identifier; + } else if (engine.name) { + name = "other-" + engine.name; + } else { + name = "UNDEFINED"; + } + + return name; + }, + + /** + * Update the default search engine value. + */ + _updateSearchEngine: function () { + if (!Services.search) { + // Just ignore cases where the search service is not implemented. + return; + } + + this._log.trace("_updateSearchEngine - isInitialized: " + Services.search.isInitialized); + if (!Services.search.isInitialized) { + return; + } + + // Make sure we have a settings section. + this._currentEnvironment.settings = this._currentEnvironment.settings || {}; + // Update the search engine entry in the current environment. + this._currentEnvironment.settings.defaultSearchEngine = this._getDefaultSearchEngine(); + this._currentEnvironment.settings.defaultSearchEngineData = + Services.search.getDefaultEngineInfo(); + + // Record the cohort identifier used for search defaults A/B testing. + if (Services.prefs.prefHasUserValue(PREF_SEARCH_COHORT)) + this._currentEnvironment.settings.searchCohort = Services.prefs.getCharPref(PREF_SEARCH_COHORT); + }, + + /** + * Update the default search engine value and trigger the environment change. + */ + _onSearchEngineChange: function () { + this._log.trace("_onSearchEngineChange"); + + // Finally trigger the environment change notification. + let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope); + this._updateSearchEngine(); + this._onEnvironmentChange("search-engine-changed", oldEnvironment); + }, + + /** + * Update the graphics features object. + */ + _updateGraphicsFeatures: function () { + let gfxData = this._currentEnvironment.system.gfx; + try { + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + gfxData.features = gfxInfo.getFeatures(); + } catch (e) { + this._log.error("nsIGfxInfo.getFeatures() caught error", e); + } + }, + + /** + * Update the partner prefs. + */ + _updatePartner: function() { + this._currentEnvironment.partner = this._getPartner(); + }, + + /** + * Get the build data in object form. + * @return Object containing the build data. + */ + _getBuild: function () { + let buildData = { + applicationId: Services.appinfo.ID || null, + applicationName: Services.appinfo.name || null, + architecture: Services.sysinfo.get("arch"), + buildId: Services.appinfo.appBuildID || null, + version: Services.appinfo.version || null, + vendor: Services.appinfo.vendor || null, + platformVersion: Services.appinfo.platformVersion || null, + xpcomAbi: Services.appinfo.XPCOMABI, + hotfixVersion: Preferences.get(PREF_HOTFIX_LASTVERSION, null), + }; + + // Add |architecturesInBinary| only for Mac Universal builds. + if ("@mozilla.org/xpcom/mac-utils;1" in Cc) { + let macUtils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils); + if (macUtils && macUtils.isUniversalBinary) { + buildData.architecturesInBinary = macUtils.architecturesInBinary; + } + } + + return buildData; + }, + + /** + * Determine if we're the default browser. + * @returns null on error, true if we are the default browser, or false otherwise. + */ + _isDefaultBrowser: function () { + if (AppConstants.platform === "gonk") { + return true; + } + + if (!("@mozilla.org/browser/shell-service;1" in Cc)) { + this._log.info("_isDefaultBrowser - Could not obtain browser shell service"); + return null; + } + + let shellService; + try { + let scope = {}; + Cu.import("resource:///modules/ShellService.jsm", scope); + shellService = scope.ShellService; + } catch (ex) { + this._log.error("_isDefaultBrowser - Could not obtain shell service JSM"); + } + + if (!shellService) { + try { + shellService = Cc["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService); + } catch (ex) { + this._log.error("_isDefaultBrowser - Could not obtain shell service", ex); + return null; + } + } + + try { + // This uses the same set of flags used by the pref pane. + return shellService.isDefaultBrowser(false, true) ? true : false; + } catch (ex) { + this._log.error("_isDefaultBrowser - Could not determine if default browser", ex); + return null; + } + }, + + /** + * Update the cached settings data. + */ + _updateSettings: function () { + let updateChannel = null; + try { + updateChannel = UpdateUtils.getUpdateChannel(false); + } catch (e) {} + + this._currentEnvironment.settings = { + blocklistEnabled: Preferences.get(PREF_BLOCKLIST_ENABLED, true), + e10sEnabled: Services.appinfo.browserTabsRemoteAutostart, + e10sCohort: Preferences.get(PREF_E10S_COHORT, "unknown"), + telemetryEnabled: Utils.isTelemetryEnabled, + locale: getBrowserLocale(), + update: { + channel: updateChannel, + enabled: Preferences.get(PREF_UPDATE_ENABLED, true), + autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true), + }, + userPrefs: this._getPrefData(), + }; + + if (AppConstants.platform !== "gonk") { + this._currentEnvironment.settings.addonCompatibilityCheckEnabled = + AddonManager.checkCompatibility; + } + + if (AppConstants.platform !== "android") { + this._currentEnvironment.settings.isDefaultBrowser = + this._isDefaultBrowser(); + } + + this._updateSearchEngine(); + }, + + /** + * Update the cached profile data. + * @returns Promise<> resolved when the I/O is complete. + */ + _updateProfile: Task.async(function* () { + const logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "ProfileAge - "); + let profileAccessor = new ProfileAge(null, logger); + + let creationDate = yield profileAccessor.created; + let resetDate = yield profileAccessor.reset; + + this._currentEnvironment.profile.creationDate = + Utils.millisecondsToDays(creationDate); + if (resetDate) { + this._currentEnvironment.profile.resetDate = + Utils.millisecondsToDays(resetDate); + } + }), + + /** + * Update the cached attribution data object. + * @returns Promise<> resolved when the I/O is complete. + */ + _updateAttribution: Task.async(function* () { + let data = yield AttributionCode.getAttrDataAsync(); + if (Object.keys(data).length > 0) { + this._currentEnvironment.settings.attribution = {}; + for (let key in data) { + this._currentEnvironment.settings.attribution[key] = + limitStringToLength(data[key], MAX_ATTRIBUTION_STRING_LENGTH); + } + } + }), + + /** + * Get the partner data in object form. + * @return Object containing the partner data. + */ + _getPartner: function () { + let partnerData = { + distributionId: Preferences.get(PREF_DISTRIBUTION_ID, null), + distributionVersion: Preferences.get(PREF_DISTRIBUTION_VERSION, null), + partnerId: Preferences.get(PREF_PARTNER_ID, null), + distributor: Preferences.get(PREF_DISTRIBUTOR, null), + distributorChannel: Preferences.get(PREF_DISTRIBUTOR_CHANNEL, null), + }; + + // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data. + let partnerBranch = Services.prefs.getBranch(PREF_APP_PARTNER_BRANCH); + partnerData.partnerNames = partnerBranch.getChildList(""); + + return partnerData; + }, + + /** + * Get the CPU information. + * @return Object containing the CPU information data. + */ + _getCpuData: function () { + let cpuData = { + count: getSysinfoProperty("cpucount", null), + cores: getSysinfoProperty("cpucores", null), + vendor: getSysinfoProperty("cpuvendor", null), + family: getSysinfoProperty("cpufamily", null), + model: getSysinfoProperty("cpumodel", null), + stepping: getSysinfoProperty("cpustepping", null), + l2cacheKB: getSysinfoProperty("cpucachel2", null), + l3cacheKB: getSysinfoProperty("cpucachel3", null), + speedMHz: getSysinfoProperty("cpuspeed", null), + }; + + const CPU_EXTENSIONS = ["hasMMX", "hasSSE", "hasSSE2", "hasSSE3", "hasSSSE3", + "hasSSE4A", "hasSSE4_1", "hasSSE4_2", "hasAVX", "hasAVX2", + "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON"]; + + // Enumerate the available CPU extensions. + let availableExts = []; + for (let ext of CPU_EXTENSIONS) { + if (getSysinfoProperty(ext, false)) { + availableExts.push(ext); + } + } + + cpuData.extensions = availableExts; + + return cpuData; + }, + + /** + * Get the device information, if we are on a portable device. + * @return Object containing the device information data, or null if + * not a portable device. + */ + _getDeviceData: function () { + if (!["gonk", "android"].includes(AppConstants.platform)) { + return null; + } + + return { + model: getSysinfoProperty("device", null), + manufacturer: getSysinfoProperty("manufacturer", null), + hardware: getSysinfoProperty("hardware", null), + isTablet: getSysinfoProperty("tablet", null), + }; + }, + + /** + * Get the OS information. + * @return Object containing the OS data. + */ + _getOSData: function () { + let data = { + name: forceToStringOrNull(getSysinfoProperty("name", null)), + version: forceToStringOrNull(getSysinfoProperty("version", null)), + locale: forceToStringOrNull(getSystemLocale()), + }; + + if (["gonk", "android"].includes(AppConstants.platform)) { + data.kernelVersion = forceToStringOrNull(getSysinfoProperty("kernel_version", null)); + } else if (AppConstants.platform === "win") { + // The path to the "UBR" key, queried to get additional version details on Windows. + const WINDOWS_UBR_KEY_PATH = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; + + let versionInfo = getWindowsVersionInfo(); + data.servicePackMajor = versionInfo.servicePackMajor; + data.servicePackMinor = versionInfo.servicePackMinor; + // We only need the build number and UBR if we're at or above Windows 10. + if (typeof(data.version) === 'string' && + Services.vc.compare(data.version, "10") >= 0) { + data.windowsBuildNumber = versionInfo.buildNumber; + // Query the UBR key and only add it to the environment if it's available. + // |readRegKey| doesn't throw, but rather returns 'undefined' on error. + let ubr = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + WINDOWS_UBR_KEY_PATH, "UBR", + Ci.nsIWindowsRegKey.WOW64_64); + data.windowsUBR = (ubr !== undefined) ? ubr : null; + } + data.installYear = getSysinfoProperty("installYear", null); + } + + return data; + }, + + /** + * Get the HDD information. + * @return Object containing the HDD data. + */ + _getHDDData: function () { + return { + profile: { // hdd where the profile folder is located + model: getSysinfoProperty("profileHDDModel", null), + revision: getSysinfoProperty("profileHDDRevision", null), + }, + binary: { // hdd where the application binary is located + model: getSysinfoProperty("binHDDModel", null), + revision: getSysinfoProperty("binHDDRevision", null), + }, + system: { // hdd where the system files are located + model: getSysinfoProperty("winHDDModel", null), + revision: getSysinfoProperty("winHDDRevision", null), + }, + }; + }, + + /** + * Get the GFX information. + * @return Object containing the GFX data. + */ + _getGFXData: function () { + let gfxData = { + D2DEnabled: getGfxField("D2DEnabled", null), + DWriteEnabled: getGfxField("DWriteEnabled", null), + ContentBackend: getGfxField("ContentBackend", null), + // The following line is disabled due to main thread jank and will be enabled + // again as part of bug 1154500. + // DWriteVersion: getGfxField("DWriteVersion", null), + adapters: [], + monitors: [], + features: {}, + }; + + if (!["gonk", "android", "linux"].includes(AppConstants.platform)) { + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + try { + gfxData.monitors = gfxInfo.getMonitors(); + } catch (e) { + this._log.error("nsIGfxInfo.getMonitors() caught error", e); + } + } + + try { + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + gfxData.features = gfxInfo.getFeatures(); + } catch (e) { + this._log.error("nsIGfxInfo.getFeatures() caught error", e); + } + + // GfxInfo does not yet expose a way to iterate through all the adapters. + gfxData.adapters.push(getGfxAdapter("")); + gfxData.adapters[0].GPUActive = true; + + // If we have a second adapter add it to the gfxData.adapters section. + let hasGPU2 = getGfxField("adapterDeviceID2", null) !== null; + if (!hasGPU2) { + this._log.trace("_getGFXData - Only one display adapter detected."); + return gfxData; + } + + this._log.trace("_getGFXData - Two display adapters detected."); + + gfxData.adapters.push(getGfxAdapter("2")); + gfxData.adapters[1].GPUActive = getGfxField("isGPU2Active", null); + + return gfxData; + }, + + /** + * Get the system data in object form. + * @return Object containing the system data. + */ + _getSystem: function () { + let memoryMB = getSysinfoProperty("memsize", null); + if (memoryMB) { + // Send RAM size in megabytes. Rounding because sysinfo doesn't + // always provide RAM in multiples of 1024. + memoryMB = Math.round(memoryMB / 1024 / 1024); + } + + let virtualMB = getSysinfoProperty("virtualmemsize", null); + if (virtualMB) { + // Send the total virtual memory size in megabytes. Rounding because + // sysinfo doesn't always provide RAM in multiples of 1024. + virtualMB = Math.round(virtualMB / 1024 / 1024); + } + + let data = { + memoryMB: memoryMB, + virtualMaxMB: virtualMB, + cpu: this._getCpuData(), + os: this._getOSData(), + hdd: this._getHDDData(), + gfx: this._getGFXData(), + }; + + if (AppConstants.platform === "win") { + data.isWow64 = getSysinfoProperty("isWow64", null); + } else if (["gonk", "android"].includes(AppConstants.platform)) { + data.device = this._getDeviceData(); + } + + return data; + }, + + _onEnvironmentChange: function (what, oldEnvironment) { + this._log.trace("_onEnvironmentChange for " + what); + + // We are already skipping change events in _checkChanges if there is a pending change task running. + if (this._shutdown) { + this._log.trace("_onEnvironmentChange - Already shut down."); + return; + } + + for (let [name, listener] of this._changeListeners) { + try { + this._log.debug("_onEnvironmentChange - calling " + name); + listener(what, oldEnvironment); + } catch (e) { + this._log.error("_onEnvironmentChange - listener " + name + " caught error", e); + } + } + }, + + reset: function () { + this._shutdown = false; + this._delayedInitFinished = false; + } +}; diff --git a/toolkit/components/telemetry/TelemetryEvent.cpp b/toolkit/components/telemetry/TelemetryEvent.cpp new file mode 100644 index 000000000..1e8126f66 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryEvent.cpp @@ -0,0 +1,687 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include +#include "nsITelemetry.h" +#include "nsHashKeys.h" +#include "nsDataHashtable.h" +#include "nsClassHashtable.h" +#include "nsTArray.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Unused.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPtr.h" +#include "jsapi.h" +#include "nsJSUtils.h" +#include "nsXULAppAPI.h" +#include "nsUTF8Utils.h" + +#include "TelemetryCommon.h" +#include "TelemetryEvent.h" +#include "TelemetryEventData.h" + +using mozilla::StaticMutex; +using mozilla::StaticMutexAutoLock; +using mozilla::ArrayLength; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Pair; +using mozilla::StaticAutoPtr; +using mozilla::Telemetry::Common::AutoHashtable; +using mozilla::Telemetry::Common::IsExpiredVersion; +using mozilla::Telemetry::Common::CanRecordDataset; +using mozilla::Telemetry::Common::IsInDataset; +using mozilla::Telemetry::Common::MsSinceProcessStart; +using mozilla::Telemetry::Common::LogToBrowserConsole; + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// Naming: there are two kinds of functions in this file: +// +// * Functions taking a StaticMutexAutoLock: these can only be reached via +// an interface function (TelemetryEvent::*). They expect the interface +// function to have acquired |gTelemetryEventsMutex|, so they do not +// have to be thread-safe. +// +// * Functions named TelemetryEvent::*. This is the external interface. +// Entries and exits to these functions are serialised using +// |gTelemetryEventsMutex|. +// +// Avoiding races and deadlocks: +// +// All functions in the external interface (TelemetryEvent::*) are +// serialised using the mutex |gTelemetryEventsMutex|. This means +// that the external interface is thread-safe, and the internal +// functions can ignore thread safety. But it also brings a danger +// of deadlock if any function in the external interface can get back +// to that interface. That is, we will deadlock on any call chain like +// this: +// +// TelemetryEvent::* -> .. any functions .. -> TelemetryEvent::* +// +// To reduce the danger of that happening, observe the following rules: +// +// * No function in TelemetryEvent::* may directly call, nor take the +// address of, any other function in TelemetryEvent::*. +// +// * No internal function may call, nor take the address +// of, any function in TelemetryEvent::*. + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE TYPES + +namespace { + +const uint32_t kEventCount = mozilla::Telemetry::EventID::EventCount; +// This is a special event id used to mark expired events, to make expiry checks +// faster at runtime. +const uint32_t kExpiredEventId = kEventCount + 1; +static_assert(kEventCount < kExpiredEventId, "Should not overflow."); + +// This is the hard upper limit on the number of event records we keep in storage. +// If we cross this limit, we will drop any further event recording until elements +// are removed from storage. +const uint32_t kMaxEventRecords = 1000; +// Maximum length of any passed value string, in UTF8 byte sequence length. +const uint32_t kMaxValueByteLength = 80; +// Maximum length of any string value in the extra dictionary, in UTF8 byte sequence length. +const uint32_t kMaxExtraValueByteLength = 80; + +typedef nsDataHashtable EventMapType; +typedef nsClassHashtable StringMap; + +enum class RecordEventResult { + Ok, + UnknownEvent, + InvalidExtraKey, + StorageLimitReached, +}; + +struct ExtraEntry { + const nsCString key; + const nsCString value; +}; + +typedef nsTArray ExtraArray; + +class EventRecord { +public: + EventRecord(double timestamp, uint32_t eventId, const Maybe& value, + const ExtraArray& extra) + : mTimestamp(timestamp) + , mEventId(eventId) + , mValue(value) + , mExtra(extra) + {} + + EventRecord(const EventRecord& other) + : mTimestamp(other.mTimestamp) + , mEventId(other.mEventId) + , mValue(other.mValue) + , mExtra(other.mExtra) + {} + + EventRecord& operator=(const EventRecord& other) = delete; + + double Timestamp() const { return mTimestamp; } + uint32_t EventId() const { return mEventId; } + const Maybe& Value() const { return mValue; } + const ExtraArray& Extra() const { return mExtra; } + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + +private: + const double mTimestamp; + const uint32_t mEventId; + const Maybe mValue; + const ExtraArray mExtra; +}; + +// Implements the methods for EventInfo. +const char* +EventInfo::method() const +{ + return &gEventsStringTable[this->method_offset]; +} + +const char* +EventInfo::object() const +{ + return &gEventsStringTable[this->object_offset]; +} + +// Implements the methods for CommonEventInfo. +const char* +CommonEventInfo::category() const +{ + return &gEventsStringTable[this->category_offset]; +} + +const char* +CommonEventInfo::expiration_version() const +{ + return &gEventsStringTable[this->expiration_version_offset]; +} + +const char* +CommonEventInfo::extra_key(uint32_t index) const +{ + MOZ_ASSERT(index < this->extra_count); + uint32_t key_index = gExtraKeysTable[this->extra_index + index]; + return &gEventsStringTable[key_index]; +} + +// Implementation for the EventRecord class. +size_t +EventRecord::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + size_t n = 0; + + if (mValue) { + n += mValue.value().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + n += mExtra.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (uint32_t i = 0; i < mExtra.Length(); ++i) { + n += mExtra[i].key.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += mExtra[i].value.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + return n; +} + +nsCString +UniqueEventName(const nsACString& category, const nsACString& method, const nsACString& object) +{ + nsCString name; + name.Append(category); + name.AppendLiteral("#"); + name.Append(method); + name.AppendLiteral("#"); + name.Append(object); + return name; +} + +nsCString +UniqueEventName(const EventInfo& info) +{ + return UniqueEventName(nsDependentCString(info.common_info.category()), + nsDependentCString(info.method()), + nsDependentCString(info.object())); +} + +bool +IsExpiredDate(uint32_t expires_days_since_epoch) { + if (expires_days_since_epoch == 0) { + return false; + } + + const uint32_t days_since_epoch = PR_Now() / (PRTime(PR_USEC_PER_SEC) * 24 * 60 * 60); + return expires_days_since_epoch <= days_since_epoch; +} + +void +TruncateToByteLength(nsCString& str, uint32_t length) +{ + // last will be the index of the first byte of the current multi-byte sequence. + uint32_t last = RewindToPriorUTF8Codepoint(str.get(), length); + str.Truncate(last); +} + +} // anonymous namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE STATE, SHARED BY ALL THREADS + +namespace { + +// Set to true once this global state has been initialized. +bool gInitDone = false; + +bool gCanRecordBase; +bool gCanRecordExtended; + +// The Name -> ID cache map. +EventMapType gEventNameIDMap(kEventCount); + +// The main event storage. Events are inserted here in recording order. +StaticAutoPtr> gEventRecords; + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: thread-unsafe helpers for event recording. + +namespace { + +bool +CanRecordEvent(const StaticMutexAutoLock& lock, const CommonEventInfo& info) +{ + if (!gCanRecordBase) { + return false; + } + + return CanRecordDataset(info.dataset, gCanRecordBase, gCanRecordExtended); +} + +RecordEventResult +RecordEvent(const StaticMutexAutoLock& lock, double timestamp, + const nsACString& category, const nsACString& method, + const nsACString& object, const Maybe& value, + const ExtraArray& extra) +{ + // Apply hard limit on event count in storage. + if (gEventRecords->Length() >= kMaxEventRecords) { + return RecordEventResult::StorageLimitReached; + } + + // Look up the event id. + const nsCString& name = UniqueEventName(category, method, object); + uint32_t eventId; + if (!gEventNameIDMap.Get(name, &eventId)) { + return RecordEventResult::UnknownEvent; + } + + // If the event is expired, silently drop this call. + // We don't want recording for expired probes to be an error so code doesn't + // have to be removed at a specific time or version. + // Even logging warnings would become very noisy. + if (eventId == kExpiredEventId) { + return RecordEventResult::Ok; + } + + // Check whether we can record this event. + const CommonEventInfo& common = gEventInfo[eventId].common_info; + if (!CanRecordEvent(lock, common)) { + return RecordEventResult::Ok; + } + + // Check whether the extra keys passed are valid. + nsTHashtable validExtraKeys; + for (uint32_t i = 0; i < common.extra_count; ++i) { + validExtraKeys.PutEntry(nsDependentCString(common.extra_key(i))); + } + + for (uint32_t i = 0; i < extra.Length(); ++i) { + if (!validExtraKeys.GetEntry(extra[i].key)) { + return RecordEventResult::InvalidExtraKey; + } + } + + // Add event record. + gEventRecords->AppendElement(EventRecord(timestamp, eventId, value, extra)); + return RecordEventResult::Ok; +} + +} // anonymous namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents:: + +// This is a StaticMutex rather than a plain Mutex (1) so that +// it gets initialised in a thread-safe manner the first time +// it is used, and (2) because it is never de-initialised, and +// a normal Mutex would show up as a leak in BloatView. StaticMutex +// also has the "OffTheBooks" property, so it won't show as a leak +// in BloatView. +// Another reason to use a StaticMutex instead of a plain Mutex is +// that, due to the nature of Telemetry, we cannot rely on having a +// mutex initialized in InitializeGlobalState. Unfortunately, we +// cannot make sure that no other function is called before this point. +static StaticMutex gTelemetryEventsMutex; + +void +TelemetryEvent::InitializeGlobalState(bool aCanRecordBase, bool aCanRecordExtended) +{ + StaticMutexAutoLock locker(gTelemetryEventsMutex); + MOZ_ASSERT(!gInitDone, "TelemetryEvent::InitializeGlobalState " + "may only be called once"); + + gCanRecordBase = aCanRecordBase; + gCanRecordExtended = aCanRecordExtended; + + gEventRecords = new nsTArray(); + + // Populate the static event name->id cache. Note that the event names are + // statically allocated and come from the automatically generated TelemetryEventData.h. + const uint32_t eventCount = static_cast(mozilla::Telemetry::EventID::EventCount); + for (uint32_t i = 0; i < eventCount; ++i) { + const EventInfo& info = gEventInfo[i]; + uint32_t eventId = i; + + // If this event is expired, mark it with a special event id. + // This avoids doing expensive expiry checks at runtime. + if (IsExpiredVersion(info.common_info.expiration_version()) || + IsExpiredDate(info.common_info.expiration_day)) { + eventId = kExpiredEventId; + } + + gEventNameIDMap.Put(UniqueEventName(info), eventId); + } + +#ifdef DEBUG + gEventNameIDMap.MarkImmutable(); +#endif + gInitDone = true; +} + +void +TelemetryEvent::DeInitializeGlobalState() +{ + StaticMutexAutoLock locker(gTelemetryEventsMutex); + MOZ_ASSERT(gInitDone); + + gCanRecordBase = false; + gCanRecordExtended = false; + + gEventNameIDMap.Clear(); + gEventRecords->Clear(); + gEventRecords = nullptr; + + gInitDone = false; +} + +void +TelemetryEvent::SetCanRecordBase(bool b) +{ + StaticMutexAutoLock locker(gTelemetryEventsMutex); + gCanRecordBase = b; +} + +void +TelemetryEvent::SetCanRecordExtended(bool b) { + StaticMutexAutoLock locker(gTelemetryEventsMutex); + gCanRecordExtended = b; +} + +nsresult +TelemetryEvent::RecordEvent(const nsACString& aCategory, const nsACString& aMethod, + const nsACString& aObject, JS::HandleValue aValue, + JS::HandleValue aExtra, JSContext* cx, + uint8_t optional_argc) +{ + // Currently only recording in the parent process is supported. + if (!XRE_IsParentProcess()) { + return NS_OK; + } + + // Get the current time. + double timestamp = -1; + nsresult rv = MsSinceProcessStart(×tamp); + if (NS_FAILED(rv)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Failed to get time since process start.")); + return NS_OK; + } + + // Check value argument. + if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Invalid type for value parameter.")); + return NS_OK; + } + + // Extract value parameter. + Maybe value; + if (aValue.isString()) { + nsAutoJSString jsStr; + if (!jsStr.init(cx, aValue)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Invalid string value for value parameter.")); + return NS_OK; + } + + nsCString str = NS_ConvertUTF16toUTF8(jsStr); + if (str.Length() > kMaxValueByteLength) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Value parameter exceeds maximum string length, truncating.")); + TruncateToByteLength(str, kMaxValueByteLength); + } + value = mozilla::Some(str); + } + + // Check extra argument. + if ((optional_argc > 1) && !aExtra.isNull() && !aExtra.isObject()) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Invalid type for extra parameter.")); + return NS_OK; + } + + // Extract extra dictionary. + ExtraArray extra; + if (aExtra.isObject()) { + JS::RootedObject obj(cx, &aExtra.toObject()); + JS::Rooted ids(cx, JS::IdVector(cx)); + if (!JS_Enumerate(cx, obj, &ids)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Failed to enumerate object.")); + return NS_OK; + } + + for (size_t i = 0, n = ids.length(); i < n; i++) { + nsAutoJSString key; + if (!key.init(cx, ids[i])) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Extra dictionary should only contain string keys.")); + return NS_OK; + } + + JS::Rooted value(cx); + if (!JS_GetPropertyById(cx, obj, ids[i], &value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Failed to get extra property.")); + return NS_OK; + } + + nsAutoJSString jsStr; + if (!value.isString() || !jsStr.init(cx, value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Extra properties should have string values.")); + return NS_OK; + } + + nsCString str = NS_ConvertUTF16toUTF8(jsStr); + if (str.Length() > kMaxExtraValueByteLength) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Extra value exceeds maximum string length, truncating.")); + TruncateToByteLength(str, kMaxExtraValueByteLength); + } + + extra.AppendElement(ExtraEntry{NS_ConvertUTF16toUTF8(key), str}); + } + } + + // Lock for accessing internal data. + // While the lock is being held, no complex calls like JS calls can be made, + // as all of these could record Telemetry, which would result in deadlock. + RecordEventResult res; + { + StaticMutexAutoLock lock(gTelemetryEventsMutex); + + if (!gInitDone) { + return NS_ERROR_FAILURE; + } + + res = ::RecordEvent(lock, timestamp, aCategory, aMethod, aObject, value, extra); + } + + // Trigger warnings or errors where needed. + switch (res) { + case RecordEventResult::UnknownEvent: { + JS_ReportErrorASCII(cx, R"(Unknown event: ["%s", "%s", "%s"])", + PromiseFlatCString(aCategory).get(), + PromiseFlatCString(aMethod).get(), + PromiseFlatCString(aObject).get()); + return NS_ERROR_INVALID_ARG; + } + case RecordEventResult::InvalidExtraKey: + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Invalid extra key for event.")); + return NS_OK; + case RecordEventResult::StorageLimitReached: + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Event storage limit reached.")); + return NS_OK; + default: + return NS_OK; + } +} + +nsresult +TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* cx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + // Extract the events from storage. + nsTArray events; + { + StaticMutexAutoLock locker(gTelemetryEventsMutex); + + if (!gInitDone) { + return NS_ERROR_FAILURE; + } + + uint32_t len = gEventRecords->Length(); + for (uint32_t i = 0; i < len; ++i) { + const EventRecord& record = (*gEventRecords)[i]; + const EventInfo& info = gEventInfo[record.EventId()]; + + if (IsInDataset(info.common_info.dataset, aDataset)) { + events.AppendElement(record); + } + } + + if (aClear) { + gEventRecords->Clear(); + } + } + + // We serialize the events to a JS array. + JS::RootedObject eventsArray(cx, JS_NewArrayObject(cx, events.Length())); + if (!eventsArray) { + return NS_ERROR_FAILURE; + } + + for (uint32_t i = 0; i < events.Length(); ++i) { + const EventRecord& record = events[i]; + const EventInfo& info = gEventInfo[record.EventId()]; + + // Each entry is an array of one of the forms: + // [timestamp, category, method, object, value] + // [timestamp, category, method, object, null, extra] + // [timestamp, category, method, object, value, extra] + JS::AutoValueVector items(cx); + + // Add timestamp. + JS::Rooted val(cx); + if (!items.append(JS::NumberValue(floor(record.Timestamp())))) { + return NS_ERROR_FAILURE; + } + + // Add category, method, object. + const char* strings[] = { + info.common_info.category(), + info.method(), + info.object(), + }; + for (const char* s : strings) { + const NS_ConvertUTF8toUTF16 wide(s); + if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) { + return NS_ERROR_FAILURE; + } + } + + // Add the optional string value only when needed. + // When extra is empty and this has no value, we can save a little space. + if (record.Value()) { + const NS_ConvertUTF8toUTF16 wide(record.Value().value()); + if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) { + return NS_ERROR_FAILURE; + } + } else if (!record.Extra().IsEmpty()) { + if (!items.append(JS::NullValue())) { + return NS_ERROR_FAILURE; + } + } + + // Add the optional extra dictionary. + // To save a little space, only add it when it is not empty. + if (!record.Extra().IsEmpty()) { + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return NS_ERROR_FAILURE; + } + + // Add extra key & value entries. + const ExtraArray& extra = record.Extra(); + for (uint32_t i = 0; i < extra.Length(); ++i) { + const NS_ConvertUTF8toUTF16 wide(extra[i].value); + JS::Rooted value(cx); + value.setString(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())); + + if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + val.setObject(*obj); + + if (!items.append(val)) { + return NS_ERROR_FAILURE; + } + } + + // Add the record to the events array. + JS::RootedObject itemsArray(cx, JS_NewArrayObject(cx, items)); + if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + aResult.setObject(*eventsArray); + return NS_OK; +} + +/** + * Resets all the stored events. This is intended to be only used in tests. + */ +void +TelemetryEvent::ClearEvents() +{ + StaticMutexAutoLock lock(gTelemetryEventsMutex); + + if (!gInitDone) { + return; + } + + gEventRecords->Clear(); +} + +size_t +TelemetryEvent::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) +{ + StaticMutexAutoLock locker(gTelemetryEventsMutex); + size_t n = 0; + + n += gEventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf); + for (uint32_t i = 0; i < gEventRecords->Length(); ++i) { + n += (*gEventRecords)[i].SizeOfExcludingThis(aMallocSizeOf); + } + + n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) { + n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + return n; +} diff --git a/toolkit/components/telemetry/TelemetryEvent.h b/toolkit/components/telemetry/TelemetryEvent.h new file mode 100644 index 000000000..34a0720b7 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryEvent.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef TelemetryEvent_h__ +#define TelemetryEvent_h__ + +#include "mozilla/TelemetryEventEnums.h" + +// This module is internal to Telemetry. It encapsulates Telemetry's +// event recording and storage logic. It should only be used by +// Telemetry.cpp. These functions should not be used anywhere else. +// For the public interface to Telemetry functionality, see Telemetry.h. + +namespace TelemetryEvent { + +void InitializeGlobalState(bool canRecordBase, bool canRecordExtended); +void DeInitializeGlobalState(); + +void SetCanRecordBase(bool b); +void SetCanRecordExtended(bool b); + +// JS API Endpoints. +nsresult RecordEvent(const nsACString& aCategory, const nsACString& aMethod, + const nsACString& aObject, JS::HandleValue aValue, + JS::HandleValue aExtra, JSContext* aCx, + uint8_t optional_argc); +nsresult CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandleValue aResult); + +// Only to be used for testing. +void ClearEvents(); + +size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + +} // namespace TelemetryEvent + +#endif // TelemetryEvent_h__ diff --git a/toolkit/components/telemetry/TelemetryHistogram.cpp b/toolkit/components/telemetry/TelemetryHistogram.cpp new file mode 100644 index 000000000..abae9c613 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryHistogram.cpp @@ -0,0 +1,2725 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/GCAPI.h" +#include "nsString.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsBaseHashtable.h" +#include "nsClassHashtable.h" +#include "nsITelemetry.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/gfx/GPUParent.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/Atomics.h" +#include "mozilla/StartupTimeline.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Unused.h" + +#include "TelemetryCommon.h" +#include "TelemetryHistogram.h" + +#include "base/histogram.h" + +using base::Histogram; +using base::StatisticsRecorder; +using base::BooleanHistogram; +using base::CountHistogram; +using base::FlagHistogram; +using base::LinearHistogram; +using mozilla::StaticMutex; +using mozilla::StaticMutexAutoLock; +using mozilla::StaticAutoPtr; +using mozilla::Telemetry::Accumulation; +using mozilla::Telemetry::KeyedAccumulation; + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// Naming: there are two kinds of functions in this file: +// +// * Functions named internal_*: these can only be reached via an +// interface function (TelemetryHistogram::*). They mostly expect +// the interface function to have acquired +// |gTelemetryHistogramMutex|, so they do not have to be +// thread-safe. However, those internal_* functions that are +// reachable from internal_WrapAndReturnHistogram and +// internal_WrapAndReturnKeyedHistogram can sometimes be called +// without |gTelemetryHistogramMutex|, and so might be racey. +// +// * Functions named TelemetryHistogram::*. This is the external interface. +// Entries and exits to these functions are serialised using +// |gTelemetryHistogramMutex|, except for GetAddonHistogramSnapshots, +// GetKeyedHistogramSnapshots and CreateHistogramSnapshots. +// +// Avoiding races and deadlocks: +// +// All functions in the external interface (TelemetryHistogram::*) are +// serialised using the mutex |gTelemetryHistogramMutex|. This means +// that the external interface is thread-safe, and many of the +// internal_* functions can ignore thread safety. But it also brings +// a danger of deadlock if any function in the external interface can +// get back to that interface. That is, we will deadlock on any call +// chain like this +// +// TelemetryHistogram::* -> .. any functions .. -> TelemetryHistogram::* +// +// To reduce the danger of that happening, observe the following rules: +// +// * No function in TelemetryHistogram::* may directly call, nor take the +// address of, any other function in TelemetryHistogram::*. +// +// * No internal function internal_* may call, nor take the address +// of, any function in TelemetryHistogram::*. +// +// internal_WrapAndReturnHistogram and +// internal_WrapAndReturnKeyedHistogram are not protected by +// |gTelemetryHistogramMutex| because they make calls to the JS +// engine, but that can in turn call back to Telemetry and hence back +// to a TelemetryHistogram:: function, in order to report GC and other +// statistics. This would lead to deadlock due to attempted double +// acquisition of |gTelemetryHistogramMutex|, if the internal_* functions +// were required to be protected by |gTelemetryHistogramMutex|. To +// break that cycle, we relax that requirement. Unfortunately this +// means that this file is not guaranteed race-free. + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE TYPES + +#define EXPIRED_ID "__expired__" +#define SUBSESSION_HISTOGRAM_PREFIX "sub#" +#define KEYED_HISTOGRAM_NAME_SEPARATOR "#" +#define CONTENT_HISTOGRAM_SUFFIX "#content" +#define GPU_HISTOGRAM_SUFFIX "#gpu" + +namespace { + +using mozilla::Telemetry::Common::AutoHashtable; +using mozilla::Telemetry::Common::IsExpiredVersion; +using mozilla::Telemetry::Common::CanRecordDataset; +using mozilla::Telemetry::Common::IsInDataset; + +class KeyedHistogram; + +typedef nsBaseHashtableET + CharPtrEntryType; + +typedef AutoHashtable HistogramMapType; + +typedef nsClassHashtable + KeyedHistogramMapType; + +// Hardcoded probes +struct HistogramInfo { + uint32_t min; + uint32_t max; + uint32_t bucketCount; + uint32_t histogramType; + uint32_t id_offset; + uint32_t expiration_offset; + uint32_t dataset; + uint32_t label_index; + uint32_t label_count; + bool keyed; + + const char *id() const; + const char *expiration() const; + nsresult label_id(const char* label, uint32_t* labelId) const; +}; + +struct AddonHistogramInfo { + uint32_t min; + uint32_t max; + uint32_t bucketCount; + uint32_t histogramType; + Histogram *h; +}; + +enum reflectStatus { + REFLECT_OK, + REFLECT_CORRUPT, + REFLECT_FAILURE +}; + +typedef StatisticsRecorder::Histograms::iterator HistogramIterator; + +typedef nsBaseHashtableET + AddonHistogramEntryType; + +typedef AutoHashtable + AddonHistogramMapType; + +typedef nsBaseHashtableET + AddonEntryType; + +typedef AutoHashtable AddonMapType; + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE STATE, SHARED BY ALL THREADS + +namespace { + +// Set to true once this global state has been initialized +bool gInitDone = false; + +bool gCanRecordBase = false; +bool gCanRecordExtended = false; + +HistogramMapType gHistogramMap(mozilla::Telemetry::HistogramCount); + +KeyedHistogramMapType gKeyedHistograms; + +bool gCorruptHistograms[mozilla::Telemetry::HistogramCount]; + +// This is for gHistograms, gHistogramStringTable +#include "TelemetryHistogramData.inc" + +AddonMapType gAddonMap; + +// The singleton StatisticsRecorder object for this process. +base::StatisticsRecorder* gStatisticsRecorder = nullptr; + +// For batching and sending child process accumulations to the parent +nsITimer* gIPCTimer = nullptr; +mozilla::Atomic gIPCTimerArmed(false); +mozilla::Atomic gIPCTimerArming(false); +StaticAutoPtr> gAccumulations; +StaticAutoPtr> gKeyedAccumulations; + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE CONSTANTS + +namespace { + +// List of histogram IDs which should have recording disabled initially. +const mozilla::Telemetry::ID kRecordingInitiallyDisabledIDs[] = { + mozilla::Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, + + // The array must not be empty. Leave these item here. + mozilla::Telemetry::TELEMETRY_TEST_COUNT_INIT_NO_RECORD, + mozilla::Telemetry::TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD +}; + +// Sending each remote accumulation immediately places undue strain on the +// IPC subsystem. Batch the remote accumulations for a period of time before +// sending them all at once. This value was chosen as a balance between data +// timeliness and performance (see bug 1218576) +const uint32_t kBatchTimeoutMs = 2000; + +// To stop growing unbounded in memory while waiting for kBatchTimeoutMs to +// drain the g*Accumulations arrays, request an immediate flush if the arrays +// manage to reach this high water mark of elements. +const size_t kAccumulationsArrayHighWaterMark = 5 * 1024; + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Misc small helpers + +namespace { + +bool +internal_CanRecordBase() { + return gCanRecordBase; +} + +bool +internal_CanRecordExtended() { + return gCanRecordExtended; +} + +bool +internal_IsHistogramEnumId(mozilla::Telemetry::ID aID) +{ + static_assert(((mozilla::Telemetry::ID)-1 > 0), "ID should be unsigned."); + return aID < mozilla::Telemetry::HistogramCount; +} + +// Note: this is completely unrelated to mozilla::IsEmpty. +bool +internal_IsEmpty(const Histogram *h) +{ + Histogram::SampleSet ss; + h->SnapshotSample(&ss); + return ss.counts(0) == 0 && ss.sum() == 0; +} + +bool +internal_IsExpired(const Histogram *histogram) +{ + return histogram->histogram_name() == EXPIRED_ID; +} + +nsresult +internal_GetRegisteredHistogramIds(bool keyed, uint32_t dataset, + uint32_t *aCount, char*** aHistograms) +{ + nsTArray collection; + + for (size_t i = 0; i < mozilla::ArrayLength(gHistograms); ++i) { + const HistogramInfo& h = gHistograms[i]; + if (IsExpiredVersion(h.expiration()) || + h.keyed != keyed || + !IsInDataset(h.dataset, dataset)) { + continue; + } + + const char* id = h.id(); + const size_t len = strlen(id); + collection.AppendElement(static_cast(nsMemory::Clone(id, len+1))); + } + + const size_t bytes = collection.Length() * sizeof(char*); + char** histograms = static_cast(moz_xmalloc(bytes)); + memcpy(histograms, collection.Elements(), bytes); + *aHistograms = histograms; + *aCount = collection.Length(); + + return NS_OK; +} + +const char * +HistogramInfo::id() const +{ + return &gHistogramStringTable[this->id_offset]; +} + +const char * +HistogramInfo::expiration() const +{ + return &gHistogramStringTable[this->expiration_offset]; +} + +nsresult +HistogramInfo::label_id(const char* label, uint32_t* labelId) const +{ + MOZ_ASSERT(label); + MOZ_ASSERT(this->histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL); + if (this->histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) { + return NS_ERROR_FAILURE; + } + + for (uint32_t i = 0; i < this->label_count; ++i) { + // gHistogramLabelTable contains the indices of the label strings in the + // gHistogramStringTable. + // They are stored in-order and consecutively, from the offset label_index + // to (label_index + label_count). + uint32_t string_offset = gHistogramLabelTable[this->label_index + i]; + const char* const str = &gHistogramStringTable[string_offset]; + if (::strcmp(label, str) == 0) { + *labelId = i; + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +void internal_DispatchToMainThread(already_AddRefed&& aEvent) +{ + nsCOMPtr event(aEvent); + nsCOMPtr thread; + nsresult rv = NS_GetMainThread(getter_AddRefs(thread)); + if (NS_FAILED(rv)) { + NS_WARNING("NS_FAILED DispatchToMainThread. Maybe we're shutting down?"); + return; + } + thread->Dispatch(event, 0); +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Histogram Get, Add, Clone, Clear functions + +namespace { + +nsresult +internal_CheckHistogramArguments(uint32_t histogramType, + uint32_t min, uint32_t max, + uint32_t bucketCount, bool haveOptArgs) +{ + if (histogramType != nsITelemetry::HISTOGRAM_BOOLEAN + && histogramType != nsITelemetry::HISTOGRAM_FLAG + && histogramType != nsITelemetry::HISTOGRAM_COUNT) { + // The min, max & bucketCount arguments are not optional for this type. + if (!haveOptArgs) + return NS_ERROR_ILLEGAL_VALUE; + + // Sanity checks for histogram parameters. + if (min >= max) + return NS_ERROR_ILLEGAL_VALUE; + + if (bucketCount <= 2) + return NS_ERROR_ILLEGAL_VALUE; + + if (min < 1) + return NS_ERROR_ILLEGAL_VALUE; + } + + return NS_OK; +} + +/* + * min, max & bucketCount are optional for boolean, flag & count histograms. + * haveOptArgs has to be set if the caller provides them. + */ +nsresult +internal_HistogramGet(const char *name, const char *expiration, + uint32_t histogramType, uint32_t min, uint32_t max, + uint32_t bucketCount, bool haveOptArgs, + Histogram **result) +{ + nsresult rv = internal_CheckHistogramArguments(histogramType, min, max, + bucketCount, haveOptArgs); + if (NS_FAILED(rv)) { + return rv; + } + + if (IsExpiredVersion(expiration)) { + name = EXPIRED_ID; + min = 1; + max = 2; + bucketCount = 3; + histogramType = nsITelemetry::HISTOGRAM_LINEAR; + } + + switch (histogramType) { + case nsITelemetry::HISTOGRAM_EXPONENTIAL: + *result = Histogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag); + break; + case nsITelemetry::HISTOGRAM_LINEAR: + case nsITelemetry::HISTOGRAM_CATEGORICAL: + *result = LinearHistogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag); + break; + case nsITelemetry::HISTOGRAM_BOOLEAN: + *result = BooleanHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag); + break; + case nsITelemetry::HISTOGRAM_FLAG: + *result = FlagHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag); + break; + case nsITelemetry::HISTOGRAM_COUNT: + *result = CountHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag); + break; + default: + NS_ASSERTION(false, "Invalid histogram type"); + return NS_ERROR_INVALID_ARG; + } + return NS_OK; +} + +// Read the process type from the given histogram name. The process type, if +// one exists, is embedded in a suffix. +GeckoProcessType +GetProcessFromName(const nsACString& aString) +{ + if (StringEndsWith(aString, NS_LITERAL_CSTRING(CONTENT_HISTOGRAM_SUFFIX))) { + return GeckoProcessType_Content; + } + if (StringEndsWith(aString, NS_LITERAL_CSTRING(GPU_HISTOGRAM_SUFFIX))) { + return GeckoProcessType_GPU; + } + return GeckoProcessType_Default; +} + +const char* +SuffixForProcessType(GeckoProcessType aProcessType) +{ + switch (aProcessType) { + case GeckoProcessType_Default: + return nullptr; + case GeckoProcessType_Content: + return CONTENT_HISTOGRAM_SUFFIX; + case GeckoProcessType_GPU: + return GPU_HISTOGRAM_SUFFIX; + default: + MOZ_ASSERT_UNREACHABLE("unknown process type"); + return nullptr; + } +} + +CharPtrEntryType* +internal_GetHistogramMapEntry(const char* aName) +{ + nsDependentCString name(aName); + GeckoProcessType process = GetProcessFromName(name); + const char* suffix = SuffixForProcessType(process); + if (!suffix) { + return gHistogramMap.GetEntry(aName); + } + + auto root = Substring(name, 0, name.Length() - strlen(suffix)); + return gHistogramMap.GetEntry(PromiseFlatCString(root).get()); +} + +nsresult +internal_GetHistogramEnumId(const char *name, mozilla::Telemetry::ID *id) +{ + if (!gInitDone) { + return NS_ERROR_FAILURE; + } + + CharPtrEntryType *entry = internal_GetHistogramMapEntry(name); + if (!entry) { + return NS_ERROR_INVALID_ARG; + } + *id = entry->mData; + return NS_OK; +} + +// O(1) histogram lookup by numeric id +nsresult +internal_GetHistogramByEnumId(mozilla::Telemetry::ID id, Histogram **ret, GeckoProcessType aProcessType) +{ + static Histogram* knownHistograms[mozilla::Telemetry::HistogramCount] = {0}; + static Histogram* knownContentHistograms[mozilla::Telemetry::HistogramCount] = {0}; + static Histogram* knownGPUHistograms[mozilla::Telemetry::HistogramCount] = {0}; + + Histogram** knownList = nullptr; + + switch (aProcessType) { + case GeckoProcessType_Default: + knownList = knownHistograms; + break; + case GeckoProcessType_Content: + knownList = knownContentHistograms; + break; + case GeckoProcessType_GPU: + knownList = knownGPUHistograms; + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown process type"); + return NS_ERROR_FAILURE; + } + + Histogram* h = knownList[id]; + if (h) { + *ret = h; + return NS_OK; + } + + const HistogramInfo &p = gHistograms[id]; + if (p.keyed) { + return NS_ERROR_FAILURE; + } + + nsCString histogramName; + histogramName.Append(p.id()); + if (const char* suffix = SuffixForProcessType(aProcessType)) { + histogramName.AppendASCII(suffix); + } + + nsresult rv = internal_HistogramGet(histogramName.get(), p.expiration(), + p.histogramType, p.min, p.max, + p.bucketCount, true, &h); + if (NS_FAILED(rv)) + return rv; + +#ifdef DEBUG + // Check that the C++ Histogram code computes the same ranges as the + // Python histogram code. + if (!IsExpiredVersion(p.expiration())) { + const struct bounds &b = gBucketLowerBoundIndex[id]; + if (b.length != 0) { + MOZ_ASSERT(size_t(b.length) == h->bucket_count(), + "C++/Python bucket # mismatch"); + for (int i = 0; i < b.length; ++i) { + MOZ_ASSERT(gBucketLowerBounds[b.offset + i] == h->ranges(i), + "C++/Python bucket mismatch"); + } + } + } +#endif + + knownList[id] = h; + *ret = h; + return NS_OK; +} + +nsresult +internal_GetHistogramByName(const nsACString &name, Histogram **ret) +{ + mozilla::Telemetry::ID id; + nsresult rv + = internal_GetHistogramEnumId(PromiseFlatCString(name).get(), &id); + if (NS_FAILED(rv)) { + return rv; + } + + GeckoProcessType process = GetProcessFromName(name); + rv = internal_GetHistogramByEnumId(id, ret, process); + if (NS_FAILED(rv)) + return rv; + + return NS_OK; +} + + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + +/** + * This clones a histogram |existing| with the id |existingId| to a + * new histogram with the name |newName|. + * For simplicity this is limited to registered histograms. + */ +Histogram* +internal_CloneHistogram(const nsACString& newName, + mozilla::Telemetry::ID existingId, + Histogram& existing) +{ + const HistogramInfo &info = gHistograms[existingId]; + Histogram *clone = nullptr; + nsresult rv; + + rv = internal_HistogramGet(PromiseFlatCString(newName).get(), + info.expiration(), + info.histogramType, existing.declared_min(), + existing.declared_max(), existing.bucket_count(), + true, &clone); + if (NS_FAILED(rv)) { + return nullptr; + } + + Histogram::SampleSet ss; + existing.SnapshotSample(&ss); + clone->AddSampleSet(ss); + + return clone; +} + +GeckoProcessType +GetProcessFromName(const std::string& aString) +{ + nsDependentCString string(aString.c_str(), aString.length()); + return GetProcessFromName(string); +} + +Histogram* +internal_GetSubsessionHistogram(Histogram& existing) +{ + mozilla::Telemetry::ID id; + nsresult rv + = internal_GetHistogramEnumId(existing.histogram_name().c_str(), &id); + if (NS_FAILED(rv) || gHistograms[id].keyed) { + return nullptr; + } + + static Histogram* subsession[mozilla::Telemetry::HistogramCount] = {}; + static Histogram* subsessionContent[mozilla::Telemetry::HistogramCount] = {}; + static Histogram* subsessionGPU[mozilla::Telemetry::HistogramCount] = {}; + + Histogram** cache = nullptr; + + GeckoProcessType process = GetProcessFromName(existing.histogram_name()); + switch (process) { + case GeckoProcessType_Default: + cache = subsession; + break; + case GeckoProcessType_Content: + cache = subsessionContent; + break; + case GeckoProcessType_GPU: + cache = subsessionGPU; + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown process type"); + return nullptr; + } + + if (Histogram* cached = cache[id]) { + return cached; + } + + NS_NAMED_LITERAL_CSTRING(prefix, SUBSESSION_HISTOGRAM_PREFIX); + nsDependentCString existingName(gHistograms[id].id()); + if (StringBeginsWith(existingName, prefix)) { + return nullptr; + } + + nsCString subsessionName(prefix); + subsessionName.Append(existing.histogram_name().c_str()); + + Histogram* clone = internal_CloneHistogram(subsessionName, id, existing); + cache[id] = clone; + return clone; +} +#endif + +nsresult +internal_HistogramAdd(Histogram& histogram, int32_t value, uint32_t dataset) +{ + // Check if we are allowed to record the data. + bool canRecordDataset = CanRecordDataset(dataset, + internal_CanRecordBase(), + internal_CanRecordExtended()); + if (!canRecordDataset || !histogram.IsRecordingEnabled()) { + return NS_OK; + } + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + if (Histogram* subsession = internal_GetSubsessionHistogram(histogram)) { + subsession->Add(value); + } +#endif + + // It is safe to add to the histogram now: the subsession histogram was already + // cloned from this so we won't add the sample twice. + histogram.Add(value); + + return NS_OK; +} + +nsresult +internal_HistogramAdd(Histogram& histogram, int32_t value) +{ + uint32_t dataset = nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN; + // We only really care about the dataset of the histogram if we are not recording + // extended telemetry. Otherwise, we always record histogram data. + if (!internal_CanRecordExtended()) { + mozilla::Telemetry::ID id; + nsresult rv + = internal_GetHistogramEnumId(histogram.histogram_name().c_str(), &id); + if (NS_FAILED(rv)) { + // If we can't look up the dataset, it might be because the histogram was added + // at runtime. Since we're not recording extended telemetry, bail out. + return NS_OK; + } + dataset = gHistograms[id].dataset; + } + + return internal_HistogramAdd(histogram, value, dataset); +} + +void +internal_HistogramClear(Histogram& aHistogram, bool onlySubsession) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + if (!XRE_IsParentProcess()) { + return; + } + if (!onlySubsession) { + aHistogram.Clear(); + } + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + if (Histogram* subsession = internal_GetSubsessionHistogram(aHistogram)) { + subsession->Clear(); + } +#endif +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Histogram corruption helpers + +namespace { + +void internal_Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample); + +void +internal_IdentifyCorruptHistograms(StatisticsRecorder::Histograms &hs) +{ + for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) { + Histogram *h = *it; + + mozilla::Telemetry::ID id; + nsresult rv = internal_GetHistogramEnumId(h->histogram_name().c_str(), &id); + // This histogram isn't a static histogram, just ignore it. + if (NS_FAILED(rv)) { + continue; + } + + if (gCorruptHistograms[id]) { + continue; + } + + Histogram::SampleSet ss; + h->SnapshotSample(&ss); + + Histogram::Inconsistencies check = h->FindCorruption(ss); + bool corrupt = (check != Histogram::NO_INCONSISTENCIES); + + if (corrupt) { + mozilla::Telemetry::ID corruptID = mozilla::Telemetry::HistogramCount; + if (check & Histogram::RANGE_CHECKSUM_ERROR) { + corruptID = mozilla::Telemetry::RANGE_CHECKSUM_ERRORS; + } else if (check & Histogram::BUCKET_ORDER_ERROR) { + corruptID = mozilla::Telemetry::BUCKET_ORDER_ERRORS; + } else if (check & Histogram::COUNT_HIGH_ERROR) { + corruptID = mozilla::Telemetry::TOTAL_COUNT_HIGH_ERRORS; + } else if (check & Histogram::COUNT_LOW_ERROR) { + corruptID = mozilla::Telemetry::TOTAL_COUNT_LOW_ERRORS; + } + internal_Accumulate(corruptID, 1); + } + + gCorruptHistograms[id] = corrupt; + } +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Histogram reflection helpers + +namespace { + +bool +internal_FillRanges(JSContext *cx, JS::Handle array, Histogram *h) +{ + JS::Rooted range(cx); + for (size_t i = 0; i < h->bucket_count(); i++) { + range.setInt32(h->ranges(i)); + if (!JS_DefineElement(cx, array, i, range, JSPROP_ENUMERATE)) + return false; + } + return true; +} + +enum reflectStatus +internal_ReflectHistogramAndSamples(JSContext *cx, + JS::Handle obj, Histogram *h, + const Histogram::SampleSet &ss) +{ + // We don't want to reflect corrupt histograms. + if (h->FindCorruption(ss) != Histogram::NO_INCONSISTENCIES) { + return REFLECT_CORRUPT; + } + + if (!(JS_DefineProperty(cx, obj, "min", + h->declared_min(), JSPROP_ENUMERATE) + && JS_DefineProperty(cx, obj, "max", + h->declared_max(), JSPROP_ENUMERATE) + && JS_DefineProperty(cx, obj, "histogram_type", + h->histogram_type(), JSPROP_ENUMERATE) + && JS_DefineProperty(cx, obj, "sum", + double(ss.sum()), JSPROP_ENUMERATE))) { + return REFLECT_FAILURE; + } + + const size_t count = h->bucket_count(); + JS::Rooted rarray(cx, JS_NewArrayObject(cx, count)); + if (!rarray) { + return REFLECT_FAILURE; + } + if (!(internal_FillRanges(cx, rarray, h) + && JS_DefineProperty(cx, obj, "ranges", rarray, JSPROP_ENUMERATE))) { + return REFLECT_FAILURE; + } + + JS::Rooted counts_array(cx, JS_NewArrayObject(cx, count)); + if (!counts_array) { + return REFLECT_FAILURE; + } + if (!JS_DefineProperty(cx, obj, "counts", counts_array, JSPROP_ENUMERATE)) { + return REFLECT_FAILURE; + } + for (size_t i = 0; i < count; i++) { + if (!JS_DefineElement(cx, counts_array, i, + ss.counts(i), JSPROP_ENUMERATE)) { + return REFLECT_FAILURE; + } + } + + return REFLECT_OK; +} + +enum reflectStatus +internal_ReflectHistogramSnapshot(JSContext *cx, + JS::Handle obj, Histogram *h) +{ + Histogram::SampleSet ss; + h->SnapshotSample(&ss); + return internal_ReflectHistogramAndSamples(cx, obj, h, ss); +} + +bool +internal_ShouldReflectHistogram(Histogram *h) +{ + const char *name = h->histogram_name().c_str(); + mozilla::Telemetry::ID id; + nsresult rv = internal_GetHistogramEnumId(name, &id); + if (NS_FAILED(rv)) { + // GetHistogramEnumId generally should not fail. But a lookup + // failure shouldn't prevent us from reflecting histograms into JS. + // + // However, these two histograms are created by Histogram itself for + // tracking corruption. We have our own histograms for that, so + // ignore these two. + if (strcmp(name, "Histogram.InconsistentCountHigh") == 0 + || strcmp(name, "Histogram.InconsistentCountLow") == 0) { + return false; + } + return true; + } else { + return !gCorruptHistograms[id]; + } +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: class KeyedHistogram + +namespace { + +class KeyedHistogram { +public: + KeyedHistogram(const nsACString &name, const nsACString &expiration, + uint32_t histogramType, uint32_t min, uint32_t max, + uint32_t bucketCount, uint32_t dataset); + nsresult GetHistogram(const nsCString& name, Histogram** histogram, bool subsession); + Histogram* GetHistogram(const nsCString& name, bool subsession); + uint32_t GetHistogramType() const { return mHistogramType; } + nsresult GetDataset(uint32_t* dataset) const; + nsresult GetJSKeys(JSContext* cx, JS::CallArgs& args); + nsresult GetJSSnapshot(JSContext* cx, JS::Handle obj, + bool subsession, bool clearSubsession); + + void SetRecordingEnabled(bool aEnabled) { mRecordingEnabled = aEnabled; }; + bool IsRecordingEnabled() const { return mRecordingEnabled; }; + + nsresult Add(const nsCString& key, uint32_t aSample); + void Clear(bool subsession); + + nsresult GetEnumId(mozilla::Telemetry::ID& id); + +private: + typedef nsBaseHashtableET KeyedHistogramEntry; + typedef AutoHashtable KeyedHistogramMapType; + KeyedHistogramMapType mHistogramMap; +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + KeyedHistogramMapType mSubsessionMap; +#endif + + static bool ReflectKeyedHistogram(KeyedHistogramEntry* entry, + JSContext* cx, + JS::Handle obj); + + const nsCString mName; + const nsCString mExpiration; + const uint32_t mHistogramType; + const uint32_t mMin; + const uint32_t mMax; + const uint32_t mBucketCount; + const uint32_t mDataset; + mozilla::Atomic mRecordingEnabled; +}; + +KeyedHistogram::KeyedHistogram(const nsACString &name, + const nsACString &expiration, + uint32_t histogramType, + uint32_t min, uint32_t max, + uint32_t bucketCount, uint32_t dataset) + : mHistogramMap() +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + , mSubsessionMap() +#endif + , mName(name) + , mExpiration(expiration) + , mHistogramType(histogramType) + , mMin(min) + , mMax(max) + , mBucketCount(bucketCount) + , mDataset(dataset) + , mRecordingEnabled(true) +{ +} + +nsresult +KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram, + bool subsession) +{ +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap; +#else + KeyedHistogramMapType& map = mHistogramMap; +#endif + KeyedHistogramEntry* entry = map.GetEntry(key); + if (entry) { + *histogram = entry->mData; + return NS_OK; + } + + nsCString histogramName; +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + if (subsession) { + histogramName.AppendLiteral(SUBSESSION_HISTOGRAM_PREFIX); + } +#endif + histogramName.Append(mName); + histogramName.AppendLiteral(KEYED_HISTOGRAM_NAME_SEPARATOR); + histogramName.Append(key); + + Histogram* h; + nsresult rv = internal_HistogramGet(histogramName.get(), mExpiration.get(), + mHistogramType, mMin, mMax, mBucketCount, + true, &h); + if (NS_FAILED(rv)) { + return rv; + } + + h->ClearFlags(Histogram::kUmaTargetedHistogramFlag); + *histogram = h; + + entry = map.PutEntry(key); + if (MOZ_UNLIKELY(!entry)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + entry->mData = h; + return NS_OK; +} + +Histogram* +KeyedHistogram::GetHistogram(const nsCString& key, bool subsession) +{ + Histogram* h = nullptr; + if (NS_FAILED(GetHistogram(key, &h, subsession))) { + return nullptr; + } + return h; +} + +nsresult +KeyedHistogram::GetDataset(uint32_t* dataset) const +{ + MOZ_ASSERT(dataset); + *dataset = mDataset; + return NS_OK; +} + +nsresult +KeyedHistogram::Add(const nsCString& key, uint32_t sample) +{ + bool canRecordDataset = CanRecordDataset(mDataset, + internal_CanRecordBase(), + internal_CanRecordExtended()); + if (!canRecordDataset) { + return NS_OK; + } + + Histogram* histogram = GetHistogram(key, false); + MOZ_ASSERT(histogram); + if (!histogram) { + return NS_ERROR_FAILURE; + } +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + Histogram* subsession = GetHistogram(key, true); + MOZ_ASSERT(subsession); + if (!subsession) { + return NS_ERROR_FAILURE; + } +#endif + + if (!IsRecordingEnabled()) { + return NS_OK; + } + + histogram->Add(sample); +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + subsession->Add(sample); +#endif + return NS_OK; +} + +void +KeyedHistogram::Clear(bool onlySubsession) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + if (!XRE_IsParentProcess()) { + return; + } +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + for (auto iter = mSubsessionMap.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->mData->Clear(); + } + mSubsessionMap.Clear(); + if (onlySubsession) { + return; + } +#endif + + for (auto iter = mHistogramMap.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->mData->Clear(); + } + mHistogramMap.Clear(); +} + +nsresult +KeyedHistogram::GetJSKeys(JSContext* cx, JS::CallArgs& args) +{ + JS::AutoValueVector keys(cx); + if (!keys.reserve(mHistogramMap.Count())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (auto iter = mHistogramMap.Iter(); !iter.Done(); iter.Next()) { + JS::RootedValue jsKey(cx); + const NS_ConvertUTF8toUTF16 key(iter.Get()->GetKey()); + jsKey.setString(JS_NewUCStringCopyN(cx, key.Data(), key.Length())); + if (!keys.append(jsKey)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + JS::RootedObject jsKeys(cx, JS_NewArrayObject(cx, keys)); + if (!jsKeys) { + return NS_ERROR_FAILURE; + } + + args.rval().setObject(*jsKeys); + return NS_OK; +} + +bool +KeyedHistogram::ReflectKeyedHistogram(KeyedHistogramEntry* entry, + JSContext* cx, JS::Handle obj) +{ + JS::RootedObject histogramSnapshot(cx, JS_NewPlainObject(cx)); + if (!histogramSnapshot) { + return false; + } + + if (internal_ReflectHistogramSnapshot(cx, histogramSnapshot, + entry->mData) != REFLECT_OK) { + return false; + } + + const NS_ConvertUTF8toUTF16 key(entry->GetKey()); + if (!JS_DefineUCProperty(cx, obj, key.Data(), key.Length(), + histogramSnapshot, JSPROP_ENUMERATE)) { + return false; + } + + return true; +} + +nsresult +KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle obj, + bool subsession, bool clearSubsession) +{ +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap; +#else + KeyedHistogramMapType& map = mHistogramMap; +#endif + if (!map.ReflectIntoJS(&KeyedHistogram::ReflectKeyedHistogram, cx, obj)) { + return NS_ERROR_FAILURE; + } + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + if (subsession && clearSubsession) { + Clear(true); + } +#endif + + return NS_OK; +} + +nsresult +KeyedHistogram::GetEnumId(mozilla::Telemetry::ID& id) +{ + return internal_GetHistogramEnumId(mName.get(), &id); +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: KeyedHistogram helpers + +namespace { + +KeyedHistogram* +internal_GetKeyedHistogramById(const nsACString &name) +{ + if (!gInitDone) { + return nullptr; + } + + KeyedHistogram* keyed = nullptr; + gKeyedHistograms.Get(name, &keyed); + return keyed; +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: functions related to addon histograms + +namespace { + +// Compute the name to pass into Histogram for the addon histogram +// 'name' from the addon 'id'. We can't use 'name' directly because it +// might conflict with other histograms in other addons or even with our +// own. +void +internal_AddonHistogramName(const nsACString &id, const nsACString &name, + nsACString &ret) +{ + ret.Append(id); + ret.Append(':'); + ret.Append(name); +} + +bool +internal_CreateHistogramForAddon(const nsACString &name, + AddonHistogramInfo &info) +{ + Histogram *h; + nsresult rv = internal_HistogramGet(PromiseFlatCString(name).get(), "never", + info.histogramType, info.min, info.max, + info.bucketCount, true, &h); + if (NS_FAILED(rv)) { + return false; + } + // Don't let this histogram be reported via the normal means + // (e.g. Telemetry.registeredHistograms); we'll make it available in + // other ways. + h->ClearFlags(Histogram::kUmaTargetedHistogramFlag); + info.h = h; + return true; +} + +bool +internal_AddonHistogramReflector(AddonHistogramEntryType *entry, + JSContext *cx, JS::Handle obj) +{ + AddonHistogramInfo &info = entry->mData; + + // Never even accessed the histogram. + if (!info.h) { + // Have to force creation of HISTOGRAM_FLAG histograms. + if (info.histogramType != nsITelemetry::HISTOGRAM_FLAG) + return true; + + if (!internal_CreateHistogramForAddon(entry->GetKey(), info)) { + return false; + } + } + + if (internal_IsEmpty(info.h)) { + return true; + } + + JS::Rooted snapshot(cx, JS_NewPlainObject(cx)); + if (!snapshot) { + // Just consider this to be skippable. + return true; + } + switch (internal_ReflectHistogramSnapshot(cx, snapshot, info.h)) { + case REFLECT_FAILURE: + case REFLECT_CORRUPT: + return false; + case REFLECT_OK: + const nsACString &histogramName = entry->GetKey(); + if (!JS_DefineProperty(cx, obj, PromiseFlatCString(histogramName).get(), + snapshot, JSPROP_ENUMERATE)) { + return false; + } + break; + } + return true; +} + +bool +internal_AddonReflector(AddonEntryType *entry, JSContext *cx, + JS::Handle obj) +{ + const nsACString &addonId = entry->GetKey(); + JS::Rooted subobj(cx, JS_NewPlainObject(cx)); + if (!subobj) { + return false; + } + + AddonHistogramMapType *map = entry->mData; + if (!(map->ReflectIntoJS(internal_AddonHistogramReflector, cx, subobj) + && JS_DefineProperty(cx, obj, PromiseFlatCString(addonId).get(), + subobj, JSPROP_ENUMERATE))) { + return false; + } + return true; +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: thread-unsafe helpers for the external interface + +// This is a StaticMutex rather than a plain Mutex (1) so that +// it gets initialised in a thread-safe manner the first time +// it is used, and (2) because it is never de-initialised, and +// a normal Mutex would show up as a leak in BloatView. StaticMutex +// also has the "OffTheBooks" property, so it won't show as a leak +// in BloatView. +static StaticMutex gTelemetryHistogramMutex; + +namespace { + +void +internal_SetHistogramRecordingEnabled(mozilla::Telemetry::ID aID, bool aEnabled) +{ + if (gHistograms[aID].keyed) { + const nsDependentCString id(gHistograms[aID].id()); + KeyedHistogram* keyed = internal_GetKeyedHistogramById(id); + if (keyed) { + keyed->SetRecordingEnabled(aEnabled); + return; + } + } else { + Histogram *h; + nsresult rv = internal_GetHistogramByEnumId(aID, &h, GeckoProcessType_Default); + if (NS_SUCCEEDED(rv)) { + h->SetRecordingEnabled(aEnabled); + return; + } + } + + MOZ_ASSERT(false, "Telemetry::SetHistogramRecordingEnabled(...) id not found"); +} + +void internal_armIPCTimerMainThread() +{ + MOZ_ASSERT(NS_IsMainThread()); + gIPCTimerArming = false; + if (gIPCTimerArmed) { + return; + } + if (!gIPCTimer) { + CallCreateInstance(NS_TIMER_CONTRACTID, &gIPCTimer); + } + if (gIPCTimer) { + gIPCTimer->InitWithFuncCallback(TelemetryHistogram::IPCTimerFired, + nullptr, kBatchTimeoutMs, + nsITimer::TYPE_ONE_SHOT); + gIPCTimerArmed = true; + } +} + +void internal_armIPCTimer() +{ + if (gIPCTimerArmed || gIPCTimerArming) { + return; + } + gIPCTimerArming = true; + if (NS_IsMainThread()) { + internal_armIPCTimerMainThread(); + } else { + internal_DispatchToMainThread(NS_NewRunnableFunction([]() -> void { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_armIPCTimerMainThread(); + })); + } +} + +bool +internal_RemoteAccumulate(mozilla::Telemetry::ID aId, uint32_t aSample) +{ + if (XRE_IsParentProcess()) { + return false; + } + Histogram *h; + nsresult rv = internal_GetHistogramByEnumId(aId, &h, GeckoProcessType_Default); + if (NS_SUCCEEDED(rv) && !h->IsRecordingEnabled()) { + return true; + } + if (!gAccumulations) { + gAccumulations = new nsTArray(); + } + if (gAccumulations->Length() == kAccumulationsArrayHighWaterMark) { + internal_DispatchToMainThread(NS_NewRunnableFunction([]() -> void { + TelemetryHistogram::IPCTimerFired(nullptr, nullptr); + })); + } + gAccumulations->AppendElement(Accumulation{aId, aSample}); + internal_armIPCTimer(); + return true; +} + +bool +internal_RemoteAccumulate(mozilla::Telemetry::ID aId, + const nsCString& aKey, uint32_t aSample) +{ + if (XRE_IsParentProcess()) { + return false; + } + const HistogramInfo& th = gHistograms[aId]; + KeyedHistogram* keyed + = internal_GetKeyedHistogramById(nsDependentCString(th.id())); + MOZ_ASSERT(keyed); + if (!keyed->IsRecordingEnabled()) { + return false; + } + if (!gKeyedAccumulations) { + gKeyedAccumulations = new nsTArray(); + } + if (gKeyedAccumulations->Length() == kAccumulationsArrayHighWaterMark) { + internal_DispatchToMainThread(NS_NewRunnableFunction([]() -> void { + TelemetryHistogram::IPCTimerFired(nullptr, nullptr); + })); + } + gKeyedAccumulations->AppendElement(KeyedAccumulation{aId, aSample, aKey}); + internal_armIPCTimer(); + return true; +} + +void internal_Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample) +{ + if (!internal_CanRecordBase() || + internal_RemoteAccumulate(aHistogram, aSample)) { + return; + } + Histogram *h; + nsresult rv = internal_GetHistogramByEnumId(aHistogram, &h, GeckoProcessType_Default); + if (NS_SUCCEEDED(rv)) { + internal_HistogramAdd(*h, aSample, gHistograms[aHistogram].dataset); + } +} + +void +internal_Accumulate(mozilla::Telemetry::ID aID, + const nsCString& aKey, uint32_t aSample) +{ + if (!gInitDone || !internal_CanRecordBase() || + internal_RemoteAccumulate(aID, aKey, aSample)) { + return; + } + const HistogramInfo& th = gHistograms[aID]; + KeyedHistogram* keyed + = internal_GetKeyedHistogramById(nsDependentCString(th.id())); + MOZ_ASSERT(keyed); + keyed->Add(aKey, aSample); +} + +void +internal_Accumulate(Histogram& aHistogram, uint32_t aSample) +{ + if (XRE_IsParentProcess()) { + internal_HistogramAdd(aHistogram, aSample); + return; + } + + mozilla::Telemetry::ID id; + nsresult rv = internal_GetHistogramEnumId(aHistogram.histogram_name().c_str(), &id); + if (NS_SUCCEEDED(rv)) { + internal_RemoteAccumulate(id, aSample); + } +} + +void +internal_Accumulate(KeyedHistogram& aKeyed, + const nsCString& aKey, uint32_t aSample) +{ + if (XRE_IsParentProcess()) { + aKeyed.Add(aKey, aSample); + return; + } + + mozilla::Telemetry::ID id; + if (NS_SUCCEEDED(aKeyed.GetEnumId(id))) { + internal_RemoteAccumulate(id, aKey, aSample); + } +} + +void +internal_AccumulateChild(GeckoProcessType aProcessType, mozilla::Telemetry::ID aId, uint32_t aSample) +{ + if (!internal_CanRecordBase()) { + return; + } + Histogram* h; + nsresult rv = internal_GetHistogramByEnumId(aId, &h, aProcessType); + if (NS_SUCCEEDED(rv)) { + internal_HistogramAdd(*h, aSample, gHistograms[aId].dataset); + } else { + NS_WARNING("NS_FAILED GetHistogramByEnumId for CHILD"); + } +} + +void +internal_AccumulateChildKeyed(GeckoProcessType aProcessType, mozilla::Telemetry::ID aId, + const nsCString& aKey, uint32_t aSample) +{ + if (!gInitDone || !internal_CanRecordBase()) { + return; + } + + const char* suffix = SuffixForProcessType(aProcessType); + if (!suffix) { + MOZ_ASSERT_UNREACHABLE("suffix should not be null"); + return; + } + + const HistogramInfo& th = gHistograms[aId]; + + nsCString id; + id.Append(th.id()); + id.AppendASCII(suffix); + + KeyedHistogram* keyed = internal_GetKeyedHistogramById(id); + MOZ_ASSERT(keyed); + keyed->Add(aKey, aSample); +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: JSHistogram_* functions + +// NOTE: the functions in this section: +// +// internal_JSHistogram_Add +// internal_JSHistogram_Snapshot +// internal_JSHistogram_Clear +// internal_JSHistogram_Dataset +// internal_WrapAndReturnHistogram +// +// all run without protection from |gTelemetryHistogramMutex|. If they +// held |gTelemetryHistogramMutex|, there would be the possibility of +// deadlock because the JS_ calls that they make may call back into the +// TelemetryHistogram interface, hence trying to re-acquire the mutex. +// +// This means that these functions potentially race against threads, but +// that seems preferable to risking deadlock. + +namespace { + +bool +internal_JSHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JSObject *obj = JS_THIS_OBJECT(cx, vp); + MOZ_ASSERT(obj); + if (!obj) { + return false; + } + + Histogram *h = static_cast(JS_GetPrivate(obj)); + MOZ_ASSERT(h); + Histogram::ClassType type = h->histogram_type(); + + JS::CallArgs args = CallArgsFromVp(argc, vp); + + if (!internal_CanRecordBase()) { + return true; + } + + uint32_t value = 0; + mozilla::Telemetry::ID id; + if ((type == base::CountHistogram::COUNT_HISTOGRAM) && (args.length() == 0)) { + // If we don't have an argument for the count histogram, assume an increment of 1. + // Otherwise, make sure to run some sanity checks on the argument. + value = 1; + } else if (type == base::LinearHistogram::LINEAR_HISTOGRAM && + (args.length() > 0) && args[0].isString() && + NS_SUCCEEDED(internal_GetHistogramEnumId(h->histogram_name().c_str(), &id)) && + gHistograms[id].histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL) { + // For categorical histograms we allow passing a string argument that specifies the label. + nsAutoJSString label; + if (!label.init(cx, args[0])) { + JS_ReportErrorASCII(cx, "Invalid string parameter"); + return false; + } + + nsresult rv = gHistograms[id].label_id(NS_ConvertUTF16toUTF8(label).get(), &value); + if (NS_FAILED(rv)) { + JS_ReportErrorASCII(cx, "Unknown label for categorical histogram"); + return false; + } + } else { + // All other accumulations expect one numerical argument. + if (!args.length()) { + JS_ReportErrorASCII(cx, "Expected one argument"); + return false; + } + + if (!(args[0].isNumber() || args[0].isBoolean())) { + JS_ReportErrorASCII(cx, "Not a number"); + return false; + } + + if (!JS::ToUint32(cx, args[0], &value)) { + JS_ReportErrorASCII(cx, "Failed to convert argument"); + return false; + } + } + + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_Accumulate(*h, value); + } + return true; +} + +bool +internal_JSHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + Histogram *h = static_cast(JS_GetPrivate(obj)); + JS::Rooted snapshot(cx, JS_NewPlainObject(cx)); + if (!snapshot) + return false; + + switch (internal_ReflectHistogramSnapshot(cx, snapshot, h)) { + case REFLECT_FAILURE: + return false; + case REFLECT_CORRUPT: + JS_ReportErrorASCII(cx, "Histogram is corrupt"); + return false; + case REFLECT_OK: + args.rval().setObject(*snapshot); + return true; + default: + MOZ_CRASH("unhandled reflection status"); + } +} + +bool +internal_JSHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + bool onlySubsession = false; +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (args.length() >= 1) { + if (!args[0].isBoolean()) { + JS_ReportErrorASCII(cx, "Not a boolean"); + return false; + } + + onlySubsession = JS::ToBoolean(args[0]); + } +#endif + + Histogram *h = static_cast(JS_GetPrivate(obj)); + MOZ_ASSERT(h); + if (h) { + internal_HistogramClear(*h, onlySubsession); + } + + return true; +} + +bool +internal_JSHistogram_Dataset(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + Histogram *h = static_cast(JS_GetPrivate(obj)); + mozilla::Telemetry::ID id; + nsresult rv = internal_GetHistogramEnumId(h->histogram_name().c_str(), &id); + if (NS_SUCCEEDED(rv)) { + args.rval().setNumber(gHistograms[id].dataset); + return true; + } + + return false; +} + +// NOTE: Runs without protection from |gTelemetryHistogramMutex|. +// See comment at the top of this section. +nsresult +internal_WrapAndReturnHistogram(Histogram *h, JSContext *cx, + JS::MutableHandle ret) +{ + static const JSClass JSHistogram_class = { + "JSHistogram", /* name */ + JSCLASS_HAS_PRIVATE /* flags */ + }; + + JS::Rooted obj(cx, JS_NewObject(cx, &JSHistogram_class)); + if (!obj) + return NS_ERROR_FAILURE; + // The 4 functions that are wrapped up here are eventually called + // by the same thread that runs this function. + if (!(JS_DefineFunction(cx, obj, "add", internal_JSHistogram_Add, 1, 0) + && JS_DefineFunction(cx, obj, "snapshot", + internal_JSHistogram_Snapshot, 0, 0) + && JS_DefineFunction(cx, obj, "clear", internal_JSHistogram_Clear, 0, 0) + && JS_DefineFunction(cx, obj, "dataset", + internal_JSHistogram_Dataset, 0, 0))) { + return NS_ERROR_FAILURE; + } + JS_SetPrivate(obj, h); + ret.setObject(*obj); + return NS_OK; +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: JSKeyedHistogram_* functions + +// NOTE: the functions in this section: +// +// internal_KeyedHistogram_SnapshotImpl +// internal_JSKeyedHistogram_Add +// internal_JSKeyedHistogram_Keys +// internal_JSKeyedHistogram_Snapshot +// internal_JSKeyedHistogram_SubsessionSnapshot +// internal_JSKeyedHistogram_SnapshotSubsessionAndClear +// internal_JSKeyedHistogram_Clear +// internal_JSKeyedHistogram_Dataset +// internal_WrapAndReturnKeyedHistogram +// +// Same comments as above, at the JSHistogram_* section, regarding +// deadlock avoidance, apply. + +namespace { + +bool +internal_KeyedHistogram_SnapshotImpl(JSContext *cx, unsigned argc, + JS::Value *vp, + bool subsession, bool clearSubsession) +{ + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + KeyedHistogram* keyed = static_cast(JS_GetPrivate(obj)); + if (!keyed) { + return false; + } + + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS::RootedObject snapshot(cx, JS_NewPlainObject(cx)); + if (!snapshot) { + JS_ReportErrorASCII(cx, "Failed to create object"); + return false; + } + + if (!NS_SUCCEEDED(keyed->GetJSSnapshot(cx, snapshot, subsession, clearSubsession))) { + JS_ReportErrorASCII(cx, "Failed to reflect keyed histograms"); + return false; + } + + args.rval().setObject(*snapshot); + return true; + } + + nsAutoJSString key; + if (!args[0].isString() || !key.init(cx, args[0])) { + JS_ReportErrorASCII(cx, "Not a string"); + return false; + } + + Histogram* h = nullptr; + nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h, subsession); + if (NS_FAILED(rv)) { + JS_ReportErrorASCII(cx, "Failed to get histogram"); + return false; + } + + JS::RootedObject snapshot(cx, JS_NewPlainObject(cx)); + if (!snapshot) { + return false; + } + + switch (internal_ReflectHistogramSnapshot(cx, snapshot, h)) { + case REFLECT_FAILURE: + return false; + case REFLECT_CORRUPT: + JS_ReportErrorASCII(cx, "Histogram is corrupt"); + return false; + case REFLECT_OK: + args.rval().setObject(*snapshot); + return true; + default: + MOZ_CRASH("unhandled reflection status"); + } +} + +bool +internal_JSKeyedHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + KeyedHistogram* keyed = static_cast(JS_GetPrivate(obj)); + if (!keyed) { + return false; + } + + JS::CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "Expected one argument"); + return false; + } + + nsAutoJSString key; + if (!args[0].isString() || !key.init(cx, args[0])) { + JS_ReportErrorASCII(cx, "Not a string"); + return false; + } + + const uint32_t type = keyed->GetHistogramType(); + + // If we don't have an argument for the count histogram, assume an increment of 1. + // Otherwise, make sure to run some sanity checks on the argument. + int32_t value = 1; + if ((type != base::CountHistogram::COUNT_HISTOGRAM) || (args.length() == 2)) { + if (args.length() < 2) { + JS_ReportErrorASCII(cx, "Expected two arguments for this histogram type"); + return false; + } + + if (!(args[1].isNumber() || args[1].isBoolean())) { + JS_ReportErrorASCII(cx, "Not a number"); + return false; + } + + if (!JS::ToInt32(cx, args[1], &value)) { + return false; + } + } + + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_Accumulate(*keyed, NS_ConvertUTF16toUTF8(key), value); + } + return true; +} + +bool +internal_JSKeyedHistogram_Keys(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + KeyedHistogram* keyed = static_cast(JS_GetPrivate(obj)); + if (!keyed) { + return false; + } + + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + return NS_SUCCEEDED(keyed->GetJSKeys(cx, args)); +} + +bool +internal_JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp) +{ + return internal_KeyedHistogram_SnapshotImpl(cx, argc, vp, false, false); +} + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) +bool +internal_JSKeyedHistogram_SubsessionSnapshot(JSContext *cx, + unsigned argc, JS::Value *vp) +{ + return internal_KeyedHistogram_SnapshotImpl(cx, argc, vp, true, false); +} +#endif + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) +bool +internal_JSKeyedHistogram_SnapshotSubsessionAndClear(JSContext *cx, + unsigned argc, + JS::Value *vp) +{ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (args.length() != 0) { + JS_ReportErrorASCII(cx, "No key arguments supported for snapshotSubsessionAndClear"); + } + + return internal_KeyedHistogram_SnapshotImpl(cx, argc, vp, true, true); +} +#endif + +bool +internal_JSKeyedHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + KeyedHistogram* keyed = static_cast(JS_GetPrivate(obj)); + if (!keyed) { + return false; + } + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + bool onlySubsession = false; + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (args.length() >= 1) { + if (!(args[0].isNumber() || args[0].isBoolean())) { + JS_ReportErrorASCII(cx, "Not a boolean"); + return false; + } + + onlySubsession = JS::ToBoolean(args[0]); + } + + keyed->Clear(onlySubsession); +#else + keyed->Clear(false); +#endif + return true; +} + +bool +internal_JSKeyedHistogram_Dataset(JSContext *cx, unsigned argc, JS::Value *vp) +{ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JSObject *obj = JS_THIS_OBJECT(cx, vp); + if (!obj) { + return false; + } + + KeyedHistogram* keyed = static_cast(JS_GetPrivate(obj)); + if (!keyed) { + return false; + } + + uint32_t dataset = nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN; + nsresult rv = keyed->GetDataset(&dataset);; + if (NS_FAILED(rv)) { + return false; + } + + args.rval().setNumber(dataset); + return true; +} + +// NOTE: Runs without protection from |gTelemetryHistogramMutex|. +// See comment at the top of this section. +nsresult +internal_WrapAndReturnKeyedHistogram(KeyedHistogram *h, JSContext *cx, + JS::MutableHandle ret) +{ + static const JSClass JSHistogram_class = { + "JSKeyedHistogram", /* name */ + JSCLASS_HAS_PRIVATE /* flags */ + }; + + JS::Rooted obj(cx, JS_NewObject(cx, &JSHistogram_class)); + if (!obj) + return NS_ERROR_FAILURE; + // The 7 functions that are wrapped up here are eventually called + // by the same thread that runs this function. + if (!(JS_DefineFunction(cx, obj, "add", internal_JSKeyedHistogram_Add, 2, 0) + && JS_DefineFunction(cx, obj, "snapshot", + internal_JSKeyedHistogram_Snapshot, 1, 0) +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + && JS_DefineFunction(cx, obj, "subsessionSnapshot", + internal_JSKeyedHistogram_SubsessionSnapshot, 1, 0) + && JS_DefineFunction(cx, obj, "snapshotSubsessionAndClear", + internal_JSKeyedHistogram_SnapshotSubsessionAndClear, 0, 0) +#endif + && JS_DefineFunction(cx, obj, "keys", + internal_JSKeyedHistogram_Keys, 0, 0) + && JS_DefineFunction(cx, obj, "clear", + internal_JSKeyedHistogram_Clear, 0, 0) + && JS_DefineFunction(cx, obj, "dataset", + internal_JSKeyedHistogram_Dataset, 0, 0))) { + return NS_ERROR_FAILURE; + } + + JS_SetPrivate(obj, h); + ret.setObject(*obj); + return NS_OK; +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryHistogram:: + +// All of these functions are actually in namespace TelemetryHistogram::, +// but the ::TelemetryHistogram prefix is given explicitly. This is +// because it is critical to see which calls from these functions are +// to another function in this interface. Mis-identifying "inwards +// calls" from "calls to another function in this interface" will lead +// to deadlocking and/or races. See comments at the top of the file +// for further (important!) details. + +// Create and destroy the singleton StatisticsRecorder object. +void TelemetryHistogram::CreateStatisticsRecorder() +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + MOZ_ASSERT(!gStatisticsRecorder); + gStatisticsRecorder = new base::StatisticsRecorder(); +} + +void TelemetryHistogram::DestroyStatisticsRecorder() +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + MOZ_ASSERT(gStatisticsRecorder); + if (gStatisticsRecorder) { + delete gStatisticsRecorder; + gStatisticsRecorder = nullptr; + } +} + +void TelemetryHistogram::InitializeGlobalState(bool canRecordBase, + bool canRecordExtended) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + MOZ_ASSERT(!gInitDone, "TelemetryHistogram::InitializeGlobalState " + "may only be called once"); + + gCanRecordBase = canRecordBase; + gCanRecordExtended = canRecordExtended; + + // gHistogramMap should have been pre-sized correctly at the + // declaration point further up in this file. + + // Populate the static histogram name->id cache. + // Note that the histogram names are statically allocated. + for (uint32_t i = 0; i < mozilla::Telemetry::HistogramCount; i++) { + CharPtrEntryType *entry = gHistogramMap.PutEntry(gHistograms[i].id()); + entry->mData = (mozilla::Telemetry::ID) i; + } + +#ifdef DEBUG + gHistogramMap.MarkImmutable(); +#endif + + mozilla::PodArrayZero(gCorruptHistograms); + + // Create registered keyed histograms + for (size_t i = 0; i < mozilla::ArrayLength(gHistograms); ++i) { + const HistogramInfo& h = gHistograms[i]; + if (!h.keyed) { + continue; + } + + const nsDependentCString id(h.id()); + const nsDependentCString expiration(h.expiration()); + gKeyedHistograms.Put(id, new KeyedHistogram(id, expiration, h.histogramType, + h.min, h.max, h.bucketCount, h.dataset)); + if (XRE_IsParentProcess()) { + // We must create registered child keyed histograms as well or else the + // same code in TelemetrySession.jsm that fails without parent keyed + // histograms will fail without child keyed histograms. + nsCString contentId(id); + contentId.AppendLiteral(CONTENT_HISTOGRAM_SUFFIX); + gKeyedHistograms.Put(contentId, + new KeyedHistogram(id, expiration, h.histogramType, + h.min, h.max, h.bucketCount, h.dataset)); + + + nsCString gpuId(id); + gpuId.AppendLiteral(GPU_HISTOGRAM_SUFFIX); + gKeyedHistograms.Put(gpuId, + new KeyedHistogram(id, expiration, h.histogramType, + h.min, h.max, h.bucketCount, h.dataset)); + } + } + + // Some Telemetry histograms depend on the value of C++ constants and hardcode + // their values in Histograms.json. + // We add static asserts here for those values to match so that future changes + // don't go unnoticed. + // TODO: Compare explicitly with gHistograms[].bucketCount here + // once we can make gHistograms constexpr (requires VS2015). + static_assert((JS::gcreason::NUM_TELEMETRY_REASONS == 100), + "NUM_TELEMETRY_REASONS is assumed to be a fixed value in Histograms.json." + " If this was an intentional change, update this assert with its value " + "and update the n_values for the following in Histograms.json: " + "GC_MINOR_REASON, GC_MINOR_REASON_LONG, GC_REASON_2"); + static_assert((mozilla::StartupTimeline::MAX_EVENT_ID == 16), + "MAX_EVENT_ID is assumed to be a fixed value in Histograms.json. If this" + " was an intentional change, update this assert with its value and update" + " the n_values for the following in Histograms.json:" + " STARTUP_MEASUREMENT_ERRORS"); + + gInitDone = true; +} + +void TelemetryHistogram::DeInitializeGlobalState() +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + gCanRecordBase = false; + gCanRecordExtended = false; + gHistogramMap.Clear(); + gKeyedHistograms.Clear(); + gAddonMap.Clear(); + gAccumulations = nullptr; + gKeyedAccumulations = nullptr; + if (gIPCTimer) { + NS_RELEASE(gIPCTimer); + } + gInitDone = false; +} + +#ifdef DEBUG +bool TelemetryHistogram::GlobalStateHasBeenInitialized() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return gInitDone; +} +#endif + +bool +TelemetryHistogram::CanRecordBase() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return internal_CanRecordBase(); +} + +void +TelemetryHistogram::SetCanRecordBase(bool b) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + gCanRecordBase = b; +} + +bool +TelemetryHistogram::CanRecordExtended() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return internal_CanRecordExtended(); +} + +void +TelemetryHistogram::SetCanRecordExtended(bool b) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + gCanRecordExtended = b; +} + + +void +TelemetryHistogram::InitHistogramRecordingEnabled() +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + const size_t length = mozilla::ArrayLength(kRecordingInitiallyDisabledIDs); + for (size_t i = 0; i < length; i++) { + internal_SetHistogramRecordingEnabled(kRecordingInitiallyDisabledIDs[i], + false); + } +} + +void +TelemetryHistogram::SetHistogramRecordingEnabled(mozilla::Telemetry::ID aID, + bool aEnabled) +{ + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_SetHistogramRecordingEnabled(aID, aEnabled); +} + + +nsresult +TelemetryHistogram::SetHistogramRecordingEnabled(const nsACString &id, + bool aEnabled) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + Histogram *h; + nsresult rv = internal_GetHistogramByName(id, &h); + if (NS_SUCCEEDED(rv)) { + h->SetRecordingEnabled(aEnabled); + return NS_OK; + } + + KeyedHistogram* keyed = internal_GetKeyedHistogramById(id); + if (keyed) { + keyed->SetRecordingEnabled(aEnabled); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + + +void +TelemetryHistogram::Accumulate(mozilla::Telemetry::ID aID, + uint32_t aSample) +{ + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_Accumulate(aID, aSample); +} + +void +TelemetryHistogram::Accumulate(mozilla::Telemetry::ID aID, + const nsCString& aKey, uint32_t aSample) +{ + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_Accumulate(aID, aKey, aSample); +} + +void +TelemetryHistogram::Accumulate(const char* name, uint32_t sample) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + mozilla::Telemetry::ID id; + nsresult rv = internal_GetHistogramEnumId(name, &id); + if (NS_FAILED(rv)) { + return; + } + internal_Accumulate(id, sample); +} + +void +TelemetryHistogram::Accumulate(const char* name, + const nsCString& key, uint32_t sample) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + mozilla::Telemetry::ID id; + nsresult rv = internal_GetHistogramEnumId(name, &id); + if (NS_SUCCEEDED(rv)) { + internal_Accumulate(id, key, sample); + } +} + +void +TelemetryHistogram::AccumulateCategorical(mozilla::Telemetry::ID aId, + const nsCString& label) +{ + if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + uint32_t labelId = 0; + if (NS_FAILED(gHistograms[aId].label_id(label.get(), &labelId))) { + return; + } + internal_Accumulate(aId, labelId); +} + +void +TelemetryHistogram::AccumulateChild(GeckoProcessType aProcessType, + const nsTArray& aAccumulations) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + for (uint32_t i = 0; i < aAccumulations.Length(); ++i) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + continue; + } + internal_AccumulateChild(aProcessType, aAccumulations[i].mId, aAccumulations[i].mSample); + } +} + +void +TelemetryHistogram::AccumulateChildKeyed(GeckoProcessType aProcessType, + const nsTArray& aAccumulations) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + for (uint32_t i = 0; i < aAccumulations.Length(); ++i) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + continue; + } + internal_AccumulateChildKeyed(aProcessType, + aAccumulations[i].mId, + aAccumulations[i].mKey, + aAccumulations[i].mSample); + } +} + +nsresult +TelemetryHistogram::GetHistogramById(const nsACString &name, JSContext *cx, + JS::MutableHandle ret) +{ + Histogram *h = nullptr; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + nsresult rv = internal_GetHistogramByName(name, &h); + if (NS_FAILED(rv)) + return rv; + } + // Runs without protection from |gTelemetryHistogramMutex| + return internal_WrapAndReturnHistogram(h, cx, ret); +} + +nsresult +TelemetryHistogram::GetKeyedHistogramById(const nsACString &name, + JSContext *cx, + JS::MutableHandle ret) +{ + KeyedHistogram* keyed = nullptr; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!gKeyedHistograms.Get(name, &keyed)) { + return NS_ERROR_FAILURE; + } + } + // Runs without protection from |gTelemetryHistogramMutex| + return internal_WrapAndReturnKeyedHistogram(keyed, cx, ret); +} + +const char* +TelemetryHistogram::GetHistogramName(mozilla::Telemetry::ID id) +{ + if (NS_WARN_IF(!internal_IsHistogramEnumId(id))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return nullptr; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + const HistogramInfo& h = gHistograms[id]; + return h.id(); +} + +nsresult +TelemetryHistogram::CreateHistogramSnapshots(JSContext *cx, + JS::MutableHandle ret, + bool subsession, + bool clearSubsession) +{ + // Runs without protection from |gTelemetryHistogramMutex| + JS::Rooted root_obj(cx, JS_NewPlainObject(cx)); + if (!root_obj) + return NS_ERROR_FAILURE; + ret.setObject(*root_obj); + + // Include the GPU process in histogram snapshots only if we actually tried + // to launch a process for it. + bool includeGPUProcess = false; + if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) { + includeGPUProcess = gpm->AttemptedGPUProcess(); + } + + // Ensure that all the HISTOGRAM_FLAG & HISTOGRAM_COUNT histograms have + // been created, so that their values are snapshotted. + for (size_t i = 0; i < mozilla::Telemetry::HistogramCount; ++i) { + if (gHistograms[i].keyed) { + continue; + } + const uint32_t type = gHistograms[i].histogramType; + if (type == nsITelemetry::HISTOGRAM_FLAG || + type == nsITelemetry::HISTOGRAM_COUNT) { + Histogram *h; + mozilla::DebugOnly rv; + mozilla::Telemetry::ID id = mozilla::Telemetry::ID(i); + + rv = internal_GetHistogramByEnumId(id, &h, GeckoProcessType_Default); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = internal_GetHistogramByEnumId(id, &h, GeckoProcessType_Content); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (includeGPUProcess) { + rv = internal_GetHistogramByEnumId(id, &h, GeckoProcessType_GPU); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + } + + StatisticsRecorder::Histograms hs; + StatisticsRecorder::GetHistograms(&hs); + + // We identify corrupt histograms first, rather than interspersing it + // in the loop below, to ensure that our corruption statistics don't + // depend on histogram enumeration order. + // + // Of course, we hope that all of these corruption-statistics + // histograms are not themselves corrupt... + internal_IdentifyCorruptHistograms(hs); + + // OK, now we can actually reflect things. + JS::Rooted hobj(cx); + for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) { + Histogram *h = *it; + if (!internal_ShouldReflectHistogram(h) || internal_IsEmpty(h) || + internal_IsExpired(h)) { + continue; + } + + Histogram* original = h; +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + if (subsession) { + h = internal_GetSubsessionHistogram(*h); + if (!h) { + continue; + } + } +#endif + + hobj = JS_NewPlainObject(cx); + if (!hobj) { + return NS_ERROR_FAILURE; + } + switch (internal_ReflectHistogramSnapshot(cx, hobj, h)) { + case REFLECT_CORRUPT: + // We can still hit this case even if ShouldReflectHistograms + // returns true. The histogram lies outside of our control + // somehow; just skip it. + continue; + case REFLECT_FAILURE: + return NS_ERROR_FAILURE; + case REFLECT_OK: + if (!JS_DefineProperty(cx, root_obj, original->histogram_name().c_str(), + hobj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + if (subsession && clearSubsession) { + h->Clear(); + } +#endif + } + return NS_OK; +} + +nsresult +TelemetryHistogram::RegisteredHistograms(uint32_t aDataset, uint32_t *aCount, + char*** aHistograms) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return internal_GetRegisteredHistogramIds(false, + aDataset, aCount, aHistograms); +} + +nsresult +TelemetryHistogram::RegisteredKeyedHistograms(uint32_t aDataset, + uint32_t *aCount, + char*** aHistograms) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return internal_GetRegisteredHistogramIds(true, + aDataset, aCount, aHistograms); +} + +nsresult +TelemetryHistogram::GetKeyedHistogramSnapshots(JSContext *cx, + JS::MutableHandle ret) +{ + // Runs without protection from |gTelemetryHistogramMutex| + JS::Rooted obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return NS_ERROR_FAILURE; + } + + for (auto iter = gKeyedHistograms.Iter(); !iter.Done(); iter.Next()) { + JS::RootedObject snapshot(cx, JS_NewPlainObject(cx)); + if (!snapshot) { + return NS_ERROR_FAILURE; + } + + if (!NS_SUCCEEDED(iter.Data()->GetJSSnapshot(cx, snapshot, false, false))) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineProperty(cx, obj, PromiseFlatCString(iter.Key()).get(), + snapshot, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + ret.setObject(*obj); + return NS_OK; +} + +nsresult +TelemetryHistogram::RegisterAddonHistogram(const nsACString &id, + const nsACString &name, + uint32_t histogramType, + uint32_t min, uint32_t max, + uint32_t bucketCount, + uint8_t optArgCount) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (histogramType == nsITelemetry::HISTOGRAM_EXPONENTIAL || + histogramType == nsITelemetry::HISTOGRAM_LINEAR) { + if (optArgCount != 3) { + return NS_ERROR_INVALID_ARG; + } + + // Sanity checks for histogram parameters. + if (min >= max) + return NS_ERROR_ILLEGAL_VALUE; + + if (bucketCount <= 2) + return NS_ERROR_ILLEGAL_VALUE; + + if (min < 1) + return NS_ERROR_ILLEGAL_VALUE; + } else { + min = 1; + max = 2; + bucketCount = 3; + } + + AddonEntryType *addonEntry = gAddonMap.GetEntry(id); + if (!addonEntry) { + addonEntry = gAddonMap.PutEntry(id); + if (MOZ_UNLIKELY(!addonEntry)) { + return NS_ERROR_OUT_OF_MEMORY; + } + addonEntry->mData = new AddonHistogramMapType(); + } + + AddonHistogramMapType *histogramMap = addonEntry->mData; + AddonHistogramEntryType *histogramEntry = histogramMap->GetEntry(name); + // Can't re-register the same histogram. + if (histogramEntry) { + return NS_ERROR_FAILURE; + } + + histogramEntry = histogramMap->PutEntry(name); + if (MOZ_UNLIKELY(!histogramEntry)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + AddonHistogramInfo &info = histogramEntry->mData; + info.min = min; + info.max = max; + info.bucketCount = bucketCount; + info.histogramType = histogramType; + + return NS_OK; +} + +nsresult +TelemetryHistogram::GetAddonHistogram(const nsACString &id, + const nsACString &name, + JSContext *cx, + JS::MutableHandle ret) +{ + AddonHistogramInfo* info = nullptr; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + AddonEntryType *addonEntry = gAddonMap.GetEntry(id); + // The given id has not been registered. + if (!addonEntry) { + return NS_ERROR_INVALID_ARG; + } + + AddonHistogramMapType *histogramMap = addonEntry->mData; + AddonHistogramEntryType *histogramEntry = histogramMap->GetEntry(name); + // The given histogram name has not been registered. + if (!histogramEntry) { + return NS_ERROR_INVALID_ARG; + } + + info = &histogramEntry->mData; + if (!info->h) { + nsAutoCString actualName; + internal_AddonHistogramName(id, name, actualName); + if (!internal_CreateHistogramForAddon(actualName, *info)) { + return NS_ERROR_FAILURE; + } + } + } + + // Runs without protection from |gTelemetryHistogramMutex| + return internal_WrapAndReturnHistogram(info->h, cx, ret); +} + +nsresult +TelemetryHistogram::UnregisterAddonHistograms(const nsACString &id) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + AddonEntryType *addonEntry = gAddonMap.GetEntry(id); + if (addonEntry) { + // Histogram's destructor is private, so this is the best we can do. + // The histograms the addon created *will* stick around, but they + // will be deleted if and when the addon registers histograms with + // the same names. + delete addonEntry->mData; + gAddonMap.RemoveEntry(addonEntry); + } + + return NS_OK; +} + +nsresult +TelemetryHistogram::GetAddonHistogramSnapshots(JSContext *cx, + JS::MutableHandle ret) +{ + // Runs without protection from |gTelemetryHistogramMutex| + JS::Rooted obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return NS_ERROR_FAILURE; + } + + if (!gAddonMap.ReflectIntoJS(internal_AddonReflector, cx, obj)) { + return NS_ERROR_FAILURE; + } + ret.setObject(*obj); + return NS_OK; +} + +size_t +TelemetryHistogram::GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf + aMallocSizeOf) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return gAddonMap.ShallowSizeOfExcludingThis(aMallocSizeOf) + + gHistogramMap.ShallowSizeOfExcludingThis(aMallocSizeOf); +} + +size_t +TelemetryHistogram::GetHistogramSizesofIncludingThis(mozilla::MallocSizeOf + aMallocSizeOf) +{ + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + StatisticsRecorder::Histograms hs; + StatisticsRecorder::GetHistograms(&hs); + size_t n = 0; + for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) { + Histogram *h = *it; + n += h->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +// This method takes the lock only to double-buffer the batched telemetry. +// It releases the lock before calling out to IPC code which can (and does) +// Accumulate (which would deadlock) +// +// To ensure we don't loop IPCTimerFired->AccumulateChild->arm timer, we don't +// unset gIPCTimerArmed until the IPC completes +// +// This function must be called on the main thread, otherwise IPC will fail. +void +TelemetryHistogram::IPCTimerFired(nsITimer* aTimer, void* aClosure) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsTArray accumulationsToSend; + nsTArray keyedAccumulationsToSend; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (gAccumulations) { + accumulationsToSend.SwapElements(*gAccumulations); + } + if (gKeyedAccumulations) { + keyedAccumulationsToSend.SwapElements(*gKeyedAccumulations); + } + } + + switch (XRE_GetProcessType()) { + case GeckoProcessType_Content: { + mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton(); + mozilla::Unused << NS_WARN_IF(!contentChild); + if (contentChild) { + if (accumulationsToSend.Length()) { + mozilla::Unused << + NS_WARN_IF(!contentChild->SendAccumulateChildHistogram(accumulationsToSend)); + } + if (keyedAccumulationsToSend.Length()) { + mozilla::Unused << + NS_WARN_IF(!contentChild->SendAccumulateChildKeyedHistogram(keyedAccumulationsToSend)); + } + } + break; + } + case GeckoProcessType_GPU: { + if (mozilla::gfx::GPUParent* gpu = mozilla::gfx::GPUParent::GetSingleton()) { + if (accumulationsToSend.Length()) { + mozilla::Unused << gpu->SendAccumulateChildHistogram(accumulationsToSend); + } + if (keyedAccumulationsToSend.Length()) { + mozilla::Unused << gpu->SendAccumulateChildKeyedHistogram(keyedAccumulationsToSend); + } + } + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Unsupported process type"); + break; + } + + gIPCTimerArmed = false; +} diff --git a/toolkit/components/telemetry/TelemetryHistogram.h b/toolkit/components/telemetry/TelemetryHistogram.h new file mode 100644 index 000000000..4aa13e259 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryHistogram.h @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef TelemetryHistogram_h__ +#define TelemetryHistogram_h__ + +#include "mozilla/TelemetryHistogramEnums.h" + +#include "mozilla/TelemetryComms.h" +#include "nsXULAppAPI.h" + +// This module is internal to Telemetry. It encapsulates Telemetry's +// histogram accumulation and storage logic. It should only be used by +// Telemetry.cpp. These functions should not be used anywhere else. +// For the public interface to Telemetry functionality, see Telemetry.h. + +namespace TelemetryHistogram { + +void CreateStatisticsRecorder(); +void DestroyStatisticsRecorder(); + +void InitializeGlobalState(bool canRecordBase, bool canRecordExtended); +void DeInitializeGlobalState(); +#ifdef DEBUG +bool GlobalStateHasBeenInitialized(); +#endif + +bool CanRecordBase(); +void SetCanRecordBase(bool b); +bool CanRecordExtended(); +void SetCanRecordExtended(bool b); + +void InitHistogramRecordingEnabled(); +void SetHistogramRecordingEnabled(mozilla::Telemetry::ID aID, bool aEnabled); + +nsresult SetHistogramRecordingEnabled(const nsACString &id, bool aEnabled); + +void Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample); +void Accumulate(mozilla::Telemetry::ID aID, const nsCString& aKey, + uint32_t aSample); +void Accumulate(const char* name, uint32_t sample); +void Accumulate(const char* name, const nsCString& key, uint32_t sample); + +void AccumulateCategorical(mozilla::Telemetry::ID aId, const nsCString& aLabel); + +void AccumulateChild(GeckoProcessType aProcessType, + const nsTArray& aAccumulations); +void AccumulateChildKeyed(GeckoProcessType aProcessType, + const nsTArray& aAccumulations); + +nsresult +GetHistogramById(const nsACString &name, JSContext *cx, + JS::MutableHandle ret); + +nsresult +GetKeyedHistogramById(const nsACString &name, JSContext *cx, + JS::MutableHandle ret); + +const char* +GetHistogramName(mozilla::Telemetry::ID id); + +nsresult +CreateHistogramSnapshots(JSContext *cx, JS::MutableHandle ret, + bool subsession, bool clearSubsession); + +nsresult +RegisteredHistograms(uint32_t aDataset, uint32_t *aCount, + char*** aHistograms); + +nsresult +RegisteredKeyedHistograms(uint32_t aDataset, uint32_t *aCount, + char*** aHistograms); + +nsresult +GetKeyedHistogramSnapshots(JSContext *cx, JS::MutableHandle ret); + +nsresult +RegisterAddonHistogram(const nsACString &id, const nsACString &name, + uint32_t histogramType, uint32_t min, uint32_t max, + uint32_t bucketCount, uint8_t optArgCount); + +nsresult +GetAddonHistogram(const nsACString &id, const nsACString &name, + JSContext *cx, JS::MutableHandle ret); + +nsresult +UnregisterAddonHistograms(const nsACString &id); + +nsresult +GetAddonHistogramSnapshots(JSContext *cx, JS::MutableHandle ret); + +size_t +GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf); + +size_t +GetHistogramSizesofIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + +void +IPCTimerFired(nsITimer* aTimer, void* aClosure); +} // namespace TelemetryHistogram + +#endif // TelemetryHistogram_h__ diff --git a/toolkit/components/telemetry/TelemetryLog.jsm b/toolkit/components/telemetry/TelemetryLog.jsm new file mode 100644 index 000000000..ab62f195b --- /dev/null +++ b/toolkit/components/telemetry/TelemetryLog.jsm @@ -0,0 +1,35 @@ +/* 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/. */ + +this.EXPORTED_SYMBOLS = ["TelemetryLog"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry); +var gLogEntries = []; + +this.TelemetryLog = Object.freeze({ + log: function(id, data) { + id = String(id); + var ts; + try { + ts = Math.floor(Telemetry.msSinceProcessStart()); + } catch (e) { + // If timestamp is screwed up, we just give up instead of making up + // data. + return; + } + + var entry = [id, ts]; + if (data !== undefined) { + entry = entry.concat(Array.prototype.map.call(data, String)); + } + gLogEntries.push(entry); + }, + + entries: function() { + return gLogEntries; + } +}); diff --git a/toolkit/components/telemetry/TelemetryReportingPolicy.jsm b/toolkit/components/telemetry/TelemetryReportingPolicy.jsm new file mode 100644 index 000000000..d9c99df49 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryReportingPolicy.jsm @@ -0,0 +1,496 @@ +/* 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 = [ + "TelemetryReportingPolicy" +]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/Timer.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://services-common/observers.js", this); + +XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySend", + "resource://gre/modules/TelemetrySend.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryReportingPolicy::"; + +// Oldest year to allow in date preferences. The FHR infobar was implemented in +// 2012 and no dates older than that should be encountered. +const OLDEST_ALLOWED_ACCEPTANCE_YEAR = 2012; + +const PREF_BRANCH = "datareporting.policy."; +// Indicates whether this is the first run or not. This is used to decide when to display +// the policy. +const PREF_FIRST_RUN = "toolkit.telemetry.reportingpolicy.firstRun"; +// Allows to skip the datachoices infobar. This should only be used in tests. +const PREF_BYPASS_NOTIFICATION = PREF_BRANCH + "dataSubmissionPolicyBypassNotification"; +// The submission kill switch: if this preference is disable, no submission will ever take place. +const PREF_DATA_SUBMISSION_ENABLED = PREF_BRANCH + "dataSubmissionEnabled"; +// This preference holds the current policy version, which overrides +// DEFAULT_DATAREPORTING_POLICY_VERSION +const PREF_CURRENT_POLICY_VERSION = PREF_BRANCH + "currentPolicyVersion"; +// This indicates the minimum required policy version. If the accepted policy version +// is lower than this, the notification bar must be showed again. +const PREF_MINIMUM_POLICY_VERSION = PREF_BRANCH + "minimumPolicyVersion"; +// The version of the accepted policy. +const PREF_ACCEPTED_POLICY_VERSION = PREF_BRANCH + "dataSubmissionPolicyAcceptedVersion"; +// The date user accepted the policy. +const PREF_ACCEPTED_POLICY_DATE = PREF_BRANCH + "dataSubmissionPolicyNotifiedTime"; +// URL of privacy policy to be opened in a background tab on first run instead of showing the +// data choices infobar. +const PREF_FIRST_RUN_URL = PREF_BRANCH + "firstRunURL"; +// The following preferences are deprecated and will be purged during the preferences +// migration process. +const DEPRECATED_FHR_PREFS = [ + PREF_BRANCH + "dataSubmissionPolicyAccepted", + PREF_BRANCH + "dataSubmissionPolicyBypassAcceptance", + PREF_BRANCH + "dataSubmissionPolicyResponseType", + PREF_BRANCH + "dataSubmissionPolicyResponseTime" +]; + +// How much time until we display the data choices notification bar, on the first run. +const NOTIFICATION_DELAY_FIRST_RUN_MSEC = 60 * 1000; // 60s +// Same as above, for the next runs. +const NOTIFICATION_DELAY_NEXT_RUNS_MSEC = 10 * 1000; // 10s + +/** + * This is a policy object used to override behavior within this module. + * Tests override properties on this object to allow for control of behavior + * that would otherwise be very hard to cover. + */ +var Policy = { + now: () => new Date(), + setShowInfobarTimeout: (callback, delayMs) => setTimeout(callback, delayMs), + clearShowInfobarTimeout: (id) => clearTimeout(id), +}; + +/** + * Represents a request to display data policy. + * + * Receivers of these instances are expected to call one or more of the on* + * functions when events occur. + * + * When one of these requests is received, the first thing a callee should do + * is present notification to the user of the data policy. When the notice + * is displayed to the user, the callee should call `onUserNotifyComplete`. + * + * If for whatever reason the callee could not display a notice, + * it should call `onUserNotifyFailed`. + * + * @param {Object} aLog The log object used to log the error in case of failures. + */ +function NotifyPolicyRequest(aLog) { + this._log = aLog; +} + +NotifyPolicyRequest.prototype = Object.freeze({ + /** + * Called when the user is notified of the policy. + */ + onUserNotifyComplete: function() { + return TelemetryReportingPolicyImpl._userNotified(); + }, + + /** + * Called when there was an error notifying the user about the policy. + * + * @param error + * (Error) Explains what went wrong. + */ + onUserNotifyFailed: function (error) { + this._log.error("onUserNotifyFailed - " + error); + }, +}); + +this.TelemetryReportingPolicy = { + // The current policy version number. If the version number stored in the prefs + // is smaller than this, data upload will be disabled until the user is re-notified + // about the policy changes. + DEFAULT_DATAREPORTING_POLICY_VERSION: 1, + + /** + * Setup the policy. + */ + setup: function() { + return TelemetryReportingPolicyImpl.setup(); + }, + + /** + * Shutdown and clear the policy. + */ + shutdown: function() { + return TelemetryReportingPolicyImpl.shutdown(); + }, + + /** + * Check if we are allowed to upload data. In order to submit data both these conditions + * should be true: + * - The data submission preference should be true. + * - The datachoices infobar should have been displayed. + * + * @return {Boolean} True if we are allowed to upload data, false otherwise. + */ + canUpload: function() { + return TelemetryReportingPolicyImpl.canUpload(); + }, + + /** + * Test only method, restarts the policy. + */ + reset: function() { + return TelemetryReportingPolicyImpl.reset(); + }, + + /** + * Test only method, used to check if user is notified of the policy in tests. + */ + testIsUserNotified: function() { + return TelemetryReportingPolicyImpl.isUserNotifiedOfCurrentPolicy; + }, + + /** + * Test only method, used to simulate the infobar being shown in xpcshell tests. + */ + testInfobarShown: function() { + return TelemetryReportingPolicyImpl._userNotified(); + }, +}; + +var TelemetryReportingPolicyImpl = { + _logger: null, + // Keep track of the notification status if user wasn't notified already. + _notificationInProgress: false, + // The timer used to show the datachoices notification at startup. + _startupNotificationTimerId: null, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + + return this._logger; + }, + + /** + * Get the date the policy was notified. + * @return {Object} A date object or null on errors. + */ + get dataSubmissionPolicyNotifiedDate() { + let prefString = Preferences.get(PREF_ACCEPTED_POLICY_DATE, "0"); + let valueInteger = parseInt(prefString, 10); + + // Bail out if we didn't store any value yet. + if (valueInteger == 0) { + this._log.info("get dataSubmissionPolicyNotifiedDate - No date stored yet."); + return null; + } + + // If an invalid value is saved in the prefs, bail out too. + if (Number.isNaN(valueInteger)) { + this._log.error("get dataSubmissionPolicyNotifiedDate - Invalid date stored."); + return null; + } + + // Make sure the notification date is newer then the oldest allowed date. + let date = new Date(valueInteger); + if (date.getFullYear() < OLDEST_ALLOWED_ACCEPTANCE_YEAR) { + this._log.error("get dataSubmissionPolicyNotifiedDate - The stored date is too old."); + return null; + } + + return date; + }, + + /** + * Set the date the policy was notified. + * @param {Object} aDate A valid date object. + */ + set dataSubmissionPolicyNotifiedDate(aDate) { + this._log.trace("set dataSubmissionPolicyNotifiedDate - aDate: " + aDate); + + if (!aDate || aDate.getFullYear() < OLDEST_ALLOWED_ACCEPTANCE_YEAR) { + this._log.error("set dataSubmissionPolicyNotifiedDate - Invalid notification date."); + return; + } + + Preferences.set(PREF_ACCEPTED_POLICY_DATE, aDate.getTime().toString()); + }, + + /** + * Whether submission of data is allowed. + * + * This is the master switch for remote server communication. If it is + * false, we never request upload or deletion. + */ + get dataSubmissionEnabled() { + // Default is true because we are opt-out. + return Preferences.get(PREF_DATA_SUBMISSION_ENABLED, true); + }, + + get currentPolicyVersion() { + return Preferences.get(PREF_CURRENT_POLICY_VERSION, + TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION); + }, + + /** + * The minimum policy version which for dataSubmissionPolicyAccepted to + * to be valid. + */ + get minimumPolicyVersion() { + const minPolicyVersion = Preferences.get(PREF_MINIMUM_POLICY_VERSION, 1); + + // First check if the current channel has a specific minimum policy version. If not, + // use the general minimum policy version. + let channel = ""; + try { + channel = UpdateUtils.getUpdateChannel(false); + } catch (e) { + this._log.error("minimumPolicyVersion - Unable to retrieve the current channel."); + return minPolicyVersion; + } + const channelPref = PREF_MINIMUM_POLICY_VERSION + ".channel-" + channel; + return Preferences.get(channelPref, minPolicyVersion); + }, + + get dataSubmissionPolicyAcceptedVersion() { + return Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0); + }, + + set dataSubmissionPolicyAcceptedVersion(value) { + Preferences.set(PREF_ACCEPTED_POLICY_VERSION, value); + }, + + /** + * Checks to see if the user has been notified about data submission + * @return {Bool} True if user has been notified and the notification is still valid, + * false otherwise. + */ + get isUserNotifiedOfCurrentPolicy() { + // If we don't have a sane notification date, the user was not notified yet. + if (!this.dataSubmissionPolicyNotifiedDate || + this.dataSubmissionPolicyNotifiedDate.getTime() <= 0) { + return false; + } + + // The accepted policy version should not be less than the minimum policy version. + if (this.dataSubmissionPolicyAcceptedVersion < this.minimumPolicyVersion) { + return false; + } + + // Otherwise the user was already notified. + return true; + }, + + /** + * Test only method, restarts the policy. + */ + reset: function() { + this.shutdown(); + return this.setup(); + }, + + /** + * Setup the policy. + */ + setup: function() { + this._log.trace("setup"); + + // Migrate the data choices infobar, if needed. + this._migratePreferences(); + + // Add the event observers. + Services.obs.addObserver(this, "sessionstore-windows-restored", false); + }, + + /** + * Clean up the reporting policy. + */ + shutdown: function() { + this._log.trace("shutdown"); + + this._detachObservers(); + + Policy.clearShowInfobarTimeout(this._startupNotificationTimerId); + }, + + /** + * Detach the observers that were attached during setup. + */ + _detachObservers: function() { + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + }, + + /** + * Check if we are allowed to upload data. In order to submit data both these conditions + * should be true: + * - The data submission preference should be true. + * - The datachoices infobar should have been displayed. + * + * @return {Boolean} True if we are allowed to upload data, false otherwise. + */ + canUpload: function() { + // If data submission is disabled, there's no point in showing the infobar. Just + // forbid to upload. + if (!this.dataSubmissionEnabled) { + return false; + } + + // Submission is enabled. We enable upload if user is notified or we need to bypass + // the policy. + const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false); + return this.isUserNotifiedOfCurrentPolicy || bypassNotification; + }, + + /** + * Migrate the data policy preferences, if needed. + */ + _migratePreferences: function() { + // Current prefs are mostly the same than the old ones, except for some deprecated ones. + for (let pref of DEPRECATED_FHR_PREFS) { + Preferences.reset(pref); + } + }, + + /** + * Show the data choices infobar if the user wasn't already notified and data submission + * is enabled. + */ + _showInfobar: function() { + if (!this.dataSubmissionEnabled) { + this._log.trace("_showInfobar - Data submission disabled by the policy."); + return; + } + + const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false); + if (this.isUserNotifiedOfCurrentPolicy || bypassNotification) { + this._log.trace("_showInfobar - User already notified or bypassing the policy."); + return; + } + + if (this._notificationInProgress) { + this._log.trace("_showInfobar - User not notified, notification already in progress."); + return; + } + + this._log.trace("_showInfobar - User not notified, notifying now."); + this._notificationInProgress = true; + let request = new NotifyPolicyRequest(this._log); + Observers.notify("datareporting:notify-data-policy:request", request); + }, + + /** + * Called when the user is notified with the infobar or otherwise. + */ + _userNotified() { + this._log.trace("_userNotified"); + this._recordNotificationData(); + TelemetrySend.notifyCanUpload(); + }, + + /** + * Record date and the version of the accepted policy. + */ + _recordNotificationData: function() { + this._log.trace("_recordNotificationData"); + this.dataSubmissionPolicyNotifiedDate = Policy.now(); + this.dataSubmissionPolicyAcceptedVersion = this.currentPolicyVersion; + // The user was notified and the notification data saved: the notification + // is no longer in progress. + this._notificationInProgress = false; + }, + + /** + * Try to open the privacy policy in a background tab instead of showing the infobar. + */ + _openFirstRunPage() { + let firstRunPolicyURL = Preferences.get(PREF_FIRST_RUN_URL, ""); + if (!firstRunPolicyURL) { + return false; + } + firstRunPolicyURL = Services.urlFormatter.formatURL(firstRunPolicyURL); + + let win; + try { + const { RecentWindow } = Cu.import("resource:///modules/RecentWindow.jsm", {}); + win = RecentWindow.getMostRecentBrowserWindow(); + } catch (e) {} + + if (!win) { + this._log.info("Couldn't find browser window to open first-run page. Falling back to infobar."); + return false; + } + + // We'll consider the user notified once the privacy policy has been loaded + // in a background tab even if that tab hasn't been selected. + let tab; + let progressListener = {}; + progressListener.onStateChange = + (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) => { + if (aWebProgress.isTopLevel && + tab && + tab.linkedBrowser == aBrowser && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + let uri = aBrowser.documentURI; + if (uri && !/^about:(blank|neterror|certerror|blocked)/.test(uri.spec)) { + this._userNotified(); + } else { + this._log.info("Failed to load first-run page. Falling back to infobar."); + this._showInfobar(); + } + removeListeners(); + } + }; + + let removeListeners = () => { + win.removeEventListener("unload", removeListeners); + win.gBrowser.removeTabsProgressListener(progressListener); + }; + + win.addEventListener("unload", removeListeners); + win.gBrowser.addTabsProgressListener(progressListener); + + tab = win.gBrowser.loadOneTab(firstRunPolicyURL, { inBackground: true }); + + return true; + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic != "sessionstore-windows-restored") { + return; + } + + const isFirstRun = Preferences.get(PREF_FIRST_RUN, true); + if (isFirstRun) { + // We're performing the first run, flip firstRun preference for subsequent runs. + Preferences.set(PREF_FIRST_RUN, false); + + try { + if (this._openFirstRunPage()) { + return; + } + } catch (e) { + this._log.error("Failed to open privacy policy tab: " + e); + } + } + + // Show the info bar. + const delay = + isFirstRun ? NOTIFICATION_DELAY_FIRST_RUN_MSEC: NOTIFICATION_DELAY_NEXT_RUNS_MSEC; + + this._startupNotificationTimerId = Policy.setShowInfobarTimeout( + // Calling |canUpload| eventually shows the infobar, if needed. + () => this._showInfobar(), delay); + }, +}; diff --git a/toolkit/components/telemetry/TelemetryScalar.cpp b/toolkit/components/telemetry/TelemetryScalar.cpp new file mode 100644 index 000000000..6e9558070 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryScalar.cpp @@ -0,0 +1,1896 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsITelemetry.h" +#include "nsIVariant.h" +#include "nsVariant.h" +#include "nsHashKeys.h" +#include "nsBaseHashtable.h" +#include "nsClassHashtable.h" +#include "nsIXPConnect.h" +#include "nsContentUtils.h" +#include "nsThreadUtils.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Unused.h" + +#include "TelemetryCommon.h" +#include "TelemetryScalar.h" +#include "TelemetryScalarData.h" + +using mozilla::StaticMutex; +using mozilla::StaticMutexAutoLock; +using mozilla::Telemetry::Common::AutoHashtable; +using mozilla::Telemetry::Common::IsExpiredVersion; +using mozilla::Telemetry::Common::CanRecordDataset; +using mozilla::Telemetry::Common::IsInDataset; +using mozilla::Telemetry::Common::LogToBrowserConsole; + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// Naming: there are two kinds of functions in this file: +// +// * Functions named internal_*: these can only be reached via an +// interface function (TelemetryScalar::*). They expect the interface +// function to have acquired |gTelemetryScalarsMutex|, so they do not +// have to be thread-safe. +// +// * Functions named TelemetryScalar::*. This is the external interface. +// Entries and exits to these functions are serialised using +// |gTelemetryScalarsMutex|. +// +// Avoiding races and deadlocks: +// +// All functions in the external interface (TelemetryScalar::*) are +// serialised using the mutex |gTelemetryScalarsMutex|. This means +// that the external interface is thread-safe, and many of the +// internal_* functions can ignore thread safety. But it also brings +// a danger of deadlock if any function in the external interface can +// get back to that interface. That is, we will deadlock on any call +// chain like this +// +// TelemetryScalar::* -> .. any functions .. -> TelemetryScalar::* +// +// To reduce the danger of that happening, observe the following rules: +// +// * No function in TelemetryScalar::* may directly call, nor take the +// address of, any other function in TelemetryScalar::*. +// +// * No internal function internal_* may call, nor take the address +// of, any function in TelemetryScalar::*. + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE TYPES + +namespace { + +const uint32_t kMaximumNumberOfKeys = 100; +const uint32_t kMaximumKeyStringLength = 70; +const uint32_t kMaximumStringValueLength = 50; +const uint32_t kScalarCount = + static_cast(mozilla::Telemetry::ScalarID::ScalarCount); + +enum class ScalarResult : uint8_t { + // Nothing went wrong. + Ok, + // General Scalar Errors + OperationNotSupported, + InvalidType, + InvalidValue, + // Keyed Scalar Errors + KeyTooLong, + TooManyKeys, + // String Scalar Errors + StringTooLong, + // Unsigned Scalar Errors + UnsignedNegativeValue, + UnsignedTruncatedValue +}; + +typedef nsBaseHashtableET + CharPtrEntryType; + +typedef AutoHashtable ScalarMapType; + +/** + * Map the error codes used internally to NS_* error codes. + * @param aSr The error code used internally in this module. + * @return {nsresult} A NS_* error code. + */ +nsresult +MapToNsResult(ScalarResult aSr) +{ + switch (aSr) { + case ScalarResult::Ok: + return NS_OK; + case ScalarResult::OperationNotSupported: + return NS_ERROR_NOT_AVAILABLE; + case ScalarResult::StringTooLong: + // We don't want to throw if we're setting a string that is too long. + return NS_OK; + case ScalarResult::InvalidType: + case ScalarResult::InvalidValue: + case ScalarResult::KeyTooLong: + return NS_ERROR_ILLEGAL_VALUE; + case ScalarResult::TooManyKeys: + return NS_ERROR_FAILURE; + case ScalarResult::UnsignedNegativeValue: + case ScalarResult::UnsignedTruncatedValue: + // We shouldn't throw if trying to set a negative number or are truncated, + // only warn the user. + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +bool +IsValidEnumId(mozilla::Telemetry::ScalarID aID) +{ + return aID < mozilla::Telemetry::ScalarID::ScalarCount; +} + +// Implements the methods for ScalarInfo. +const char * +ScalarInfo::name() const +{ + return &gScalarsStringTable[this->name_offset]; +} + +const char * +ScalarInfo::expiration() const +{ + return &gScalarsStringTable[this->expiration_offset]; +} + +/** + * The base scalar object, that servers as a common ancestor for storage + * purposes. + */ +class ScalarBase +{ +public: + virtual ~ScalarBase() {}; + + // Set, Add and SetMaximum functions as described in the Telemetry IDL. + virtual ScalarResult SetValue(nsIVariant* aValue) = 0; + virtual ScalarResult AddValue(nsIVariant* aValue) { return ScalarResult::OperationNotSupported; } + virtual ScalarResult SetMaximum(nsIVariant* aValue) { return ScalarResult::OperationNotSupported; } + + // Convenience methods used by the C++ API. + virtual void SetValue(uint32_t aValue) { mozilla::Unused << HandleUnsupported(); } + virtual ScalarResult SetValue(const nsAString& aValue) { return HandleUnsupported(); } + virtual void SetValue(bool aValue) { mozilla::Unused << HandleUnsupported(); } + virtual void AddValue(uint32_t aValue) { mozilla::Unused << HandleUnsupported(); } + virtual void SetMaximum(uint32_t aValue) { mozilla::Unused << HandleUnsupported(); } + + // GetValue is used to get the value of the scalar when persisting it to JS. + virtual nsresult GetValue(nsCOMPtr& aResult) const = 0; + + // To measure the memory stats. + virtual size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const = 0; + +private: + ScalarResult HandleUnsupported() const; +}; + +ScalarResult +ScalarBase::HandleUnsupported() const +{ + MOZ_ASSERT(false, "This operation is not support for this scalar type."); + return ScalarResult::OperationNotSupported; +} + +/** + * The implementation for the unsigned int scalar type. + */ +class ScalarUnsigned : public ScalarBase +{ +public: + using ScalarBase::SetValue; + + ScalarUnsigned() : mStorage(0) {}; + ~ScalarUnsigned() {}; + + ScalarResult SetValue(nsIVariant* aValue) final; + void SetValue(uint32_t aValue) final; + ScalarResult AddValue(nsIVariant* aValue) final; + void AddValue(uint32_t aValue) final; + ScalarResult SetMaximum(nsIVariant* aValue) final; + void SetMaximum(uint32_t aValue) final; + nsresult GetValue(nsCOMPtr& aResult) const final; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; + +private: + uint32_t mStorage; + + ScalarResult CheckInput(nsIVariant* aValue); + + // Prevent copying. + ScalarUnsigned(const ScalarUnsigned& aOther) = delete; + void operator=(const ScalarUnsigned& aOther) = delete; +}; + +ScalarResult +ScalarUnsigned::SetValue(nsIVariant* aValue) +{ + ScalarResult sr = CheckInput(aValue); + if (sr == ScalarResult::UnsignedNegativeValue) { + return sr; + } + + if (NS_FAILED(aValue->GetAsUint32(&mStorage))) { + return ScalarResult::InvalidValue; + } + return sr; +} + +void +ScalarUnsigned::SetValue(uint32_t aValue) +{ + mStorage = aValue; +} + +ScalarResult +ScalarUnsigned::AddValue(nsIVariant* aValue) +{ + ScalarResult sr = CheckInput(aValue); + if (sr == ScalarResult::UnsignedNegativeValue) { + return sr; + } + + uint32_t newAddend = 0; + nsresult rv = aValue->GetAsUint32(&newAddend); + if (NS_FAILED(rv)) { + return ScalarResult::InvalidValue; + } + mStorage += newAddend; + return sr; +} + +void +ScalarUnsigned::AddValue(uint32_t aValue) +{ + mStorage += aValue; +} + +ScalarResult +ScalarUnsigned::SetMaximum(nsIVariant* aValue) +{ + ScalarResult sr = CheckInput(aValue); + if (sr == ScalarResult::UnsignedNegativeValue) { + return sr; + } + + uint32_t newValue = 0; + nsresult rv = aValue->GetAsUint32(&newValue); + if (NS_FAILED(rv)) { + return ScalarResult::InvalidValue; + } + if (newValue > mStorage) { + mStorage = newValue; + } + return sr; +} + +void +ScalarUnsigned::SetMaximum(uint32_t aValue) +{ + if (aValue > mStorage) { + mStorage = aValue; + } +} + +nsresult +ScalarUnsigned::GetValue(nsCOMPtr& aResult) const +{ + nsCOMPtr outVar(new nsVariant()); + nsresult rv = outVar->SetAsUint32(mStorage); + if (NS_FAILED(rv)) { + return rv; + } + aResult = outVar.forget(); + return NS_OK; +} + +size_t +ScalarUnsigned::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + return aMallocSizeOf(this); +} + +ScalarResult +ScalarUnsigned::CheckInput(nsIVariant* aValue) +{ + // If this is a floating point value/double, we will probably get truncated. + uint16_t type; + aValue->GetDataType(&type); + if (type == nsIDataType::VTYPE_FLOAT || + type == nsIDataType::VTYPE_DOUBLE) { + return ScalarResult::UnsignedTruncatedValue; + } + + int32_t signedTest; + // If we're able to cast the number to an int, check its sign. + // Warn the user if he's trying to set the unsigned scalar to a negative + // number. + if (NS_SUCCEEDED(aValue->GetAsInt32(&signedTest)) && + signedTest < 0) { + return ScalarResult::UnsignedNegativeValue; + } + return ScalarResult::Ok; +} + +/** + * The implementation for the string scalar type. + */ +class ScalarString : public ScalarBase +{ +public: + using ScalarBase::SetValue; + + ScalarString() : mStorage(EmptyString()) {}; + ~ScalarString() {}; + + ScalarResult SetValue(nsIVariant* aValue) final; + ScalarResult SetValue(const nsAString& aValue) final; + nsresult GetValue(nsCOMPtr& aResult) const final; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; + +private: + nsString mStorage; + + // Prevent copying. + ScalarString(const ScalarString& aOther) = delete; + void operator=(const ScalarString& aOther) = delete; +}; + +ScalarResult +ScalarString::SetValue(nsIVariant* aValue) +{ + // Check that we got the correct data type. + uint16_t type; + aValue->GetDataType(&type); + if (type != nsIDataType::VTYPE_CHAR && + type != nsIDataType::VTYPE_WCHAR && + type != nsIDataType::VTYPE_DOMSTRING && + type != nsIDataType::VTYPE_CHAR_STR && + type != nsIDataType::VTYPE_WCHAR_STR && + type != nsIDataType::VTYPE_STRING_SIZE_IS && + type != nsIDataType::VTYPE_WSTRING_SIZE_IS && + type != nsIDataType::VTYPE_UTF8STRING && + type != nsIDataType::VTYPE_CSTRING && + type != nsIDataType::VTYPE_ASTRING) { + return ScalarResult::InvalidType; + } + + nsAutoString convertedString; + nsresult rv = aValue->GetAsAString(convertedString); + if (NS_FAILED(rv)) { + return ScalarResult::InvalidValue; + } + return SetValue(convertedString); +}; + +ScalarResult +ScalarString::SetValue(const nsAString& aValue) +{ + mStorage = Substring(aValue, 0, kMaximumStringValueLength); + if (aValue.Length() > kMaximumStringValueLength) { + return ScalarResult::StringTooLong; + } + return ScalarResult::Ok; +} + +nsresult +ScalarString::GetValue(nsCOMPtr& aResult) const +{ + nsCOMPtr outVar(new nsVariant()); + nsresult rv = outVar->SetAsAString(mStorage); + if (NS_FAILED(rv)) { + return rv; + } + aResult = outVar.forget(); + return NS_OK; +} + +size_t +ScalarString::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + size_t n = aMallocSizeOf(this); + n+= mStorage.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + return n; +} + +/** + * The implementation for the boolean scalar type. + */ +class ScalarBoolean : public ScalarBase +{ +public: + using ScalarBase::SetValue; + + ScalarBoolean() : mStorage(false) {}; + ~ScalarBoolean() {}; + + ScalarResult SetValue(nsIVariant* aValue) final; + void SetValue(bool aValue) final; + nsresult GetValue(nsCOMPtr& aResult) const final; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; + +private: + bool mStorage; + + // Prevent copying. + ScalarBoolean(const ScalarBoolean& aOther) = delete; + void operator=(const ScalarBoolean& aOther) = delete; +}; + +ScalarResult +ScalarBoolean::SetValue(nsIVariant* aValue) +{ + // Check that we got the correct data type. + uint16_t type; + aValue->GetDataType(&type); + if (type != nsIDataType::VTYPE_BOOL && + type != nsIDataType::VTYPE_INT8 && + type != nsIDataType::VTYPE_INT16 && + type != nsIDataType::VTYPE_INT32 && + type != nsIDataType::VTYPE_INT64 && + type != nsIDataType::VTYPE_UINT8 && + type != nsIDataType::VTYPE_UINT16 && + type != nsIDataType::VTYPE_UINT32 && + type != nsIDataType::VTYPE_UINT64) { + return ScalarResult::InvalidType; + } + + if (NS_FAILED(aValue->GetAsBool(&mStorage))) { + return ScalarResult::InvalidValue; + } + return ScalarResult::Ok; +}; + +void +ScalarBoolean::SetValue(bool aValue) +{ + mStorage = aValue; +} + +nsresult +ScalarBoolean::GetValue(nsCOMPtr& aResult) const +{ + nsCOMPtr outVar(new nsVariant()); + nsresult rv = outVar->SetAsBool(mStorage); + if (NS_FAILED(rv)) { + return rv; + } + aResult = outVar.forget(); + return NS_OK; +} + +size_t +ScalarBoolean::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + return aMallocSizeOf(this); +} + +/** + * Allocate a scalar class given the scalar info. + * + * @param aInfo The informations for the scalar coming from the definition file. + * @return nullptr if the scalar type is unknown, otherwise a valid pointer to the + * scalar type. + */ +ScalarBase* +internal_ScalarAllocate(uint32_t aScalarKind) +{ + ScalarBase* scalar = nullptr; + switch (aScalarKind) { + case nsITelemetry::SCALAR_COUNT: + scalar = new ScalarUnsigned(); + break; + case nsITelemetry::SCALAR_STRING: + scalar = new ScalarString(); + break; + case nsITelemetry::SCALAR_BOOLEAN: + scalar = new ScalarBoolean(); + break; + default: + MOZ_ASSERT(false, "Invalid scalar type"); + } + return scalar; +} + +/** + * The implementation for the keyed scalar type. + */ +class KeyedScalar +{ +public: + typedef mozilla::Pair> KeyValuePair; + + explicit KeyedScalar(uint32_t aScalarKind) : mScalarKind(aScalarKind) {}; + ~KeyedScalar() {}; + + // Set, Add and SetMaximum functions as described in the Telemetry IDL. + // These methods implicitly instantiate a Scalar[*] for each key. + ScalarResult SetValue(const nsAString& aKey, nsIVariant* aValue); + ScalarResult AddValue(const nsAString& aKey, nsIVariant* aValue); + ScalarResult SetMaximum(const nsAString& aKey, nsIVariant* aValue); + + // Convenience methods used by the C++ API. + void SetValue(const nsAString& aKey, uint32_t aValue); + void SetValue(const nsAString& aKey, bool aValue); + void AddValue(const nsAString& aKey, uint32_t aValue); + void SetMaximum(const nsAString& aKey, uint32_t aValue); + + // GetValue is used to get the key-value pairs stored in the keyed scalar + // when persisting it to JS. + nsresult GetValue(nsTArray& aValues) const; + + // To measure the memory stats. + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + +private: + typedef nsClassHashtable ScalarKeysMapType; + + ScalarKeysMapType mScalarKeys; + const uint32_t mScalarKind; + + ScalarResult GetScalarForKey(const nsAString& aKey, ScalarBase** aRet); +}; + +ScalarResult +KeyedScalar::SetValue(const nsAString& aKey, nsIVariant* aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + return sr; + } + + return scalar->SetValue(aValue); +} + +ScalarResult +KeyedScalar::AddValue(const nsAString& aKey, nsIVariant* aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + return sr; + } + + return scalar->AddValue(aValue); +} + +ScalarResult +KeyedScalar::SetMaximum(const nsAString& aKey, nsIVariant* aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + return sr; + } + + return scalar->SetMaximum(aValue); +} + +void +KeyedScalar::SetValue(const nsAString& aKey, uint32_t aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar."); + return; + } + + return scalar->SetValue(aValue); +} + +void +KeyedScalar::SetValue(const nsAString& aKey, bool aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar."); + return; + } + + return scalar->SetValue(aValue); +} + +void +KeyedScalar::AddValue(const nsAString& aKey, uint32_t aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar."); + return; + } + + return scalar->AddValue(aValue); +} + +void +KeyedScalar::SetMaximum(const nsAString& aKey, uint32_t aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar."); + return; + } + + return scalar->SetMaximum(aValue); +} + +/** + * Get a key-value array with the values for the Keyed Scalar. + * @param aValue The array that will hold the key-value pairs. + * @return {nsresult} NS_OK or an error value as reported by the + * the specific scalar objects implementations (e.g. + * ScalarUnsigned). + */ +nsresult +KeyedScalar::GetValue(nsTArray& aValues) const +{ + for (auto iter = mScalarKeys.ConstIter(); !iter.Done(); iter.Next()) { + ScalarBase* scalar = static_cast(iter.Data()); + + // Get the scalar value. + nsCOMPtr scalarValue; + nsresult rv = scalar->GetValue(scalarValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Append it to value list. + aValues.AppendElement(mozilla::MakePair(nsCString(iter.Key()), scalarValue)); + } + + return NS_OK; +} + +/** + * Get the scalar for the referenced key. + * If there's no such key, instantiate a new Scalar object with the + * same type of the Keyed scalar and create the key. + */ +ScalarResult +KeyedScalar::GetScalarForKey(const nsAString& aKey, ScalarBase** aRet) +{ + if (aKey.Length() >= kMaximumKeyStringLength) { + return ScalarResult::KeyTooLong; + } + + if (mScalarKeys.Count() >= kMaximumNumberOfKeys) { + return ScalarResult::TooManyKeys; + } + + NS_ConvertUTF16toUTF8 utf8Key(aKey); + + ScalarBase* scalar = nullptr; + if (mScalarKeys.Get(utf8Key, &scalar)) { + *aRet = scalar; + return ScalarResult::Ok; + } + + scalar = internal_ScalarAllocate(mScalarKind); + if (!scalar) { + return ScalarResult::InvalidType; + } + + mScalarKeys.Put(utf8Key, scalar); + + *aRet = scalar; + return ScalarResult::Ok; +} + +size_t +KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) +{ + size_t n = aMallocSizeOf(this); + for (auto iter = mScalarKeys.Iter(); !iter.Done(); iter.Next()) { + ScalarBase* scalar = static_cast(iter.Data()); + n += scalar->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +typedef nsUint32HashKey ScalarIDHashKey; +typedef nsClassHashtable ScalarStorageMapType; +typedef nsClassHashtable KeyedScalarStorageMapType; + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE STATE, SHARED BY ALL THREADS + +namespace { + +// Set to true once this global state has been initialized. +bool gInitDone = false; + +bool gCanRecordBase; +bool gCanRecordExtended; + +// The Name -> ID cache map. +ScalarMapType gScalarNameIDMap(kScalarCount); +// The ID -> Scalar Object map. This is a nsClassHashtable, it owns +// the scalar instance and takes care of deallocating them when they +// get removed from the map. +ScalarStorageMapType gScalarStorageMap; +// The ID -> Keyed Scalar Object map. As for plain scalars, this is +// nsClassHashtable. See above. +KeyedScalarStorageMapType gKeyedScalarStorageMap; + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Function that may call JS code. + +// NOTE: the functions in this section all run without protection from +// |gTelemetryScalarsMutex|. If they held the mutex, there would be the +// possibility of deadlock because the JS_ calls that they make may call +// back into the TelemetryScalar interface, hence trying to re-acquire the mutex. +// +// This means that these functions potentially race against threads, but +// that seems preferable to risking deadlock. + +namespace { + +/** + * Checks if the error should be logged. + * + * @param aSr The error code. + * @return true if the error should be logged, false otherwise. + */ +bool +internal_ShouldLogError(ScalarResult aSr) +{ + switch (aSr) { + case ScalarResult::StringTooLong: MOZ_FALLTHROUGH; + case ScalarResult::KeyTooLong: MOZ_FALLTHROUGH; + case ScalarResult::TooManyKeys: MOZ_FALLTHROUGH; + case ScalarResult::UnsignedNegativeValue: MOZ_FALLTHROUGH; + case ScalarResult::UnsignedTruncatedValue: + // Intentional fall-through. + return true; + + default: + return false; + } + + // It should never reach this point. + return false; +} + +/** + * Converts the error code to a human readable error message and prints it to the + * browser console. + * + * @param aScalarName The name of the scalar that raised the error. + * @param aSr The error code. + */ +void +internal_LogScalarError(const nsACString& aScalarName, ScalarResult aSr) +{ + nsAutoString errorMessage; + AppendUTF8toUTF16(aScalarName, errorMessage); + + switch (aSr) { + case ScalarResult::StringTooLong: + errorMessage.Append(NS_LITERAL_STRING(" - Truncating scalar value to 50 characters.")); + break; + case ScalarResult::KeyTooLong: + errorMessage.Append(NS_LITERAL_STRING(" - The key length must be limited to 70 characters.")); + break; + case ScalarResult::TooManyKeys: + errorMessage.Append(NS_LITERAL_STRING(" - Keyed scalars cannot have more than 100 keys.")); + break; + case ScalarResult::UnsignedNegativeValue: + errorMessage.Append(NS_LITERAL_STRING(" - Trying to set an unsigned scalar to a negative number.")); + break; + case ScalarResult::UnsignedTruncatedValue: + errorMessage.Append(NS_LITERAL_STRING(" - Truncating float/double number.")); + break; + default: + // Nothing. + return; + } + + LogToBrowserConsole(nsIScriptError::warningFlag, errorMessage); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: thread-unsafe helpers for the external interface + +namespace { + +bool +internal_CanRecordBase() +{ + return gCanRecordBase; +} + +bool +internal_CanRecordExtended() +{ + return gCanRecordExtended; +} + +const ScalarInfo& +internal_InfoForScalarID(mozilla::Telemetry::ScalarID aId) +{ + return gScalars[static_cast(aId)]; +} + +/** + * Check if the given scalar is a keyed scalar. + * + * @param aId The scalar enum. + * @return true if aId refers to a keyed scalar, false otherwise. + */ +bool +internal_IsKeyedScalar(mozilla::Telemetry::ScalarID aId) +{ + return internal_InfoForScalarID(aId).keyed; +} + +bool +internal_CanRecordForScalarID(mozilla::Telemetry::ScalarID aId) +{ + // Get the scalar info from the id. + const ScalarInfo &info = internal_InfoForScalarID(aId); + + // Can we record at all? + bool canRecordBase = internal_CanRecordBase(); + if (!canRecordBase) { + return false; + } + + bool canRecordDataset = CanRecordDataset(info.dataset, + canRecordBase, + internal_CanRecordExtended()); + if (!canRecordDataset) { + return false; + } + + return true; +} + +/** + * Get the scalar enum id from the scalar name. + * + * @param aName The scalar name. + * @param aId The output variable to contain the enum. + * @return + * NS_ERROR_FAILURE if this was called before init is completed. + * NS_ERROR_INVALID_ARG if the name can't be found in the scalar definitions. + * NS_OK if the scalar was found and aId contains a valid enum id. + */ +nsresult +internal_GetEnumByScalarName(const nsACString& aName, mozilla::Telemetry::ScalarID* aId) +{ + if (!gInitDone) { + return NS_ERROR_FAILURE; + } + + CharPtrEntryType *entry = gScalarNameIDMap.GetEntry(PromiseFlatCString(aName).get()); + if (!entry) { + return NS_ERROR_INVALID_ARG; + } + *aId = entry->mData; + return NS_OK; +} + +/** + * Get a scalar object by its enum id. This implicitly allocates the scalar + * object in the storage if it wasn't previously allocated. + * + * @param aId The scalar id. + * @param aRes The output variable that stores scalar object. + * @return + * NS_ERROR_INVALID_ARG if the scalar id is unknown. + * NS_ERROR_NOT_AVAILABLE if the scalar is expired. + * NS_OK if the scalar was found. If that's the case, aResult contains a + * valid pointer to a scalar type. + */ +nsresult +internal_GetScalarByEnum(mozilla::Telemetry::ScalarID aId, ScalarBase** aRet) +{ + if (!IsValidEnumId(aId)) { + MOZ_ASSERT(false, "Requested a scalar with an invalid id."); + return NS_ERROR_INVALID_ARG; + } + + const uint32_t id = static_cast(aId); + + ScalarBase* scalar = nullptr; + if (gScalarStorageMap.Get(id, &scalar)) { + *aRet = scalar; + return NS_OK; + } + + const ScalarInfo &info = gScalars[id]; + + if (IsExpiredVersion(info.expiration())) { + return NS_ERROR_NOT_AVAILABLE; + } + + scalar = internal_ScalarAllocate(info.kind); + if (!scalar) { + return NS_ERROR_INVALID_ARG; + } + + gScalarStorageMap.Put(id, scalar); + + *aRet = scalar; + return NS_OK; +} + +/** + * Get a scalar object by its enum id, if we're allowed to record it. + * + * @param aId The scalar id. + * @return The ScalarBase instance or nullptr if we're not allowed to record + * the scalar. + */ +ScalarBase* +internal_GetRecordableScalar(mozilla::Telemetry::ScalarID aId) +{ + // Get the scalar by the enum (it also internally checks for aId validity). + ScalarBase* scalar = nullptr; + nsresult rv = internal_GetScalarByEnum(aId, &scalar); + if (NS_FAILED(rv)) { + return nullptr; + } + + if (internal_IsKeyedScalar(aId)) { + return nullptr; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(aId)) { + return nullptr; + } + + return scalar; +} + +} // namespace + + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: thread-unsafe helpers for the keyed scalars + +namespace { + +/** + * Get a keyed scalar object by its enum id. This implicitly allocates the keyed + * scalar object in the storage if it wasn't previously allocated. + * + * @param aId The scalar id. + * @param aRes The output variable that stores scalar object. + * @return + * NS_ERROR_INVALID_ARG if the scalar id is unknown or a this is a keyed string + * scalar. + * NS_ERROR_NOT_AVAILABLE if the scalar is expired. + * NS_OK if the scalar was found. If that's the case, aResult contains a + * valid pointer to a scalar type. + */ +nsresult +internal_GetKeyedScalarByEnum(mozilla::Telemetry::ScalarID aId, KeyedScalar** aRet) +{ + if (!IsValidEnumId(aId)) { + MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id."); + return NS_ERROR_INVALID_ARG; + } + + const uint32_t id = static_cast(aId); + + KeyedScalar* scalar = nullptr; + if (gKeyedScalarStorageMap.Get(id, &scalar)) { + *aRet = scalar; + return NS_OK; + } + + const ScalarInfo &info = gScalars[id]; + + if (IsExpiredVersion(info.expiration())) { + return NS_ERROR_NOT_AVAILABLE; + } + + // We don't currently support keyed string scalars. Disable them. + if (info.kind == nsITelemetry::SCALAR_STRING) { + MOZ_ASSERT(false, "Keyed string scalars are not currently supported."); + return NS_ERROR_INVALID_ARG; + } + + scalar = new KeyedScalar(info.kind); + if (!scalar) { + return NS_ERROR_INVALID_ARG; + } + + gKeyedScalarStorageMap.Put(id, scalar); + + *aRet = scalar; + return NS_OK; +} + +/** + * Get a keyed scalar object by its enum id, if we're allowed to record it. + * + * @param aId The scalar id. + * @return The KeyedScalar instance or nullptr if we're not allowed to record + * the scalar. + */ +KeyedScalar* +internal_GetRecordableKeyedScalar(mozilla::Telemetry::ScalarID aId) +{ + // Get the scalar by the enum (it also internally checks for aId validity). + KeyedScalar* scalar = nullptr; + nsresult rv = internal_GetKeyedScalarByEnum(aId, &scalar); + if (NS_FAILED(rv)) { + return nullptr; + } + + if (!internal_IsKeyedScalar(aId)) { + return nullptr; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(aId)) { + return nullptr; + } + + return scalar; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryScalars:: + +// This is a StaticMutex rather than a plain Mutex (1) so that +// it gets initialised in a thread-safe manner the first time +// it is used, and (2) because it is never de-initialised, and +// a normal Mutex would show up as a leak in BloatView. StaticMutex +// also has the "OffTheBooks" property, so it won't show as a leak +// in BloatView. +// Another reason to use a StaticMutex instead of a plain Mutex is +// that, due to the nature of Telemetry, we cannot rely on having a +// mutex initialized in InitializeGlobalState. Unfortunately, we +// cannot make sure that no other function is called before this point. +static StaticMutex gTelemetryScalarsMutex; + +void +TelemetryScalar::InitializeGlobalState(bool aCanRecordBase, bool aCanRecordExtended) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + MOZ_ASSERT(!gInitDone, "TelemetryScalar::InitializeGlobalState " + "may only be called once"); + + gCanRecordBase = aCanRecordBase; + gCanRecordExtended = aCanRecordExtended; + + // Populate the static scalar name->id cache. Note that the scalar names are + // statically allocated and come from the automatically generated TelemetryScalarData.h. + uint32_t scalarCount = static_cast(mozilla::Telemetry::ScalarID::ScalarCount); + for (uint32_t i = 0; i < scalarCount; i++) { + CharPtrEntryType *entry = gScalarNameIDMap.PutEntry(gScalars[i].name()); + entry->mData = static_cast(i); + } + +#ifdef DEBUG + gScalarNameIDMap.MarkImmutable(); +#endif + gInitDone = true; +} + +void +TelemetryScalar::DeInitializeGlobalState() +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + gCanRecordBase = false; + gCanRecordExtended = false; + gScalarNameIDMap.Clear(); + gScalarStorageMap.Clear(); + gKeyedScalarStorageMap.Clear(); + gInitDone = false; +} + +void +TelemetryScalar::SetCanRecordBase(bool b) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + gCanRecordBase = b; +} + +void +TelemetryScalar::SetCanRecordExtended(bool b) { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + gCanRecordExtended = b; +} + +/** + * Adds the value to the given scalar. + * + * @param aName The scalar name. + * @param aVal The numeric value to add to the scalar. + * @param aCx The JS context. + * @return NS_OK if the value was added or if we're not allowed to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Add(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // We're trying to set a plain scalar, so make sure this is one. + if (internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + ScalarBase* scalar = nullptr; + rv = internal_GetScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->AddValue(unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * Adds the value to the given scalar. + * + * @param aName The scalar name. + * @param aKey The key name. + * @param aVal The numeric value to add to the scalar. + * @param aCx The JS context. + * @return NS_OK if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // Make sure this is a keyed scalar. + if (!internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + KeyedScalar* scalar = nullptr; + rv = internal_GetKeyedScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->AddValue(aKey, unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * Adds the value to the given scalar. + * + * @param aId The scalar enum id. + * @param aVal The numeric value to add to the scalar. + */ +void +TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + return; + } + + scalar->AddValue(aValue); +} + +/** + * Adds the value to the given keyed scalar. + * + * @param aId The scalar enum id. + * @param aKey The key name. + * @param aVal The numeric value to add to the scalar. + */ +void +TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, + uint32_t aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->AddValue(aKey, aValue); +} + +/** + * Sets the scalar to the given value. + * + * @param aName The scalar name. + * @param aVal The value to set the scalar to. + * @param aCx The JS context. + * @return NS_OK if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Set(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // We're trying to set a plain scalar, so make sure this is one. + if (internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + ScalarBase* scalar = nullptr; + rv = internal_GetScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetValue(unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * Sets the keyed scalar to the given value. + * + * @param aName The scalar name. + * @param aKey The key name. + * @param aVal The value to set the scalar to. + * @param aCx The JS context. + * @return NS_OK if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // We're trying to set a keyed scalar. Report an error if this isn't one. + if (!internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + KeyedScalar* scalar = nullptr; + rv = internal_GetKeyedScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetValue(aKey, unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * Sets the scalar to the given numeric value. + * + * @param aId The scalar enum id. + * @param aValue The numeric, unsigned value to set the scalar to. + */ +void +TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + return; + } + + scalar->SetValue(aValue); +} + +/** + * Sets the scalar to the given string value. + * + * @param aId The scalar enum id. + * @param aValue The string value to set the scalar to. + */ +void +TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + return; + } + + scalar->SetValue(aValue); +} + +/** + * Sets the scalar to the given boolean value. + * + * @param aId The scalar enum id. + * @param aValue The boolean value to set the scalar to. + */ +void +TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, bool aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + return; + } + + scalar->SetValue(aValue); +} + +/** + * Sets the keyed scalar to the given numeric value. + * + * @param aId The scalar enum id. + * @param aKey The scalar key. + * @param aValue The numeric, unsigned value to set the scalar to. + */ +void +TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, + uint32_t aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->SetValue(aKey, aValue); +} + +/** + * Sets the scalar to the given boolean value. + * + * @param aId The scalar enum id. + * @param aKey The scalar key. + * @param aValue The boolean value to set the scalar to. + */ +void +TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, + bool aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->SetValue(aKey, aValue); +} + +/** + * Sets the scalar to the maximum of the current and the passed value. + * + * @param aName The scalar name. + * @param aVal The numeric value to set the scalar to. + * @param aCx The JS context. + * @return NS_OK if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // Make sure this is not a keyed scalar. + if (internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + ScalarBase* scalar = nullptr; + rv = internal_GetScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetMaximum(unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * Sets the scalar to the maximum of the current and the passed value. + * + * @param aName The scalar name. + * @param aKey The key name. + * @param aVal The numeric value to set the scalar to. + * @param aCx The JS context. + * @return NS_OK if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // Make sure this is a keyed scalar. + if (!internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + KeyedScalar* scalar = nullptr; + rv = internal_GetKeyedScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetMaximum(aKey, unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * Sets the scalar to the maximum of the current and the passed value. + * + * @param aId The scalar enum id. + * @param aValue The numeric value to set the scalar to. + */ +void +TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + return; + } + + scalar->SetMaximum(aValue); +} + +/** + * Sets the keyed scalar to the maximum of the current and the passed value. + * + * @param aId The scalar enum id. + * @param aKey The key name. + * @param aValue The numeric value to set the scalar to. + */ +void +TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, + uint32_t aValue) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->SetMaximum(aKey, aValue); +} + +/** + * Serializes the scalars from the given dataset to a json-style object and resets them. + * The returned structure looks like {"group1.probe":1,"group1.other_probe":false,...}. + * + * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN. + * @param aClear Whether to clear out the scalars after snapshotting. + */ +nsresult +TelemetryScalar::CreateSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandle aResult) +{ + // If no arguments were passed in, apply the default value. + if (!optional_argc) { + aClearScalars = false; + } + + JS::Rooted root_obj(aCx, JS_NewPlainObject(aCx)); + if (!root_obj) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root_obj); + + // Only lock the mutex while accessing our data, without locking any JS related code. + typedef mozilla::Pair> DataPair; + nsTArray scalarsToReflect; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + // Iterate the scalars in gScalarStorageMap. The storage may contain empty or yet to be + // initialized scalars. + for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + ScalarBase* scalar = static_cast(iter.Data()); + + // Get the informations for this scalar. + const ScalarInfo& info = gScalars[iter.Key()]; + + // Serialize the scalar if it's in the desired dataset. + if (IsInDataset(info.dataset, aDataset)) { + // Get the scalar value. + nsCOMPtr scalarValue; + nsresult rv = scalar->GetValue(scalarValue); + if (NS_FAILED(rv)) { + return rv; + } + // Append it to our list. + scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarValue)); + } + } + + if (aClearScalars) { + // The map already takes care of freeing the allocated memory. + gScalarStorageMap.Clear(); + } + } + + // Reflect it to JS. + for (nsTArray::size_type i = 0; i < scalarsToReflect.Length(); i++) { + const DataPair& scalar = scalarsToReflect[i]; + + // Convert it to a JS Val. + JS::Rooted scalarJsValue(aCx); + nsresult rv = + nsContentUtils::XPConnect()->VariantToJS(aCx, root_obj, scalar.second(), &scalarJsValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Add it to the scalar object. + if (!JS_DefineProperty(aCx, root_obj, scalar.first(), scalarJsValue, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +/** + * Serializes the scalars from the given dataset to a json-style object and resets them. + * The returned structure looks like: + * { "group1.probe": { "key_1": 2, "key_2": 1, ... }, ... } + * + * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN. + * @param aClear Whether to clear out the keyed scalars after snapshotting. + */ +nsresult +TelemetryScalar::CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandle aResult) +{ + // If no arguments were passed in, apply the default value. + if (!optional_argc) { + aClearScalars = false; + } + + JS::Rooted root_obj(aCx, JS_NewPlainObject(aCx)); + if (!root_obj) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root_obj); + + // Only lock the mutex while accessing our data, without locking any JS related code. + typedef mozilla::Pair> DataPair; + nsTArray scalarsToReflect; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + // Iterate the scalars in gKeyedScalarStorageMap. The storage may contain empty or yet + // to be initialized scalars. + for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + KeyedScalar* scalar = static_cast(iter.Data()); + + // Get the informations for this scalar. + const ScalarInfo& info = gScalars[iter.Key()]; + + // Serialize the scalar if it's in the desired dataset. + if (IsInDataset(info.dataset, aDataset)) { + // Get the keys for this scalar. + nsTArray scalarKeyedData; + nsresult rv = scalar->GetValue(scalarKeyedData); + if (NS_FAILED(rv)) { + return rv; + } + // Append it to our list. + scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarKeyedData)); + } + } + + if (aClearScalars) { + // The map already takes care of freeing the allocated memory. + gKeyedScalarStorageMap.Clear(); + } + } + + // Reflect it to JS. + for (nsTArray::size_type i = 0; i < scalarsToReflect.Length(); i++) { + const DataPair& keyedScalarData = scalarsToReflect[i]; + + // Go through each keyed scalar and create a keyed scalar object. + // This object will hold the values for all the keyed scalar keys. + JS::RootedObject keyedScalarObj(aCx, JS_NewPlainObject(aCx)); + + // Define a property for each scalar key, then add it to the keyed scalar + // object. + const nsTArray& keyProps = keyedScalarData.second(); + for (uint32_t i = 0; i < keyProps.Length(); i++) { + const KeyedScalar::KeyValuePair& keyData = keyProps[i]; + + // Convert the value for the key to a JSValue. + JS::Rooted keyJsValue(aCx); + nsresult rv = + nsContentUtils::XPConnect()->VariantToJS(aCx, keyedScalarObj, keyData.second(), &keyJsValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Add the key to the scalar representation. + const NS_ConvertUTF8toUTF16 key(keyData.first()); + if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(), keyJsValue, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + // Add the scalar to the root object. + if (!JS_DefineProperty(aCx, root_obj, keyedScalarData.first(), keyedScalarObj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +/** + * Resets all the stored scalars. This is intended to be only used in tests. + */ +void +TelemetryScalar::ClearScalars() +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + gScalarStorageMap.Clear(); + gKeyedScalarStorageMap.Clear(); +} + +size_t +TelemetryScalar::GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + return gScalarNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf); +} + +size_t +TelemetryScalar::GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + size_t n = 0; + // For the plain scalars... + for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + ScalarBase* scalar = static_cast(iter.Data()); + n += scalar->SizeOfIncludingThis(aMallocSizeOf); + } + // ...and for the keyed scalars. + for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + KeyedScalar* scalar = static_cast(iter.Data()); + n += scalar->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} diff --git a/toolkit/components/telemetry/TelemetryScalar.h b/toolkit/components/telemetry/TelemetryScalar.h new file mode 100644 index 000000000..b20a8dace --- /dev/null +++ b/toolkit/components/telemetry/TelemetryScalar.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef TelemetryScalar_h__ +#define TelemetryScalar_h__ + +#include "mozilla/TelemetryScalarEnums.h" + +// This module is internal to Telemetry. It encapsulates Telemetry's +// scalar accumulation and storage logic. It should only be used by +// Telemetry.cpp. These functions should not be used anywhere else. +// For the public interface to Telemetry functionality, see Telemetry.h. + +namespace TelemetryScalar { + +void InitializeGlobalState(bool canRecordBase, bool canRecordExtended); +void DeInitializeGlobalState(); + +void SetCanRecordBase(bool b); +void SetCanRecordExtended(bool b); + +// JS API Endpoints. +nsresult Add(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx); +nsresult Set(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx); +nsresult SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx); +nsresult CreateSnapshots(unsigned int aDataset, bool aClearScalars, + JSContext* aCx, uint8_t optional_argc, + JS::MutableHandle aResult); + +// Keyed JS API Endpoints. +nsresult Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx); +nsresult Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx); +nsresult SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx); +nsresult CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars, + JSContext* aCx, uint8_t optional_argc, + JS::MutableHandle aResult); + +// C++ API Endpoints. +void Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue); +void Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue); +void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue); +void Set(mozilla::Telemetry::ScalarID aId, bool aValue); +void SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue); + +// Keyed C++ API Endpoints. +void Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); +void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); +void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue); +void SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); + +// Only to be used for testing. +void ClearScalars(); + +size_t GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf); +size_t GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + +} // namespace TelemetryScalar + +#endif // TelemetryScalar_h__ \ No newline at end of file diff --git a/toolkit/components/telemetry/TelemetrySend.jsm b/toolkit/components/telemetry/TelemetrySend.jsm new file mode 100644 index 000000000..4694ac6a9 --- /dev/null +++ b/toolkit/components/telemetry/TelemetrySend.jsm @@ -0,0 +1,1114 @@ +/* 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/. */ + +/* + * This module is responsible for uploading pings to the server and persisting + * pings that can't be send now. + * Those pending pings are persisted on disk and sent at the next opportunity, + * newest first. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "TelemetrySend", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/Log.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/PromiseUtils.jsm"); +Cu.import("resource://gre/modules/ServiceRequest.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); +Cu.import("resource://gre/modules/Timer.jsm", this); + +XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", + "resource://gre/modules/AsyncShutdown.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage", + "resource://gre/modules/TelemetryStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryReportingPolicy", + "resource://gre/modules/TelemetryReportingPolicy.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", + "@mozilla.org/base/telemetry;1", + "nsITelemetry"); + +const Utils = TelemetryUtils; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetrySend::"; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_SERVER = PREF_BRANCH + "server"; +const PREF_UNIFIED = PREF_BRANCH + "unified"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +const TOPIC_IDLE_DAILY = "idle-daily"; +const TOPIC_QUIT_APPLICATION = "quit-application"; + +// Whether the FHR/Telemetry unification features are enabled. +// Changing this pref requires a restart. +const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false); + +const PING_FORMAT_VERSION = 4; + +const MS_IN_A_MINUTE = 60 * 1000; + +const PING_TYPE_DELETION = "deletion"; + +// We try to spread "midnight" pings out over this interval. +const MIDNIGHT_FUZZING_INTERVAL_MS = 60 * MS_IN_A_MINUTE; +// We delay sending "midnight" pings on this client by this interval. +const MIDNIGHT_FUZZING_DELAY_MS = Math.random() * MIDNIGHT_FUZZING_INTERVAL_MS; + +// Timeout after which we consider a ping submission failed. +const PING_SUBMIT_TIMEOUT_MS = 1.5 * MS_IN_A_MINUTE; + +// To keep resource usage in check, we limit ping sending to a maximum number +// of pings per minute. +const MAX_PING_SENDS_PER_MINUTE = 10; + +// If we have more pending pings then we can send right now, we schedule the next +// send for after SEND_TICK_DELAY. +const SEND_TICK_DELAY = 1 * MS_IN_A_MINUTE; +// If we had any ping send failures since the last ping, we use a backoff timeout +// for the next ping sends. We increase the delay exponentially up to a limit of +// SEND_MAXIMUM_BACKOFF_DELAY_MS. +// This exponential backoff will be reset by external ping submissions & idle-daily. +const SEND_MAXIMUM_BACKOFF_DELAY_MS = 120 * MS_IN_A_MINUTE; + +// The age of a pending ping to be considered overdue (in milliseconds). +const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * MS_IN_A_MINUTE; // 1 week + +function monotonicNow() { + try { + return Telemetry.msSinceProcessStart(); + } catch (ex) { + // If this fails fall back to the (non-monotonic) Date value. + return Date.now(); + } +} + +/** + * This is a policy object used to override behavior within this module. + * Tests override properties on this object to allow for control of behavior + * that would otherwise be very hard to cover. + */ +var Policy = { + now: () => new Date(), + midnightPingFuzzingDelay: () => MIDNIGHT_FUZZING_DELAY_MS, + setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs), + clearSchedulerTickTimeout: (id) => clearTimeout(id), +}; + +/** + * Determine if the ping has the new v4 ping format or the legacy v2 one or earlier. + */ +function isV4PingFormat(aPing) { + return ("id" in aPing) && ("application" in aPing) && + ("version" in aPing) && (aPing.version >= 2); +} + +/** + * Check if the provided ping is a deletion ping. + * @param {Object} aPing The ping to check. + * @return {Boolean} True if the ping is a deletion ping, false otherwise. + */ +function isDeletionPing(aPing) { + return isV4PingFormat(aPing) && (aPing.type == PING_TYPE_DELETION); +} + +/** + * Save the provided ping as a pending ping. If it's a deletion ping, save it + * to a special location. + * @param {Object} aPing The ping to save. + * @return {Promise} A promise resolved when the ping is saved. + */ +function savePing(aPing) { + if (isDeletionPing(aPing)) { + return TelemetryStorage.saveDeletionPing(aPing); + } + return TelemetryStorage.savePendingPing(aPing); +} + +/** + * @return {String} This returns a string with the gzip compressed data. + */ +function gzipCompressString(string) { + let observer = { + buffer: "", + onStreamComplete: function(loader, context, status, length, result) { + this.buffer = String.fromCharCode.apply(this, result); + } + }; + + let scs = Cc["@mozilla.org/streamConverters;1"] + .getService(Ci.nsIStreamConverterService); + let listener = Cc["@mozilla.org/network/stream-loader;1"] + .createInstance(Ci.nsIStreamLoader); + listener.init(observer); + let converter = scs.asyncConvertData("uncompressed", "gzip", + listener, null); + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + stringStream.data = string; + converter.onStartRequest(null, null); + converter.onDataAvailable(null, null, stringStream, 0, string.length); + converter.onStopRequest(null, null, null); + return observer.buffer; +} + +this.TelemetrySend = { + + /** + * Age in ms of a pending ping to be considered overdue. + */ + get OVERDUE_PING_FILE_AGE() { + return OVERDUE_PING_FILE_AGE; + }, + + get pendingPingCount() { + return TelemetrySendImpl.pendingPingCount; + }, + + /** + * Initializes this module. + * + * @param {Boolean} testing Whether this is run in a test. This changes some behavior + * to enable proper testing. + * @return {Promise} Resolved when setup is finished. + */ + setup: function(testing = false) { + return TelemetrySendImpl.setup(testing); + }, + + /** + * Shutdown this module - this will cancel any pending ping tasks and wait for + * outstanding async activity like network and disk I/O. + * + * @return {Promise} Promise that is resolved when shutdown is finished. + */ + shutdown: function() { + return TelemetrySendImpl.shutdown(); + }, + + /** + * Submit a ping for sending. This will: + * - send the ping right away if possible or + * - save the ping to disk and send it at the next opportunity + * + * @param {Object} ping The ping data to send, must be serializable to JSON. + * @return {Promise} Test-only - a promise that is resolved when the ping is sent or saved. + */ + submitPing: function(ping) { + return TelemetrySendImpl.submitPing(ping); + }, + + /** + * Count of pending pings that were found to be overdue at startup. + */ + get overduePingsCount() { + return TelemetrySendImpl.overduePingsCount; + }, + + /** + * Notify that we can start submitting data to the servers. + */ + notifyCanUpload: function() { + return TelemetrySendImpl.notifyCanUpload(); + }, + + /** + * Only used in tests. Used to reset the module data to emulate a restart. + */ + reset: function() { + return TelemetrySendImpl.reset(); + }, + + /** + * Only used in tests. + */ + setServer: function(server) { + return TelemetrySendImpl.setServer(server); + }, + + /** + * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests. + */ + clearCurrentPings: function() { + return TelemetrySendImpl.clearCurrentPings(); + }, + + /** + * Only used in tests to wait on outgoing pending pings. + */ + testWaitOnOutgoingPings: function() { + return TelemetrySendImpl.promisePendingPingActivity(); + }, + + /** + * Test-only - this allows overriding behavior to enable ping sending in debug builds. + */ + setTestModeEnabled: function(testing) { + TelemetrySendImpl.setTestModeEnabled(testing); + }, + + /** + * This returns state info for this module for AsyncShutdown timeout diagnostics. + */ + getShutdownState: function() { + return TelemetrySendImpl.getShutdownState(); + }, +}; + +var CancellableTimeout = { + _deferred: null, + _timer: null, + + /** + * This waits until either the given timeout passed or the timeout was cancelled. + * + * @param {Number} timeoutMs The timeout in ms. + * @return {Promise} Promise that is resolved with false if the timeout was cancelled, + * false otherwise. + */ + promiseWaitOnTimeout: function(timeoutMs) { + if (!this._deferred) { + this._deferred = PromiseUtils.defer(); + this._timer = Policy.setSchedulerTickTimeout(() => this._onTimeout(), timeoutMs); + } + + return this._deferred.promise; + }, + + _onTimeout: function() { + if (this._deferred) { + this._deferred.resolve(false); + this._timer = null; + this._deferred = null; + } + }, + + cancelTimeout: function() { + if (this._deferred) { + Policy.clearSchedulerTickTimeout(this._timer); + this._deferred.resolve(true); + this._timer = null; + this._deferred = null; + } + }, +}; + +/** + * SendScheduler implements the timer & scheduling behavior for ping sends. + */ +var SendScheduler = { + // Whether any ping sends failed since the last tick. If yes, we start with our exponential + // backoff timeout. + _sendsFailed: false, + // The current retry delay after ping send failures. We use this for the exponential backoff, + // increasing this value everytime we had send failures since the last tick. + _backoffDelay: SEND_TICK_DELAY, + _shutdown: false, + _sendTask: null, + // A string that tracks the last seen send task state, null if it never ran. + _sendTaskState: null, + + _logger: null, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX + "Scheduler::"); + } + + return this._logger; + }, + + shutdown: function() { + this._log.trace("shutdown"); + this._shutdown = true; + CancellableTimeout.cancelTimeout(); + return Promise.resolve(this._sendTask); + }, + + start: function() { + this._log.trace("start"); + this._sendsFailed = false; + this._backoffDelay = SEND_TICK_DELAY; + this._shutdown = false; + }, + + /** + * Only used for testing, resets the state to emulate a restart. + */ + reset: function() { + this._log.trace("reset"); + return this.shutdown().then(() => this.start()); + }, + + /** + * Notify the scheduler of a failure in sending out pings that warrants retrying. + * This will trigger the exponential backoff timer behavior on the next tick. + */ + notifySendsFailed: function() { + this._log.trace("notifySendsFailed"); + if (this._sendsFailed) { + return; + } + + this._sendsFailed = true; + this._log.trace("notifySendsFailed - had send failures"); + }, + + /** + * Returns whether ping submissions are currently throttled. + */ + isThrottled: function() { + const now = Policy.now(); + const nextPingSendTime = this._getNextPingSendTime(now); + return (nextPingSendTime > now.getTime()); + }, + + waitOnSendTask: function() { + return Promise.resolve(this._sendTask); + }, + + triggerSendingPings: function(immediately) { + this._log.trace("triggerSendingPings - active send task: " + !!this._sendTask + ", immediately: " + immediately); + + if (!this._sendTask) { + this._sendTask = this._doSendTask(); + let clear = () => this._sendTask = null; + this._sendTask.then(clear, clear); + } else if (immediately) { + CancellableTimeout.cancelTimeout(); + } + + return this._sendTask; + }, + + _doSendTask: Task.async(function*() { + this._sendTaskState = "send task started"; + this._backoffDelay = SEND_TICK_DELAY; + this._sendsFailed = false; + + const resetBackoffTimer = () => { + this._backoffDelay = SEND_TICK_DELAY; + }; + + for (;;) { + this._log.trace("_doSendTask iteration"); + this._sendTaskState = "start iteration"; + + if (this._shutdown) { + this._log.trace("_doSendTask - shutting down, bailing out"); + this._sendTaskState = "bail out - shutdown check"; + return; + } + + // Get a list of pending pings, sorted by last modified, descending. + // Filter out all the pings we can't send now. This addresses scenarios like "deletion" pings + // which can be send even when upload is disabled. + let pending = TelemetryStorage.getPendingPingList(); + let current = TelemetrySendImpl.getUnpersistedPings(); + this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length); + // Note that the two lists contain different kind of data. |pending| only holds ping + // info, while |current| holds actual ping data. + if (!TelemetrySendImpl.sendingEnabled()) { + pending = pending.filter(pingInfo => TelemetryStorage.isDeletionPing(pingInfo.id)); + current = current.filter(p => isDeletionPing(p)); + } + this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length); + + // Bail out if there is nothing to send. + if ((pending.length == 0) && (current.length == 0)) { + this._log.trace("_doSendTask - no pending pings, bailing out"); + this._sendTaskState = "bail out - no pings to send"; + return; + } + + // If we are currently throttled (e.g. fuzzing to avoid midnight spikes), wait for the next send window. + const now = Policy.now(); + if (this.isThrottled()) { + const nextPingSendTime = this._getNextPingSendTime(now); + this._log.trace("_doSendTask - throttled, delaying ping send to " + new Date(nextPingSendTime)); + this._sendTaskState = "wait for throttling to pass"; + + const delay = nextPingSendTime - now.getTime(); + const cancelled = yield CancellableTimeout.promiseWaitOnTimeout(delay); + if (cancelled) { + this._log.trace("_doSendTask - throttling wait was cancelled, resetting backoff timer"); + resetBackoffTimer(); + } + + continue; + } + + let sending = pending.slice(0, MAX_PING_SENDS_PER_MINUTE); + pending = pending.slice(MAX_PING_SENDS_PER_MINUTE); + this._log.trace("_doSendTask - triggering sending of " + sending.length + " pings now" + + ", " + pending.length + " pings waiting"); + + this._sendsFailed = false; + const sendStartTime = Policy.now(); + this._sendTaskState = "wait on ping sends"; + yield TelemetrySendImpl.sendPings(current, sending.map(p => p.id)); + if (this._shutdown || (TelemetrySend.pendingPingCount == 0)) { + this._log.trace("_doSendTask - bailing out after sending, shutdown: " + this._shutdown + + ", pendingPingCount: " + TelemetrySend.pendingPingCount); + this._sendTaskState = "bail out - shutdown & pending check after send"; + return; + } + + // Calculate the delay before sending the next batch of pings. + // We start with a delay that makes us send max. 1 batch per minute. + // If we had send failures in the last batch, we will override this with + // a backoff delay. + const timeSinceLastSend = Policy.now() - sendStartTime; + let nextSendDelay = Math.max(0, SEND_TICK_DELAY - timeSinceLastSend); + + if (!this._sendsFailed) { + this._log.trace("_doSendTask - had no send failures, resetting backoff timer"); + resetBackoffTimer(); + } else { + const newDelay = Math.min(SEND_MAXIMUM_BACKOFF_DELAY_MS, + this._backoffDelay * 2); + this._log.trace("_doSendTask - had send failures, backing off -" + + " old timeout: " + this._backoffDelay + + ", new timeout: " + newDelay); + this._backoffDelay = newDelay; + nextSendDelay = this._backoffDelay; + } + + this._log.trace("_doSendTask - waiting for next send opportunity, timeout is " + nextSendDelay) + this._sendTaskState = "wait on next send opportunity"; + const cancelled = yield CancellableTimeout.promiseWaitOnTimeout(nextSendDelay); + if (cancelled) { + this._log.trace("_doSendTask - batch send wait was cancelled, resetting backoff timer"); + resetBackoffTimer(); + } + } + }), + + /** + * This helper calculates the next time that we can send pings at. + * Currently this mostly redistributes ping sends from midnight until one hour after + * to avoid submission spikes around local midnight for daily pings. + * + * @param now Date The current time. + * @return Number The next time (ms from UNIX epoch) when we can send pings. + */ + _getNextPingSendTime: function(now) { + // 1. First we check if the time is between 0am and 1am. If it's not, we send + // immediately. + // 2. If we confirmed the time is indeed between 0am and 1am in step 1, we disallow + // sending before (midnight + fuzzing delay), which is a random time between 0am-1am + // (decided at startup). + + const midnight = Utils.truncateToDays(now); + // Don't delay pings if we are not within the fuzzing interval. + if ((now.getTime() - midnight.getTime()) > MIDNIGHT_FUZZING_INTERVAL_MS) { + return now.getTime(); + } + + // Delay ping send if we are within the midnight fuzzing range. + // We spread those ping sends out between |midnight| and |midnight + midnightPingFuzzingDelay|. + return midnight.getTime() + Policy.midnightPingFuzzingDelay(); + }, + + getShutdownState: function() { + return { + shutdown: this._shutdown, + hasSendTask: !!this._sendTask, + sendsFailed: this._sendsFailed, + sendTaskState: this._sendTaskState, + backoffDelay: this._backoffDelay, + }; + }, + }; + +var TelemetrySendImpl = { + _sendingEnabled: false, + // Tracks the shutdown state. + _shutdown: false, + _logger: null, + // This tracks all pending ping requests to the server. + _pendingPingRequests: new Map(), + // This tracks all the pending async ping activity. + _pendingPingActivity: new Set(), + // This is true when running in the test infrastructure. + _testMode: false, + // This holds pings that we currently try and haven't persisted yet. + _currentPings: new Map(), + + // Count of pending pings that were overdue. + _overduePingCount: 0, + + OBSERVER_TOPICS: [ + TOPIC_IDLE_DAILY, + ], + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + + return this._logger; + }, + + get overduePingsCount() { + return this._overduePingCount; + }, + + get pendingPingRequests() { + return this._pendingPingRequests; + }, + + get pendingPingCount() { + return TelemetryStorage.getPendingPingList().length + this._currentPings.size; + }, + + setTestModeEnabled: function(testing) { + this._testMode = testing; + }, + + setup: Task.async(function*(testing) { + this._log.trace("setup"); + + this._testMode = testing; + this._sendingEnabled = true; + + Services.obs.addObserver(this, TOPIC_IDLE_DAILY, false); + + this._server = Preferences.get(PREF_SERVER, undefined); + + // Check the pending pings on disk now. + try { + yield this._checkPendingPings(); + } catch (ex) { + this._log.error("setup - _checkPendingPings rejected", ex); + } + + // Enforce the pending pings storage quota. It could take a while so don't + // block on it. + TelemetryStorage.runEnforcePendingPingsQuotaTask(); + + // Start sending pings, but don't block on this. + SendScheduler.triggerSendingPings(true); + }), + + /** + * Discard old pings from the pending pings and detect overdue ones. + * @return {Boolean} True if we have overdue pings, false otherwise. + */ + _checkPendingPings: Task.async(function*() { + // Scan the pending pings - that gives us a list sorted by last modified, descending. + let infos = yield TelemetryStorage.loadPendingPingList(); + this._log.info("_checkPendingPings - pending ping count: " + infos.length); + if (infos.length == 0) { + this._log.trace("_checkPendingPings - no pending pings"); + return; + } + + const now = Policy.now(); + + // Check for overdue pings. + const overduePings = infos.filter((info) => + (now.getTime() - info.lastModificationDate) > OVERDUE_PING_FILE_AGE); + this._overduePingCount = overduePings.length; + + // Submit the age of the pending pings. + for (let pingInfo of infos) { + const ageInDays = + Utils.millisecondsToDays(Math.abs(now.getTime() - pingInfo.lastModificationDate)); + Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_AGE").add(ageInDays); + } + }), + + shutdown: Task.async(function*() { + this._shutdown = true; + + for (let topic of this.OBSERVER_TOPICS) { + try { + Services.obs.removeObserver(this, topic); + } catch (ex) { + this._log.error("shutdown - failed to remove observer for " + topic, ex); + } + } + + // We can't send anymore now. + this._sendingEnabled = false; + + // Cancel any outgoing requests. + yield this._cancelOutgoingRequests(); + + // Stop any active send tasks. + yield SendScheduler.shutdown(); + + // Wait for any outstanding async ping activity. + yield this.promisePendingPingActivity(); + + // Save any outstanding pending pings to disk. + yield this._persistCurrentPings(); + }), + + reset: function() { + this._log.trace("reset"); + + this._shutdown = false; + this._currentPings = new Map(); + this._overduePingCount = 0; + + const histograms = [ + "TELEMETRY_SUCCESS", + "TELEMETRY_SEND_SUCCESS", + "TELEMETRY_SEND_FAILURE", + ]; + + histograms.forEach(h => Telemetry.getHistogramById(h).clear()); + + return SendScheduler.reset(); + }, + + /** + * Notify that we can start submitting data to the servers. + */ + notifyCanUpload: function() { + // Let the scheduler trigger sending pings if possible. + SendScheduler.triggerSendingPings(true); + return this.promisePendingPingActivity(); + }, + + observe: function(subject, topic, data) { + switch (topic) { + case TOPIC_IDLE_DAILY: + SendScheduler.triggerSendingPings(true); + break; + } + }, + + submitPing: function(ping) { + this._log.trace("submitPing - ping id: " + ping.id); + + if (!this.sendingEnabled(ping)) { + this._log.trace("submitPing - Telemetry is not allowed to send pings."); + return Promise.resolve(); + } + + if (!this.canSendNow) { + // Sending is disabled or throttled, add this to the persisted pending pings. + this._log.trace("submitPing - can't send ping now, persisting to disk - " + + "canSendNow: " + this.canSendNow); + return savePing(ping); + } + + // Let the scheduler trigger sending pings if possible. + // As a safety mechanism, this resets any currently active throttling. + this._log.trace("submitPing - can send pings, trying to send now"); + this._currentPings.set(ping.id, ping); + SendScheduler.triggerSendingPings(true); + return Promise.resolve(); + }, + + /** + * Only used in tests. + */ + setServer: function (server) { + this._log.trace("setServer", server); + this._server = server; + }, + + /** + * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests. + */ + clearCurrentPings: Task.async(function*() { + if (this._shutdown) { + this._log.trace("clearCurrentPings - in shutdown, bailing out"); + return; + } + + // Temporarily disable the scheduler. It must not try to reschedule ping sending + // while we're deleting them. + yield SendScheduler.shutdown(); + + // Now that the ping activity has settled, abort outstanding ping requests. + this._cancelOutgoingRequests(); + + // Also, purge current pings. + this._currentPings.clear(); + + // We might have been interrupted and shutdown could have been started. + // We need to bail out in that case to avoid triggering send activity etc. + // at unexpected times. + if (this._shutdown) { + this._log.trace("clearCurrentPings - in shutdown, not spinning SendScheduler up again"); + return; + } + + // Enable the scheduler again and spin the send task. + SendScheduler.start(); + SendScheduler.triggerSendingPings(true); + }), + + _cancelOutgoingRequests: function() { + // Abort any pending ping XHRs. + for (let [id, request] of this._pendingPingRequests) { + this._log.trace("_cancelOutgoingRequests - aborting ping request for id " + id); + try { + request.abort(); + } catch (e) { + this._log.error("_cancelOutgoingRequests - failed to abort request for id " + id, e); + } + } + this._pendingPingRequests.clear(); + }, + + sendPings: function(currentPings, persistedPingIds) { + let pingSends = []; + + for (let current of currentPings) { + let ping = current; + let p = Task.spawn(function*() { + try { + yield this._doPing(ping, ping.id, false); + } catch (ex) { + this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex); + // Deletion pings must be saved to a special location. + yield savePing(ping); + } finally { + this._currentPings.delete(ping.id); + } + }.bind(this)); + + this._trackPendingPingTask(p); + pingSends.push(p); + } + + if (persistedPingIds.length > 0) { + pingSends.push(this._sendPersistedPings(persistedPingIds).catch((ex) => { + this._log.info("sendPings - persisted pings not sent", ex); + })); + } + + return Promise.all(pingSends); + }, + + /** + * Send the persisted pings to the server. + * + * @param {Array} List of ping ids that should be sent. + * + * @return Promise A promise that is resolved when all pings finished sending or failed. + */ + _sendPersistedPings: Task.async(function*(pingIds) { + this._log.trace("sendPersistedPings"); + + if (TelemetryStorage.pendingPingCount < 1) { + this._log.trace("_sendPersistedPings - no pings to send"); + return; + } + + if (pingIds.length < 1) { + this._log.trace("sendPersistedPings - no pings to send"); + return; + } + + // We can send now. + // If there are any send failures, _doPing() sets up handlers that e.g. trigger backoff timer behavior. + this._log.trace("sendPersistedPings - sending " + pingIds.length + " pings"); + let pingSendPromises = []; + for (let pingId of pingIds) { + const id = pingId; + pingSendPromises.push( + TelemetryStorage.loadPendingPing(id) + .then((data) => this._doPing(data, id, true)) + .catch(e => this._log.error("sendPersistedPings - failed to send ping " + id, e))); + } + + let promise = Promise.all(pingSendPromises); + this._trackPendingPingTask(promise); + yield promise; + }), + + _onPingRequestFinished: function(success, startTime, id, isPersisted) { + this._log.trace("_onPingRequestFinished - success: " + success + ", persisted: " + isPersisted); + + let sendId = success ? "TELEMETRY_SEND_SUCCESS" : "TELEMETRY_SEND_FAILURE"; + let hsend = Telemetry.getHistogramById(sendId); + let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS"); + + hsend.add(monotonicNow() - startTime); + hsuccess.add(success); + + if (!success) { + // Let the scheduler know about send failures for triggering backoff timeouts. + SendScheduler.notifySendsFailed(); + } + + if (success && isPersisted) { + if (TelemetryStorage.isDeletionPing(id)) { + return TelemetryStorage.removeDeletionPing(); + } + return TelemetryStorage.removePendingPing(id); + } + return Promise.resolve(); + }, + + _getSubmissionPath: function(ping) { + // The new ping format contains an "application" section, the old one doesn't. + let pathComponents; + if (isV4PingFormat(ping)) { + // We insert the Ping id in the URL to simplify server handling of duplicated + // pings. + let app = ping.application; + pathComponents = [ + ping.id, ping.type, app.name, app.version, app.channel, app.buildId + ]; + } else { + // This is a ping in the old format. + if (!("slug" in ping)) { + // That's odd, we don't have a slug. Generate one so that TelemetryStorage.jsm works. + ping.slug = Utils.generateUUID(); + } + + // Do we have enough info to build a submission URL? + let payload = ("payload" in ping) ? ping.payload : null; + if (payload && ("info" in payload)) { + let info = ping.payload.info; + pathComponents = [ ping.slug, info.reason, info.appName, info.appVersion, + info.appUpdateChannel, info.appBuildID ]; + } else { + // Only use the UUID as the slug. + pathComponents = [ ping.slug ]; + } + } + + let slug = pathComponents.join("/"); + return "/submit/telemetry/" + slug; + }, + + _doPing: function(ping, id, isPersisted) { + if (!this.sendingEnabled(ping)) { + // We can't send the pings to the server, so don't try to. + this._log.trace("_doPing - Can't send ping " + ping.id); + return Promise.resolve(); + } + + this._log.trace("_doPing - server: " + this._server + ", persisted: " + isPersisted + + ", id: " + id); + + const isNewPing = isV4PingFormat(ping); + const version = isNewPing ? PING_FORMAT_VERSION : 1; + const url = this._server + this._getSubmissionPath(ping) + "?v=" + version; + + let request = new ServiceRequest(); + request.mozBackgroundRequest = true; + request.timeout = PING_SUBMIT_TIMEOUT_MS; + + request.open("POST", url, true); + request.overrideMimeType("text/plain"); + request.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); + request.setRequestHeader("Date", Policy.now().toUTCString()); + + this._pendingPingRequests.set(id, request); + + // Prevent the request channel from running though URLClassifier (bug 1296802) + request.channel.loadFlags &= ~Ci.nsIChannel.LOAD_CLASSIFY_URI; + + const monotonicStartTime = monotonicNow(); + let deferred = PromiseUtils.defer(); + + let onRequestFinished = (success, event) => { + let onCompletion = () => { + if (success) { + deferred.resolve(); + } else { + deferred.reject(event); + } + }; + + this._pendingPingRequests.delete(id); + this._onPingRequestFinished(success, monotonicStartTime, id, isPersisted) + .then(() => onCompletion(), + (error) => { + this._log.error("_doPing - request success: " + success + ", error: " + error); + onCompletion(); + }); + }; + + let errorhandler = (event) => { + this._log.error("_doPing - error making request to " + url + ": " + event.type); + onRequestFinished(false, event); + }; + request.onerror = errorhandler; + request.ontimeout = errorhandler; + request.onabort = errorhandler; + + request.onload = (event) => { + let status = request.status; + let statusClass = status - (status % 100); + let success = false; + + if (statusClass === 200) { + // We can treat all 2XX as success. + this._log.info("_doPing - successfully loaded, status: " + status); + success = true; + } else if (statusClass === 400) { + // 4XX means that something with the request was broken. + this._log.error("_doPing - error submitting to " + url + ", status: " + status + + " - ping request broken?"); + Telemetry.getHistogramById("TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS").add(); + // TODO: we should handle this better, but for now we should avoid resubmitting + // broken requests by pretending success. + success = true; + } else if (statusClass === 500) { + // 5XX means there was a server-side error and we should try again later. + this._log.error("_doPing - error submitting to " + url + ", status: " + status + + " - server error, should retry later"); + } else { + // We received an unexpected status code. + this._log.error("_doPing - error submitting to " + url + ", status: " + status + + ", type: " + event.type); + } + + onRequestFinished(success, event); + }; + + // If that's a legacy ping format, just send its payload. + let networkPayload = isNewPing ? ping : ping.payload; + request.setRequestHeader("Content-Encoding", "gzip"); + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let startTime = new Date(); + let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(networkPayload)); + utf8Payload += converter.Finish(); + Telemetry.getHistogramById("TELEMETRY_STRINGIFY").add(new Date() - startTime); + + // Check the size and drop pings which are too big. + const pingSizeBytes = utf8Payload.length; + if (pingSizeBytes > TelemetryStorage.MAXIMUM_PING_SIZE) { + this._log.error("_doPing - submitted ping exceeds the size limit, size: " + pingSizeBytes); + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND").add(); + Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB") + .add(Math.floor(pingSizeBytes / 1024 / 1024)); + // We don't need to call |request.abort()| as it was not sent yet. + this._pendingPingRequests.delete(id); + return TelemetryStorage.removePendingPing(id); + } + + let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + startTime = new Date(); + payloadStream.data = gzipCompressString(utf8Payload); + Telemetry.getHistogramById("TELEMETRY_COMPRESS").add(new Date() - startTime); + startTime = new Date(); + request.send(payloadStream); + + return deferred.promise; + }, + + /** + * Check if sending is temporarily disabled. + * @return {Boolean} True if we can send pings to the server right now, false if + * sending is temporarily disabled. + */ + get canSendNow() { + // If the reporting policy was not accepted yet, don't send pings. + if (!TelemetryReportingPolicy.canUpload()) { + return false; + } + + return this._sendingEnabled; + }, + + /** + * Check if sending is disabled. If FHR is not allowed to upload, + * pings are not sent to the server (Telemetry is a sub-feature of FHR). If trying + * to send a deletion ping, don't block it. + * If unified telemetry is off, don't send pings if Telemetry is disabled. + * + * @param {Object} [ping=null] A ping to be checked. + * @return {Boolean} True if pings can be send to the servers, false otherwise. + */ + sendingEnabled: function(ping = null) { + // We only send pings from official builds, but allow overriding this for tests. + if (!Telemetry.isOfficialTelemetry && !this._testMode) { + return false; + } + + // With unified Telemetry, the FHR upload setting controls whether we can send pings. + // The Telemetry pref enables sending extended data sets instead. + if (IS_UNIFIED_TELEMETRY) { + // Deletion pings are sent even if the upload is disabled. + if (ping && isDeletionPing(ping)) { + return true; + } + return Preferences.get(PREF_FHR_UPLOAD_ENABLED, false); + } + + // Without unified Telemetry, the Telemetry enabled pref controls ping sending. + return Utils.isTelemetryEnabled; + }, + + /** + * Track any pending ping send and save tasks through the promise passed here. + * This is needed to block shutdown on any outstanding ping activity. + */ + _trackPendingPingTask: function (promise) { + let clear = () => this._pendingPingActivity.delete(promise); + promise.then(clear, clear); + this._pendingPingActivity.add(promise); + }, + + /** + * Return a promise that allows to wait on pending pings. + * @return {Object} A promise resolved when all the pending pings promises + * are resolved. + */ + promisePendingPingActivity: function () { + this._log.trace("promisePendingPingActivity - Waiting for ping task"); + let p = Array.from(this._pendingPingActivity, p => p.catch(ex => { + this._log.error("promisePendingPingActivity - ping activity had an error", ex); + })); + p.push(SendScheduler.waitOnSendTask()); + return Promise.all(p); + }, + + _persistCurrentPings: Task.async(function*() { + for (let [id, ping] of this._currentPings) { + try { + yield savePing(ping); + this._log.trace("_persistCurrentPings - saved ping " + id); + } catch (ex) { + this._log.error("_persistCurrentPings - failed to save ping " + id, ex); + } finally { + this._currentPings.delete(id); + } + } + }), + + /** + * Returns the current pending, not yet persisted, pings, newest first. + */ + getUnpersistedPings: function() { + let current = [...this._currentPings.values()]; + current.reverse(); + return current; + }, + + getShutdownState: function() { + return { + sendingEnabled: this._sendingEnabled, + pendingPingRequestCount: this._pendingPingRequests.size, + pendingPingActivityCount: this._pendingPingActivity.size, + unpersistedPingCount: this._currentPings.size, + persistedPingCount: TelemetryStorage.getPendingPingList().length, + schedulerState: SendScheduler.getShutdownState(), + }; + }, +}; diff --git a/toolkit/components/telemetry/TelemetrySession.jsm b/toolkit/components/telemetry/TelemetrySession.jsm new file mode 100644 index 000000000..3d97dc155 --- /dev/null +++ b/toolkit/components/telemetry/TelemetrySession.jsm @@ -0,0 +1,2124 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/debug.js", this); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/DeferredTask.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const Utils = TelemetryUtils; + +const myScope = this; + +// When modifying the payload in incompatible ways, please bump this version number +const PAYLOAD_VERSION = 4; +const PING_TYPE_MAIN = "main"; +const PING_TYPE_SAVED_SESSION = "saved-session"; + +const REASON_ABORTED_SESSION = "aborted-session"; +const REASON_DAILY = "daily"; +const REASON_SAVED_SESSION = "saved-session"; +const REASON_GATHER_PAYLOAD = "gather-payload"; +const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload"; +const REASON_TEST_PING = "test-ping"; +const REASON_ENVIRONMENT_CHANGE = "environment-change"; +const REASON_SHUTDOWN = "shutdown"; + +const HISTOGRAM_SUFFIXES = { + PARENT: "", + CONTENT: "#content", + GPU: "#gpu", +} + +const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange"; + +const MS_IN_ONE_HOUR = 60 * 60 * 1000; +const MIN_SUBSESSION_LENGTH_MS = Preferences.get("toolkit.telemetry.minSubsessionLength", 10 * 60) * 1000; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetrySession" + (Utils.isContentProcess ? "#content::" : "::"); + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; +const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit.enabled"; +const PREF_UNIFIED = PREF_BRANCH + "unified"; + + +const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload"; +const MESSAGE_TELEMETRY_THREAD_HANGS = "Telemetry:ChildThreadHangs"; +const MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS = "Telemetry:GetChildThreadHangs"; +const MESSAGE_TELEMETRY_USS = "Telemetry:USS"; +const MESSAGE_TELEMETRY_GET_CHILD_USS = "Telemetry:GetChildUSS"; + +const DATAREPORTING_DIRECTORY = "datareporting"; +const ABORTED_SESSION_FILE_NAME = "aborted-session-ping"; + +// Whether the FHR/Telemetry unification features are enabled. +// Changing this pref requires a restart. +const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false); + +// Maximum number of content payloads that we are willing to store. +const MAX_NUM_CONTENT_PAYLOADS = 10; + +// Do not gather data more than once a minute (ms) +const TELEMETRY_INTERVAL = 60 * 1000; +// Delay before intializing telemetry (ms) +const TELEMETRY_DELAY = Preferences.get("toolkit.telemetry.initDelay", 60) * 1000; +// Delay before initializing telemetry if we're testing (ms) +const TELEMETRY_TEST_DELAY = 1; +// Execute a scheduler tick every 5 minutes. +const SCHEDULER_TICK_INTERVAL_MS = Preferences.get("toolkit.telemetry.scheduler.tickInterval", 5 * 60) * 1000; +// When user is idle, execute a scheduler tick every 60 minutes. +const SCHEDULER_TICK_IDLE_INTERVAL_MS = Preferences.get("toolkit.telemetry.scheduler.idleTickInterval", 60 * 60) * 1000; + +// The tolerance we have when checking if it's midnight (15 minutes). +const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000; + +// Seconds of idle time before pinging. +// On idle-daily a gather-telemetry notification is fired, during it probes can +// start asynchronous tasks to gather data. +const IDLE_TIMEOUT_SECONDS = Preferences.get("toolkit.telemetry.idleTimeout", 5 * 60); + +// To avoid generating too many main pings, we ignore environment changes that +// happen within this interval since the last main ping. +const CHANGE_THROTTLE_INTERVAL_MS = 5 * 60 * 1000; + +// The frequency at which we persist session data to the disk to prevent data loss +// in case of aborted sessions (currently 5 minutes). +const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000; + +const TOPIC_CYCLE_COLLECTOR_BEGIN = "cycle-collector-begin"; + +// How long to wait in millis for all the child memory reports to come in +const TOTAL_MEMORY_COLLECTOR_TIMEOUT = 200; + +var gLastMemoryPoll = null; + +var gWasDebuggerAttached = false; + +XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", + "@mozilla.org/base/telemetry;1", + "nsITelemetry"); +XPCOMUtils.defineLazyServiceGetter(this, "idleService", + "@mozilla.org/widget/idleservice;1", + "nsIIdleService"); +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); +XPCOMUtils.defineLazyServiceGetter(this, "cpml", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageListenerManager"); +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); +XPCOMUtils.defineLazyServiceGetter(this, "ppml", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageListenerManager"); + +XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", + "resource://gre/modules/AddonManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", + "resource://gre/modules/AsyncShutdown.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryController", + "resource://gre/modules/TelemetryController.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage", + "resource://gre/modules/TelemetryStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog", + "resource://gre/modules/TelemetryLog.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ThirdPartyCookieProbe", + "resource://gre/modules/ThirdPartyCookieProbe.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", + "resource://gre/modules/UITelemetry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "GCTelemetry", + "resource://gre/modules/GCTelemetry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment", + "resource://gre/modules/TelemetryEnvironment.jsm"); + +function generateUUID() { + let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + // strip {} + return str.substring(1, str.length - 1); +} + +function getMsSinceProcessStart() { + try { + return Telemetry.msSinceProcessStart(); + } catch (ex) { + // If this fails return a special value. + return -1; + } +} + +/** + * This is a policy object used to override behavior for testing. + */ +var Policy = { + now: () => new Date(), + monotonicNow: getMsSinceProcessStart, + generateSessionUUID: () => generateUUID(), + generateSubsessionUUID: () => generateUUID(), + setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs), + clearSchedulerTickTimeout: id => clearTimeout(id), +}; + +/** + * Get the ping type based on the payload. + * @param {Object} aPayload The ping payload. + * @return {String} A string representing the ping type. + */ +function getPingType(aPayload) { + // To remain consistent with server-side ping handling, set "saved-session" as the ping + // type for "saved-session" payload reasons. + if (aPayload.info.reason == REASON_SAVED_SESSION) { + return PING_TYPE_SAVED_SESSION; + } + + return PING_TYPE_MAIN; +} + +/** + * Annotate the current session ID with the crash reporter to map potential + * crash pings with the related main ping. + */ +function annotateCrashReport(sessionId) { + try { + const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"]; + if (cr) { + cr.getService(Ci.nsICrashReporter).setTelemetrySessionId(sessionId); + } + } catch (e) { + // Ignore errors when crash reporting is disabled + } +} + +/** + * Read current process I/O counters. + */ +var processInfo = { + _initialized: false, + _IO_COUNTERS: null, + _kernel32: null, + _GetProcessIoCounters: null, + _GetCurrentProcess: null, + getCounters: function() { + let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes); + if (isWindows) + return this.getCounters_Windows(); + return null; + }, + getCounters_Windows: function() { + if (!this._initialized) { + Cu.import("resource://gre/modules/ctypes.jsm"); + this._IO_COUNTERS = new ctypes.StructType("IO_COUNTERS", [ + {'readOps': ctypes.unsigned_long_long}, + {'writeOps': ctypes.unsigned_long_long}, + {'otherOps': ctypes.unsigned_long_long}, + {'readBytes': ctypes.unsigned_long_long}, + {'writeBytes': ctypes.unsigned_long_long}, + {'otherBytes': ctypes.unsigned_long_long} ]); + try { + this._kernel32 = ctypes.open("Kernel32.dll"); + this._GetProcessIoCounters = this._kernel32.declare("GetProcessIoCounters", + ctypes.winapi_abi, + ctypes.bool, // return + ctypes.voidptr_t, // hProcess + this._IO_COUNTERS.ptr); // lpIoCounters + this._GetCurrentProcess = this._kernel32.declare("GetCurrentProcess", + ctypes.winapi_abi, + ctypes.voidptr_t); // return + this._initialized = true; + } catch (err) { + return null; + } + } + let io = new this._IO_COUNTERS(); + if (!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address())) + return null; + return [parseInt(io.readBytes), parseInt(io.writeBytes)]; + } +}; + +/** + * TelemetryScheduler contains a single timer driving all regularly-scheduled + * Telemetry related jobs. Having a single place with this logic simplifies + * reasoning about scheduling actions in a single place, making it easier to + * coordinate jobs and coalesce them. + */ +var TelemetryScheduler = { + _lastDailyPingTime: 0, + _lastSessionCheckpointTime: 0, + + // For sanity checking. + _lastAdhocPingTime: 0, + _lastTickTime: 0, + + _log: null, + + // The timer which drives the scheduler. + _schedulerTimer: null, + // The interval used by the scheduler timer. + _schedulerInterval: 0, + _shuttingDown: true, + _isUserIdle: false, + + /** + * Initialises the scheduler and schedules the first daily/aborted session pings. + */ + init: function() { + this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::"); + this._log.trace("init"); + this._shuttingDown = false; + this._isUserIdle = false; + + // Initialize the last daily ping and aborted session last due times to the current time. + // Otherwise, we might end up sending daily pings even if the subsession is not long enough. + let now = Policy.now(); + this._lastDailyPingTime = now.getTime(); + this._lastSessionCheckpointTime = now.getTime(); + this._rescheduleTimeout(); + + idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); + Services.obs.addObserver(this, "wake_notification", false); + }, + + /** + * Stops the scheduler. + */ + shutdown: function() { + if (this._shuttingDown) { + if (this._log) { + this._log.error("shutdown - Already shut down"); + } else { + Cu.reportError("TelemetryScheduler.shutdown - Already shut down"); + } + return; + } + + this._log.trace("shutdown"); + if (this._schedulerTimer) { + Policy.clearSchedulerTickTimeout(this._schedulerTimer); + this._schedulerTimer = null; + } + + idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); + Services.obs.removeObserver(this, "wake_notification"); + + this._shuttingDown = true; + }, + + _clearTimeout: function() { + if (this._schedulerTimer) { + Policy.clearSchedulerTickTimeout(this._schedulerTimer); + } + }, + + /** + * Reschedules the tick timer. + */ + _rescheduleTimeout: function() { + this._log.trace("_rescheduleTimeout - isUserIdle: " + this._isUserIdle); + if (this._shuttingDown) { + this._log.warn("_rescheduleTimeout - already shutdown"); + return; + } + + this._clearTimeout(); + + const now = Policy.now(); + let timeout = SCHEDULER_TICK_INTERVAL_MS; + + // When the user is idle we want to fire the timer less often. + if (this._isUserIdle) { + timeout = SCHEDULER_TICK_IDLE_INTERVAL_MS; + // We need to make sure though that we don't miss sending pings around + // midnight when we use the longer idle intervals. + const nextMidnight = Utils.getNextMidnight(now); + timeout = Math.min(timeout, nextMidnight.getTime() - now.getTime()); + } + + this._log.trace("_rescheduleTimeout - scheduling next tick for " + new Date(now.getTime() + timeout)); + this._schedulerTimer = + Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), timeout); + }, + + _sentDailyPingToday: function(nowDate) { + // This is today's date and also the previous midnight (0:00). + const todayDate = Utils.truncateToDays(nowDate); + // We consider a ping sent for today if it occured after or at 00:00 today. + return (this._lastDailyPingTime >= todayDate.getTime()); + }, + + /** + * Checks if we can send a daily ping or not. + * @param {Object} nowDate A date object. + * @return {Boolean} True if we can send the daily ping, false otherwise. + */ + _isDailyPingDue: function(nowDate) { + // The daily ping is not due if we already sent one today. + if (this._sentDailyPingToday(nowDate)) { + this._log.trace("_isDailyPingDue - already sent one today"); + return false; + } + + // Avoid overly short sessions. + const timeSinceLastDaily = nowDate.getTime() - this._lastDailyPingTime; + if (timeSinceLastDaily < MIN_SUBSESSION_LENGTH_MS) { + this._log.trace("_isDailyPingDue - delaying daily to keep minimum session length"); + return false; + } + + this._log.trace("_isDailyPingDue - is due"); + return true; + }, + + /** + * An helper function to save an aborted-session ping. + * @param {Number} now The current time, in milliseconds. + * @param {Object} [competingPayload=null] If we are coalescing the daily and the + * aborted-session pings, this is the payload for the former. Note + * that the reason field of this payload will be changed. + * @return {Promise} A promise resolved when the ping is saved. + */ + _saveAbortedPing: function(now, competingPayload=null) { + this._lastSessionCheckpointTime = now; + return Impl._saveAbortedSessionPing(competingPayload) + .catch(e => this._log.error("_saveAbortedPing - Failed", e)); + }, + + /** + * The notifications handler. + */ + observe: function(aSubject, aTopic, aData) { + this._log.trace("observe - aTopic: " + aTopic); + switch (aTopic) { + case "idle": + // If the user is idle, increase the tick interval. + this._isUserIdle = true; + return this._onSchedulerTick(); + case "active": + // User is back to work, restore the original tick interval. + this._isUserIdle = false; + return this._onSchedulerTick(); + case "wake_notification": + // The machine woke up from sleep, trigger a tick to avoid sessions + // spanning more than a day. + // This is needed because sleep time does not count towards timeouts + // on Mac & Linux - see bug 1262386, bug 1204823 et al. + return this._onSchedulerTick(); + } + return undefined; + }, + + /** + * Performs a scheduler tick. This function manages Telemetry recurring operations. + * @return {Promise} A promise, only used when testing, resolved when the scheduled + * operation completes. + */ + _onSchedulerTick: function() { + // This call might not be triggered from a timeout. In that case we don't want to + // leave any previously scheduled timeouts pending. + this._clearTimeout(); + + if (this._shuttingDown) { + this._log.warn("_onSchedulerTick - already shutdown."); + return Promise.reject(new Error("Already shutdown.")); + } + + let promise = Promise.resolve(); + try { + promise = this._schedulerTickLogic(); + } catch (e) { + Telemetry.getHistogramById("TELEMETRY_SCHEDULER_TICK_EXCEPTION").add(1); + this._log.error("_onSchedulerTick - There was an exception", e); + } finally { + this._rescheduleTimeout(); + } + + // This promise is returned to make testing easier. + return promise; + }, + + /** + * Implements the scheduler logic. + * @return {Promise} Resolved when the scheduled task completes. Only used in tests. + */ + _schedulerTickLogic: function() { + this._log.trace("_schedulerTickLogic"); + + let nowDate = Policy.now(); + let now = nowDate.getTime(); + + if ((now - this._lastTickTime) > (1.1 * SCHEDULER_TICK_INTERVAL_MS) && + (this._lastTickTime != 0)) { + Telemetry.getHistogramById("TELEMETRY_SCHEDULER_WAKEUP").add(1); + this._log.trace("_schedulerTickLogic - First scheduler tick after sleep."); + } + this._lastTickTime = now; + + // Check if the daily ping is due. + const shouldSendDaily = this._isDailyPingDue(nowDate); + + if (shouldSendDaily) { + Telemetry.getHistogramById("TELEMETRY_SCHEDULER_SEND_DAILY").add(1); + this._log.trace("_schedulerTickLogic - Daily ping due."); + this._lastDailyPingTime = now; + return Impl._sendDailyPing(); + } + + // Check if the aborted-session ping is due. If a daily ping was saved above, it was + // already duplicated as an aborted-session ping. + const isAbortedPingDue = + (now - this._lastSessionCheckpointTime) >= ABORTED_SESSION_UPDATE_INTERVAL_MS; + if (isAbortedPingDue) { + this._log.trace("_schedulerTickLogic - Aborted session ping due."); + return this._saveAbortedPing(now); + } + + // No ping is due. + this._log.trace("_schedulerTickLogic - No ping due."); + return Promise.resolve(); + }, + + /** + * Update the scheduled pings if some other ping was sent. + * @param {String} reason The reason of the ping that was sent. + * @param {Object} [competingPayload=null] The payload of the ping that was sent. The + * reason of this payload will be changed. + */ + reschedulePings: function(reason, competingPayload = null) { + if (this._shuttingDown) { + this._log.error("reschedulePings - already shutdown"); + return; + } + + this._log.trace("reschedulePings - reason: " + reason); + let now = Policy.now(); + this._lastAdhocPingTime = now.getTime(); + if (reason == REASON_ENVIRONMENT_CHANGE) { + // We just generated an environment-changed ping, save it as an aborted session and + // update the schedules. + this._saveAbortedPing(now.getTime(), competingPayload); + // If we're close to midnight, skip today's daily ping and reschedule it for tomorrow. + let nearestMidnight = Utils.getNearestMidnight(now, SCHEDULER_MIDNIGHT_TOLERANCE_MS); + if (nearestMidnight) { + this._lastDailyPingTime = now.getTime(); + } + } + + this._rescheduleTimeout(); + }, +}; + +this.EXPORTED_SYMBOLS = ["TelemetrySession"]; + +this.TelemetrySession = Object.freeze({ + Constants: Object.freeze({ + PREF_PREVIOUS_BUILDID: PREF_PREVIOUS_BUILDID, + }), + /** + * Send a ping to a test server. Used only for testing. + */ + testPing: function() { + return Impl.testPing(); + }, + /** + * Returns the current telemetry payload. + * @param reason Optional, the reason to trigger the payload. + * @param clearSubsession Optional, whether to clear subsession specific data. + * @returns Object + */ + getPayload: function(reason, clearSubsession = false) { + return Impl.getPayload(reason, clearSubsession); + }, + /** + * Returns a promise that resolves to an array of thread hang stats from content processes, one entry per process. + * The structure of each entry is identical to that of "threadHangStats" in nsITelemetry. + * While thread hang stats are also part of the child payloads, this function is useful for cheaply getting this information, + * which is useful for realtime hang monitoring. + * Child processes that do not respond, or spawn/die during execution of this function are excluded from the result. + * @returns Promise + */ + getChildThreadHangs: function() { + return Impl.getChildThreadHangs(); + }, + /** + * Save the session state to a pending file. + * Used only for testing purposes. + */ + testSavePendingPing: function() { + return Impl.testSavePendingPing(); + }, + /** + * Collect and store information about startup. + */ + gatherStartup: function() { + return Impl.gatherStartup(); + }, + /** + * Inform the ping which AddOns are installed. + * + * @param aAddOns - The AddOns. + */ + setAddOns: function(aAddOns) { + return Impl.setAddOns(aAddOns); + }, + /** + * Descriptive metadata + * + * @param reason + * The reason for the telemetry ping, this will be included in the + * returned metadata, + * @return The metadata as a JS object + */ + getMetadata: function(reason) { + return Impl.getMetadata(reason); + }, + /** + * Used only for testing purposes. + */ + testReset: function() { + Impl._sessionId = null; + Impl._subsessionId = null; + Impl._previousSessionId = null; + Impl._previousSubsessionId = null; + Impl._subsessionCounter = 0; + Impl._profileSubsessionCounter = 0; + Impl._subsessionStartActiveTicks = 0; + Impl._subsessionStartTimeMonotonic = 0; + Impl._lastEnvironmentChangeDate = Policy.monotonicNow(); + this.testUninstall(); + }, + /** + * Triggers shutdown of the module. + */ + shutdown: function() { + return Impl.shutdownChromeProcess(); + }, + /** + * Sets up components used in the content process. + */ + setupContent: function(testing = false) { + return Impl.setupContentProcess(testing); + }, + /** + * Used only for testing purposes. + */ + testUninstall: function() { + try { + Impl.uninstall(); + } catch (ex) { + // Ignore errors + } + }, + /** + * Lightweight init function, called as soon as Firefox starts. + */ + earlyInit: function(aTesting = false) { + return Impl.earlyInit(aTesting); + }, + /** + * Does the "heavy" Telemetry initialization later on, so we + * don't impact startup performance. + * @return {Promise} Resolved when the initialization completes. + */ + delayedInit: function() { + return Impl.delayedInit(); + }, + /** + * Send a notification. + */ + observe: function (aSubject, aTopic, aData) { + return Impl.observe(aSubject, aTopic, aData); + }, +}); + +var Impl = { + _histograms: {}, + _initialized: false, + _logger: null, + _prevValues: {}, + _slowSQLStartup: {}, + _hasWindowRestoredObserver: false, + _hasXulWindowVisibleObserver: false, + _startupIO : {}, + // The previous build ID, if this is the first run with a new build. + // Null if this is the first run, or the previous build ID is unknown. + _previousBuildId: null, + // Telemetry payloads sent by child processes. + // Each element is in the format {source: , payload: }, + // where source is a weak reference to the child process, + // and payload is the telemetry payload from that child process. + _childTelemetry: [], + // Thread hangs from child processes. + // Used for TelemetrySession.getChildThreadHangs(); not sent with Telemetry pings. + // TelemetrySession.getChildThreadHangs() is used by extensions such as Statuser (https://github.com/chutten/statuser). + // Each element is in the format {source: , payload: }, + // where source is a weak reference to the child process, + // and payload contains the thread hang stats from that child process. + _childThreadHangs: [], + // Array of the resolve functions of all the promises that are waiting for the child thread hang stats to arrive, used to resolve all those promises at once. + _childThreadHangsResolveFunctions: [], + // Timeout function for child thread hang stats retrieval. + _childThreadHangsTimeout: null, + // Unique id that identifies this session so the server can cope with duplicate + // submissions, orphaning and other oddities. The id is shared across subsessions. + _sessionId: null, + // Random subsession id. + _subsessionId: null, + // Session id of the previous session, null on first run. + _previousSessionId: null, + // Subsession id of the previous subsession (even if it was in a different session), + // null on first run. + _previousSubsessionId: null, + // The running no. of subsessions since the start of the browser session + _subsessionCounter: 0, + // The running no. of all subsessions for the whole profile life time + _profileSubsessionCounter: 0, + // Date of the last session split + _subsessionStartDate: null, + // Start time of the current subsession using a monotonic clock for the subsession + // length measurements. + _subsessionStartTimeMonotonic: 0, + // The active ticks counted when the subsession starts + _subsessionStartActiveTicks: 0, + // A task performing delayed initialization of the chrome process + _delayedInitTask: null, + // Need a timeout in case children are tardy in giving back their memory reports. + _totalMemoryTimeout: undefined, + _testing: false, + // An accumulator of total memory across all processes. Only valid once the final child reports. + _totalMemory: null, + // A Set of outstanding USS report ids + _childrenToHearFrom: null, + // monotonically-increasing id for USS reports + _nextTotalMemoryId: 1, + _lastEnvironmentChangeDate: 0, + + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + return this._logger; + }, + + /** + * Gets a series of simple measurements (counters). At the moment, this + * only returns startup data from nsIAppStartup.getStartupInfo(). + * @param {Boolean} isSubsession True if this is a subsession, false otherwise. + * @param {Boolean} clearSubsession True if a new subsession is being started, false otherwise. + * + * @return simple measurements as a dictionary. + */ + getSimpleMeasurements: function getSimpleMeasurements(forSavedSession, isSubsession, clearSubsession) { + this._log.trace("getSimpleMeasurements"); + + let si = Services.startup.getStartupInfo(); + + // Measurements common to chrome and content processes. + let elapsedTime = Date.now() - si.process; + var ret = { + totalTime: Math.round(elapsedTime / 1000), // totalTime, in seconds + uptime: Math.round(elapsedTime / 60000) // uptime in minutes + } + + // Look for app-specific timestamps + var appTimestamps = {}; + try { + let o = {}; + Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", o); + appTimestamps = o.TelemetryTimestamps.get(); + } catch (ex) {} + + // Only submit this if the extended set is enabled. + if (!Utils.isContentProcess && Telemetry.canRecordExtended) { + try { + ret.addonManager = AddonManagerPrivate.getSimpleMeasures(); + ret.UITelemetry = UITelemetry.getSimpleMeasures(); + } catch (ex) {} + } + + if (si.process) { + for (let field of Object.keys(si)) { + if (field == "process") + continue; + ret[field] = si[field] - si.process + } + + for (let p in appTimestamps) { + if (!(p in ret) && appTimestamps[p]) + ret[p] = appTimestamps[p] - si.process; + } + } + + ret.startupInterrupted = Number(Services.startup.interrupted); + + ret.js = Cu.getJSEngineTelemetryValue(); + + let maximalNumberOfConcurrentThreads = Telemetry.maximalNumberOfConcurrentThreads; + if (maximalNumberOfConcurrentThreads) { + ret.maximalNumberOfConcurrentThreads = maximalNumberOfConcurrentThreads; + } + + if (Utils.isContentProcess) { + return ret; + } + + // Measurements specific to chrome process + + // Update debuggerAttached flag + let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + let isDebuggerAttached = debugService.isDebuggerAttached; + gWasDebuggerAttached = gWasDebuggerAttached || isDebuggerAttached; + ret.debuggerAttached = Number(gWasDebuggerAttached); + + let shutdownDuration = Telemetry.lastShutdownDuration; + if (shutdownDuration) + ret.shutdownDuration = shutdownDuration; + + let failedProfileLockCount = Telemetry.failedProfileLockCount; + if (failedProfileLockCount) + ret.failedProfileLockCount = failedProfileLockCount; + + for (let ioCounter in this._startupIO) + ret[ioCounter] = this._startupIO[ioCounter]; + + ret.savedPings = TelemetryStorage.pendingPingCount; + + ret.activeTicks = -1; + let sr = TelemetryController.getSessionRecorder(); + if (sr) { + let activeTicks = sr.activeTicks; + if (isSubsession) { + activeTicks = sr.activeTicks - this._subsessionStartActiveTicks; + } + + if (clearSubsession) { + this._subsessionStartActiveTicks = activeTicks; + } + + ret.activeTicks = activeTicks; + } + + ret.pingsOverdue = TelemetrySend.overduePingsCount; + + return ret; + }, + + /** + * When reflecting a histogram into JS, Telemetry hands us an object + * with the following properties: + * + * - min, max, histogram_type, sum, sum_squares_{lo,hi}: simple integers; + * - counts: array of counts for histogram buckets; + * - ranges: array of calculated bucket sizes. + * + * This format is not straightforward to read and potentially bulky + * with lots of zeros in the counts array. Packing histograms makes + * raw histograms easier to read and compresses the data a little bit. + * + * Returns an object: + * { range: [min, max], bucket_count: , + * histogram_type: , sum: , + * values: { bucket1: count1, bucket2: count2, ... } } + */ + packHistogram: function packHistogram(hgram) { + let r = hgram.ranges; + let c = hgram.counts; + let retgram = { + range: [r[1], r[r.length - 1]], + bucket_count: r.length, + histogram_type: hgram.histogram_type, + values: {}, + sum: hgram.sum + }; + + let first = true; + let last = 0; + + for (let i = 0; i < c.length; i++) { + let value = c[i]; + if (!value) + continue; + + // add a lower bound + if (i && first) { + retgram.values[r[i - 1]] = 0; + } + first = false; + last = i + 1; + retgram.values[r[i]] = value; + } + + // add an upper bound + if (last && last < c.length) + retgram.values[r[last]] = 0; + return retgram; + }, + + /** + * Get the type of the dataset that needs to be collected, based on the preferences. + * @return {Integer} A value from nsITelemetry.DATASET_*. + */ + getDatasetType: function() { + return Telemetry.canRecordExtended ? Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN + : Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT; + }, + + getHistograms: function getHistograms(subsession, clearSubsession) { + this._log.trace("getHistograms - subsession: " + subsession + + ", clearSubsession: " + clearSubsession); + + let registered = + Telemetry.registeredHistograms(this.getDatasetType(), []); + if (this._testing == false) { + // Omit telemetry test histograms outside of tests. + registered = registered.filter(n => !n.startsWith("TELEMETRY_TEST_")); + } + registered = registered.concat(registered.map(n => "STARTUP_" + n)); + + let hls = subsession ? Telemetry.snapshotSubsessionHistograms(clearSubsession) + : Telemetry.histogramSnapshots; + let ret = {}; + + for (let name of registered) { + for (let suffix of Object.values(HISTOGRAM_SUFFIXES)) { + if (name + suffix in hls) { + if (!(suffix in ret)) { + ret[suffix] = {}; + } + ret[suffix][name] = this.packHistogram(hls[name + suffix]); + } + } + } + + return ret; + }, + + getAddonHistograms: function getAddonHistograms() { + this._log.trace("getAddonHistograms"); + + let ahs = Telemetry.addonHistogramSnapshots; + let ret = {}; + + for (let addonName in ahs) { + let addonHistograms = ahs[addonName]; + let packedHistograms = {}; + for (let name in addonHistograms) { + packedHistograms[name] = this.packHistogram(addonHistograms[name]); + } + if (Object.keys(packedHistograms).length != 0) + ret[addonName] = packedHistograms; + } + + return ret; + }, + + getKeyedHistograms: function(subsession, clearSubsession) { + this._log.trace("getKeyedHistograms - subsession: " + subsession + + ", clearSubsession: " + clearSubsession); + + let registered = + Telemetry.registeredKeyedHistograms(this.getDatasetType(), []); + if (this._testing == false) { + // Omit telemetry test histograms outside of tests. + registered = registered.filter(id => !id.startsWith("TELEMETRY_TEST_")); + } + let ret = {}; + + for (let id of registered) { + for (let suffix of Object.values(HISTOGRAM_SUFFIXES)) { + let keyed = Telemetry.getKeyedHistogramById(id + suffix); + let snapshot = null; + if (subsession) { + snapshot = clearSubsession ? keyed.snapshotSubsessionAndClear() + : keyed.subsessionSnapshot(); + } else { + snapshot = keyed.snapshot(); + } + + let keys = Object.keys(snapshot); + if (keys.length == 0) { + // Skip empty keyed histogram. + continue; + } + + if (!(suffix in ret)) { + ret[suffix] = {}; + } + ret[suffix][id] = {}; + for (let key of keys) { + ret[suffix][id][key] = this.packHistogram(snapshot[key]); + } + } + } + + return ret; + }, + + /** + * Get a snapshot of the scalars and clear them. + * @param {subsession} If true, then we collect the data for a subsession. + * @param {clearSubsession} If true, we need to clear the subsession. + * @param {keyed} Take a snapshot of keyed or non keyed scalars. + * @return {Object} The scalar data as a Javascript object. + */ + getScalars: function (subsession, clearSubsession, keyed) { + this._log.trace("getScalars - subsession: " + subsession + ", clearSubsession: " + + clearSubsession + ", keyed: " + keyed); + + if (!subsession) { + // We only support scalars for subsessions. + this._log.trace("getScalars - We only support scalars in subsessions."); + return {}; + } + + let scalarsSnapshot = keyed ? + Telemetry.snapshotKeyedScalars(this.getDatasetType(), clearSubsession) : + Telemetry.snapshotScalars(this.getDatasetType(), clearSubsession); + + // Don't return the test scalars. + let ret = {}; + for (let name in scalarsSnapshot) { + if (name.startsWith('telemetry.test') && this._testing == false) { + this._log.trace("getScalars - Skipping test scalar: " + name); + } else { + ret[name] = scalarsSnapshot[name]; + } + } + + return ret; + }, + + getEvents: function(isSubsession, clearSubsession) { + if (!isSubsession) { + // We only support scalars for subsessions. + this._log.trace("getEvents - We only support events in subsessions."); + return []; + } + + let events = Telemetry.snapshotBuiltinEvents(this.getDatasetType(), + clearSubsession); + + // Don't return the test events outside of test environments. + if (!this._testing) { + events = events.filter(e => !e[1].startsWith("telemetry.test")); + } + + return events; + }, + + getThreadHangStats: function getThreadHangStats(stats) { + this._log.trace("getThreadHangStats"); + + stats.forEach((thread) => { + thread.activity = this.packHistogram(thread.activity); + thread.hangs.forEach((hang) => { + hang.histogram = this.packHistogram(hang.histogram); + }); + }); + return stats; + }, + + /** + * Descriptive metadata + * + * @param reason + * The reason for the telemetry ping, this will be included in the + * returned metadata, + * @return The metadata as a JS object + */ + getMetadata: function getMetadata(reason) { + this._log.trace("getMetadata - Reason " + reason); + + const sessionStartDate = Utils.toLocalTimeISOString(Utils.truncateToDays(this._sessionStartDate)); + const subsessionStartDate = Utils.toLocalTimeISOString(Utils.truncateToDays(this._subsessionStartDate)); + const monotonicNow = Policy.monotonicNow(); + + let ret = { + reason: reason, + revision: AppConstants.SOURCE_REVISION_URL, + asyncPluginInit: Preferences.get(PREF_ASYNC_PLUGIN_INIT, false), + + // Date.getTimezoneOffset() unintuitively returns negative values if we are ahead of + // UTC and vice versa (e.g. -60 for UTC+1). We invert the sign here. + timezoneOffset: -this._subsessionStartDate.getTimezoneOffset(), + previousBuildId: this._previousBuildId, + + sessionId: this._sessionId, + subsessionId: this._subsessionId, + previousSessionId: this._previousSessionId, + previousSubsessionId: this._previousSubsessionId, + + subsessionCounter: this._subsessionCounter, + profileSubsessionCounter: this._profileSubsessionCounter, + + sessionStartDate: sessionStartDate, + subsessionStartDate: subsessionStartDate, + + // Compute the session and subsession length in seconds. + // We use monotonic clocks as Date() is affected by jumping clocks (leading + // to negative lengths and other issues). + sessionLength: Math.floor(monotonicNow / 1000), + subsessionLength: + Math.floor((monotonicNow - this._subsessionStartTimeMonotonic) / 1000), + }; + + // TODO: Remove this when bug 1201837 lands. + if (this._addons) + ret.addons = this._addons; + + // TODO: Remove this when bug 1201837 lands. + let flashVersion = this.getFlashVersion(); + if (flashVersion) + ret.flashVersion = flashVersion; + + return ret; + }, + + /** + * Pull values from about:memory into corresponding histograms + */ + gatherMemory: function gatherMemory() { + if (!Telemetry.canRecordExtended) { + this._log.trace("gatherMemory - Extended data recording disabled, skipping."); + return; + } + + this._log.trace("gatherMemory"); + + let mgr; + try { + mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. + getService(Ci.nsIMemoryReporterManager); + } catch (e) { + // OK to skip memory reporters in xpcshell + return; + } + + let histogram = Telemetry.getHistogramById("TELEMETRY_MEMORY_REPORTER_MS"); + let startTime = new Date(); + + // Get memory measurements from distinguished amount attributes. We used + // to measure "explicit" too, but it could cause hangs, and the data was + // always really noisy anyway. See bug 859657. + // + // test_TelemetryController.js relies on some of these histograms being + // here. If you remove any of the following histograms from here, you'll + // have to modify test_TelemetryController.js: + // + // * MEMORY_JS_GC_HEAP, and + // * MEMORY_JS_COMPARTMENTS_SYSTEM. + // + // The distinguished amount attribute names don't match the telemetry id + // names in some cases due to a combination of (a) historical reasons, and + // (b) the fact that we can't change telemetry id names without breaking + // data continuity. + // + let boundHandleMemoryReport = this.handleMemoryReport.bind(this); + function h(id, units, amountName) { + try { + // If mgr[amountName] throws an exception, just move on -- some amounts + // aren't available on all platforms. But if the attribute simply + // isn't present, that indicates the distinguished amounts have changed + // and this file hasn't been updated appropriately. + let amount = mgr[amountName]; + NS_ASSERT(amount !== undefined, + "telemetry accessed an unknown distinguished amount"); + boundHandleMemoryReport(id, units, amount); + } catch (e) { + } + } + let b = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_BYTES, n); + let c = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT, n); + let cc= (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE, n); + let p = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_PERCENTAGE, n); + + b("MEMORY_VSIZE", "vsize"); + b("MEMORY_VSIZE_MAX_CONTIGUOUS", "vsizeMaxContiguous"); + b("MEMORY_RESIDENT_FAST", "residentFast"); + b("MEMORY_UNIQUE", "residentUnique"); + b("MEMORY_HEAP_ALLOCATED", "heapAllocated"); + p("MEMORY_HEAP_OVERHEAD_FRACTION", "heapOverheadFraction"); + b("MEMORY_JS_GC_HEAP", "JSMainRuntimeGCHeap"); + c("MEMORY_JS_COMPARTMENTS_SYSTEM", "JSMainRuntimeCompartmentsSystem"); + c("MEMORY_JS_COMPARTMENTS_USER", "JSMainRuntimeCompartmentsUser"); + b("MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED", "imagesContentUsedUncompressed"); + b("MEMORY_STORAGE_SQLITE", "storageSQLite"); + cc("LOW_MEMORY_EVENTS_VIRTUAL", "lowMemoryEventsVirtual"); + cc("LOW_MEMORY_EVENTS_PHYSICAL", "lowMemoryEventsPhysical"); + c("GHOST_WINDOWS", "ghostWindows"); + cc("PAGE_FAULTS_HARD", "pageFaultsHard"); + + if (!Utils.isContentProcess && !this._totalMemoryTimeout) { + // Only the chrome process should gather total memory + // total = parent RSS + sum(child USS) + this._totalMemory = mgr.residentFast; + if (ppmm.childCount > 1) { + // Do not report If we time out waiting for the children to call + this._totalMemoryTimeout = setTimeout( + () => { + this._totalMemoryTimeout = undefined; + this._childrenToHearFrom.clear(); + }, + TOTAL_MEMORY_COLLECTOR_TIMEOUT); + this._childrenToHearFrom = new Set(); + for (let i = 1; i < ppmm.childCount; i++) { + let child = ppmm.getChildAt(i); + try { + child.sendAsyncMessage(MESSAGE_TELEMETRY_GET_CHILD_USS, {id: this._nextTotalMemoryId}); + this._childrenToHearFrom.add(this._nextTotalMemoryId); + this._nextTotalMemoryId++; + } catch (ex) { + // If a content process has just crashed, then attempting to send it + // an async message will throw an exception. + Cu.reportError(ex); + } + } + } else { + boundHandleMemoryReport( + "MEMORY_TOTAL", + Ci.nsIMemoryReporter.UNITS_BYTES, + this._totalMemory); + } + } + + histogram.add(new Date() - startTime); + }, + + handleMemoryReport: function(id, units, amount) { + let val; + if (units == Ci.nsIMemoryReporter.UNITS_BYTES) { + val = Math.floor(amount / 1024); + } + else if (units == Ci.nsIMemoryReporter.UNITS_PERCENTAGE) { + // UNITS_PERCENTAGE amounts are 100x greater than their raw value. + val = Math.floor(amount / 100); + } + else if (units == Ci.nsIMemoryReporter.UNITS_COUNT) { + val = amount; + } + else if (units == Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE) { + // If the reporter gives us a cumulative count, we'll report the + // difference in its value between now and our previous ping. + + if (!(id in this._prevValues)) { + // If this is the first time we're reading this reporter, store its + // current value but don't report it in the telemetry ping, so we + // ignore the effect startup had on the reporter. + this._prevValues[id] = amount; + return; + } + + val = amount - this._prevValues[id]; + this._prevValues[id] = amount; + } + else { + NS_ASSERT(false, "Can't handle memory reporter with units " + units); + return; + } + + let h = this._histograms[id]; + if (!h) { + h = Telemetry.getHistogramById(id); + this._histograms[id] = h; + } + h.add(val); + }, + + getChildPayloads: function getChildPayloads() { + return this._childTelemetry.map(child => child.payload); + }, + + /** + * Get the current session's payload using the provided + * simpleMeasurements and info, which are typically obtained by a call + * to |this.getSimpleMeasurements| and |this.getMetadata|, + * respectively. + */ + assemblePayloadWithMeasurements: function(simpleMeasurements, info, reason, clearSubsession) { + const isSubsession = IS_UNIFIED_TELEMETRY && !this._isClassicReason(reason); + clearSubsession = IS_UNIFIED_TELEMETRY && clearSubsession; + this._log.trace("assemblePayloadWithMeasurements - reason: " + reason + + ", submitting subsession data: " + isSubsession); + + // This allows wrapping data retrieval calls in a try-catch block so that + // failures don't break the rest of the ping assembly. + const protect = (fn, defaultReturn = null) => { + try { + return fn(); + } catch (ex) { + this._log.error("assemblePayloadWithMeasurements - caught exception", ex); + return defaultReturn; + } + }; + + // Payload common to chrome and content processes. + let payloadObj = { + ver: PAYLOAD_VERSION, + simpleMeasurements: simpleMeasurements, + }; + + // Add extended set measurements common to chrome & content processes + if (Telemetry.canRecordExtended) { + payloadObj.chromeHangs = protect(() => Telemetry.chromeHangs); + payloadObj.threadHangStats = protect(() => this.getThreadHangStats(Telemetry.threadHangStats)); + payloadObj.log = protect(() => TelemetryLog.entries()); + payloadObj.webrtc = protect(() => Telemetry.webrtcStats); + } + + if (Utils.isContentProcess) { + return payloadObj; + } + + // Additional payload for chrome process. + let histograms = protect(() => this.getHistograms(isSubsession, clearSubsession), {}); + let keyedHistograms = protect(() => this.getKeyedHistograms(isSubsession, clearSubsession), {}); + payloadObj.histograms = histograms[HISTOGRAM_SUFFIXES.PARENT] || {}; + payloadObj.keyedHistograms = keyedHistograms[HISTOGRAM_SUFFIXES.PARENT] || {}; + payloadObj.processes = { + parent: { + scalars: protect(() => this.getScalars(isSubsession, clearSubsession)), + keyedScalars: protect(() => this.getScalars(isSubsession, clearSubsession, true)), + events: protect(() => this.getEvents(isSubsession, clearSubsession)), + }, + content: { + histograms: histograms[HISTOGRAM_SUFFIXES.CONTENT], + keyedHistograms: keyedHistograms[HISTOGRAM_SUFFIXES.CONTENT], + }, + }; + + // Only include the GPU process if we've accumulated data for it. + if (HISTOGRAM_SUFFIXES.GPU in histograms || + HISTOGRAM_SUFFIXES.GPU in keyedHistograms) + { + payloadObj.processes.gpu = { + histograms: histograms[HISTOGRAM_SUFFIXES.GPU], + keyedHistograms: keyedHistograms[HISTOGRAM_SUFFIXES.GPU], + }; + } + + payloadObj.info = info; + + // Add extended set measurements for chrome process. + if (Telemetry.canRecordExtended) { + payloadObj.slowSQL = protect(() => Telemetry.slowSQL); + payloadObj.fileIOReports = protect(() => Telemetry.fileIOReports); + payloadObj.lateWrites = protect(() => Telemetry.lateWrites); + + // Add the addon histograms if they are present + let addonHistograms = protect(() => this.getAddonHistograms()); + if (addonHistograms && Object.keys(addonHistograms).length > 0) { + payloadObj.addonHistograms = addonHistograms; + } + + payloadObj.addonDetails = protect(() => AddonManagerPrivate.getTelemetryDetails()); + + let clearUIsession = !(reason == REASON_GATHER_PAYLOAD || reason == REASON_GATHER_SUBSESSION_PAYLOAD); + payloadObj.UIMeasurements = protect(() => UITelemetry.getUIMeasurements(clearUIsession)); + + if (this._slowSQLStartup && + Object.keys(this._slowSQLStartup).length != 0 && + (Object.keys(this._slowSQLStartup.mainThread).length || + Object.keys(this._slowSQLStartup.otherThreads).length)) { + payloadObj.slowSQLStartup = this._slowSQLStartup; + } + + if (!this._isClassicReason(reason)) { + payloadObj.processes.parent.gc = protect(() => GCTelemetry.entries("main", clearSubsession)); + payloadObj.processes.content.gc = protect(() => GCTelemetry.entries("content", clearSubsession)); + } + } + + if (this._childTelemetry.length) { + payloadObj.childPayloads = protect(() => this.getChildPayloads()); + } + + return payloadObj; + }, + + /** + * Start a new subsession. + */ + startNewSubsession: function () { + this._subsessionStartDate = Policy.now(); + this._subsessionStartTimeMonotonic = Policy.monotonicNow(); + this._previousSubsessionId = this._subsessionId; + this._subsessionId = Policy.generateSubsessionUUID(); + this._subsessionCounter++; + this._profileSubsessionCounter++; + }, + + getSessionPayload: function getSessionPayload(reason, clearSubsession) { + this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession); + + let payload; + try { + const isMobile = ["gonk", "android"].includes(AppConstants.platform); + const isSubsession = isMobile ? false : !this._isClassicReason(reason); + + if (isMobile) { + clearSubsession = false; + } + + let measurements = + this.getSimpleMeasurements(reason == REASON_SAVED_SESSION, isSubsession, clearSubsession); + let info = !Utils.isContentProcess ? this.getMetadata(reason) : null; + payload = this.assemblePayloadWithMeasurements(measurements, info, reason, clearSubsession); + } catch (ex) { + Telemetry.getHistogramById("TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION").add(1); + throw ex; + } finally { + if (!Utils.isContentProcess && clearSubsession) { + this.startNewSubsession(); + // Persist session data to disk (don't wait until it completes). + let sessionData = this._getSessionDataObject(); + TelemetryStorage.saveSessionData(sessionData); + + // Notify that there was a subsession split in the parent process. This is an + // internal topic and is only meant for internal Telemetry usage. + Services.obs.notifyObservers(null, "internal-telemetry-after-subsession-split", null); + } + } + + return payload; + }, + + /** + * Send data to the server. Record success/send-time in histograms + */ + send: function send(reason) { + this._log.trace("send - Reason " + reason); + // populate histograms one last time + this.gatherMemory(); + + const isSubsession = !this._isClassicReason(reason); + let payload = this.getSessionPayload(reason, isSubsession); + let options = { + addClientId: true, + addEnvironment: true, + }; + return TelemetryController.submitExternalPing(getPingType(payload), payload, options); + }, + + attachObservers: function attachObservers() { + if (!this._initialized) + return; + Services.obs.addObserver(this, "idle-daily", false); + if (Telemetry.canRecordExtended) { + Services.obs.addObserver(this, TOPIC_CYCLE_COLLECTOR_BEGIN, false); + } + }, + + detachObservers: function detachObservers() { + if (!this._initialized) + return; + Services.obs.removeObserver(this, "idle-daily"); + try { + // Tests may flip Telemetry.canRecordExtended on and off. Just try to remove this + // observer and catch if it fails because the observer was not added. + Services.obs.removeObserver(this, TOPIC_CYCLE_COLLECTOR_BEGIN); + } catch (e) { + this._log.warn("detachObservers - Failed to remove " + TOPIC_CYCLE_COLLECTOR_BEGIN, e); + } + }, + + /** + * Lightweight init function, called as soon as Firefox starts. + */ + earlyInit: function(testing) { + this._log.trace("earlyInit"); + + this._initStarted = true; + this._testing = testing; + + if (this._initialized && !testing) { + this._log.error("earlyInit - already initialized"); + return; + } + + if (!Telemetry.canRecordBase && !testing) { + this._log.config("earlyInit - Telemetry recording is disabled, skipping Chrome process setup."); + return; + } + + // Generate a unique id once per session so the server can cope with duplicate + // submissions, orphaning and other oddities. The id is shared across subsessions. + this._sessionId = Policy.generateSessionUUID(); + this.startNewSubsession(); + // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use + // the very same value for |_sessionStartDate|. + this._sessionStartDate = this._subsessionStartDate; + + annotateCrashReport(this._sessionId); + + // Initialize some probes that are kept in their own modules + this._thirdPartyCookies = new ThirdPartyCookieProbe(); + this._thirdPartyCookies.init(); + + // Record old value and update build ID preference if this is the first + // run with a new build ID. + let previousBuildId = Preferences.get(PREF_PREVIOUS_BUILDID, null); + let thisBuildID = Services.appinfo.appBuildID; + // If there is no previousBuildId preference, we send null to the server. + if (previousBuildId != thisBuildID) { + this._previousBuildId = previousBuildId; + Preferences.set(PREF_PREVIOUS_BUILDID, thisBuildID); + } + + Services.obs.addObserver(this, "sessionstore-windows-restored", false); + if (AppConstants.platform === "android") { + Services.obs.addObserver(this, "application-background", false); + } + Services.obs.addObserver(this, "xul-window-visible", false); + this._hasWindowRestoredObserver = true; + this._hasXulWindowVisibleObserver = true; + + ppml.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this); + ppml.addMessageListener(MESSAGE_TELEMETRY_THREAD_HANGS, this); + ppml.addMessageListener(MESSAGE_TELEMETRY_USS, this); +}, + +/** + * Does the "heavy" Telemetry initialization later on, so we + * don't impact startup performance. + * @return {Promise} Resolved when the initialization completes. + */ + delayedInit:function() { + this._log.trace("delayedInit"); + + this._delayedInitTask = Task.spawn(function* () { + try { + this._initialized = true; + + yield this._loadSessionData(); + // Update the session data to keep track of new subsessions created before + // the initialization. + yield TelemetryStorage.saveSessionData(this._getSessionDataObject()); + + this.attachObservers(); + this.gatherMemory(); + + if (Telemetry.canRecordExtended) { + GCTelemetry.init(); + } + + Telemetry.asyncFetchTelemetryData(function () {}); + + if (IS_UNIFIED_TELEMETRY) { + // Check for a previously written aborted session ping. + yield TelemetryController.checkAbortedSessionPing(); + + // Write the first aborted-session ping as early as possible. Just do that + // if we are not testing, since calling Telemetry.reset() will make a previous + // aborted ping a pending ping. + if (!this._testing) { + yield this._saveAbortedSessionPing(); + } + + // The last change date for the environment, used to throttle environment changes. + this._lastEnvironmentChangeDate = Policy.monotonicNow(); + TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER, + (reason, data) => this._onEnvironmentChange(reason, data)); + + // Start the scheduler. + // We skip this if unified telemetry is off, so we don't + // trigger the new unified ping types. + TelemetryScheduler.init(); + } + + this._delayedInitTask = null; + } catch (e) { + this._delayedInitTask = null; + throw e; + } + }.bind(this)); + + return this._delayedInitTask; + }, + + /** + * Initializes telemetry for a content process. + */ + setupContentProcess: function setupContentProcess(testing) { + this._log.trace("setupContentProcess"); + this._testing = testing; + + if (!Telemetry.canRecordBase) { + this._log.trace("setupContentProcess - base recording is disabled, not initializing"); + return; + } + + Services.obs.addObserver(this, "content-child-shutdown", false); + cpml.addMessageListener(MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS, this); + cpml.addMessageListener(MESSAGE_TELEMETRY_GET_CHILD_USS, this); + + let delayedTask = new DeferredTask(function* () { + this._initialized = true; + + this.attachObservers(); + this.gatherMemory(); + + if (Telemetry.canRecordExtended) { + GCTelemetry.init(); + } + }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY); + + delayedTask.arm(); + }, + + getFlashVersion: function getFlashVersion() { + this._log.trace("getFlashVersion"); + let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let tags = host.getPluginTags(); + + for (let i = 0; i < tags.length; i++) { + if (tags[i].name == "Shockwave Flash") + return tags[i].version; + } + + return null; + }, + + receiveMessage: function receiveMessage(message) { + this._log.trace("receiveMessage - Message name " + message.name); + switch (message.name) { + case MESSAGE_TELEMETRY_PAYLOAD: + { + // In parent process, receive Telemetry payload from child + let source = message.data.childUUID; + delete message.data.childUUID; + + this._childTelemetry.push({ + source: source, + payload: message.data, + }); + + if (this._childTelemetry.length == MAX_NUM_CONTENT_PAYLOADS + 1) { + this._childTelemetry.shift(); + Telemetry.getHistogramById("TELEMETRY_DISCARDED_CONTENT_PINGS_COUNT").add(); + } + + break; + } + case MESSAGE_TELEMETRY_THREAD_HANGS: + { + // Accumulate child thread hang stats from this child + this._childThreadHangs.push(message.data); + + // Check if we've got data from all the children, accounting for child processes dying + // if it happens before the last response is received and no new child processes are spawned at the exact same time + // If that happens, we can resolve the promise earlier rather than having to wait for the timeout to expire + // Basically, the number of replies is at most the number of messages sent out, this._childCount, + // and also at most the number of child processes that currently exist + if (this._childThreadHangs.length === Math.min(this._childCount, ppmm.childCount)) { + clearTimeout(this._childThreadHangsTimeout); + + // Resolve all the promises that are waiting on these thread hang stats + // We resolve here instead of rejecting because + for (let resolve of this._childThreadHangsResolveFunctions) { + resolve(this._childThreadHangs); + } + this._childThreadHangsResolveFunctions = []; + } + + break; + } + case MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS: + { + // In child process, send the requested child thread hangs + this.sendContentProcessThreadHangs(); + break; + } + case MESSAGE_TELEMETRY_USS: + { + // In parent process, receive the USS report from the child + if (this._totalMemoryTimeout && this._childrenToHearFrom.delete(message.data.id)) { + this._totalMemory += message.data.bytes; + if (this._childrenToHearFrom.size == 0) { + clearTimeout(this._totalMemoryTimeout); + this._totalMemoryTimeout = undefined; + this.handleMemoryReport( + "MEMORY_TOTAL", + Ci.nsIMemoryReporter.UNITS_BYTES, + this._totalMemory); + } + } else { + this._log.trace("Child USS report was missed"); + } + break; + } + case MESSAGE_TELEMETRY_GET_CHILD_USS: + { + // In child process, send the requested USS report + this.sendContentProcessUSS(message.data.id); + break + } + default: + throw new Error("Telemetry.receiveMessage: bad message name"); + } + }, + + _processUUID: generateUUID(), + + sendContentProcessUSS: function sendContentProcessUSS(aMessageId) { + this._log.trace("sendContentProcessUSS"); + + let mgr; + try { + mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. + getService(Ci.nsIMemoryReporterManager); + } catch (e) { + // OK to skip memory reporters in xpcshell + return; + } + + cpmm.sendAsyncMessage( + MESSAGE_TELEMETRY_USS, + {bytes: mgr.residentUnique, id: aMessageId} + ); + }, + + sendContentProcessPing: function sendContentProcessPing(reason) { + this._log.trace("sendContentProcessPing - Reason " + reason); + const isSubsession = !this._isClassicReason(reason); + let payload = this.getSessionPayload(reason, isSubsession); + payload.childUUID = this._processUUID; + cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_PAYLOAD, payload); + }, + + sendContentProcessThreadHangs: function sendContentProcessThreadHangs() { + this._log.trace("sendContentProcessThreadHangs"); + let payload = { + childUUID: this._processUUID, + hangs: Telemetry.threadHangStats, + }; + cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_THREAD_HANGS, payload); + }, + + /** + * Save both the "saved-session" and the "shutdown" pings to disk. + * This needs to be called after TelemetrySend shuts down otherwise pings + * would be sent instead of getting persisted to disk. + */ + saveShutdownPings: function() { + this._log.trace("saveShutdownPings"); + + // We don't wait for "shutdown" pings to be written to disk before gathering the + // "saved-session" payload. Instead we append the promises to this list and wait + // on both to be saved after kicking off their collection. + let p = []; + + if (IS_UNIFIED_TELEMETRY) { + let shutdownPayload = this.getSessionPayload(REASON_SHUTDOWN, false); + + let options = { + addClientId: true, + addEnvironment: true, + }; + p.push(TelemetryController.submitExternalPing(getPingType(shutdownPayload), shutdownPayload, options) + .catch(e => this._log.error("saveShutdownPings - failed to submit shutdown ping", e))); + } + + // As a temporary measure, we want to submit saved-session too if extended Telemetry is enabled + // to keep existing performance analysis working. + if (Telemetry.canRecordExtended) { + let payload = this.getSessionPayload(REASON_SAVED_SESSION, false); + + let options = { + addClientId: true, + addEnvironment: true, + }; + p.push(TelemetryController.submitExternalPing(getPingType(payload), payload, options) + .catch (e => this._log.error("saveShutdownPings - failed to submit saved-session ping", e))); + } + + // Wait on pings to be saved. + return Promise.all(p); + }, + + + testSavePendingPing: function testSaveHistograms() { + this._log.trace("testSaveHistograms"); + let payload = this.getSessionPayload(REASON_SAVED_SESSION, false); + let options = { + addClientId: true, + addEnvironment: true, + overwrite: true, + }; + return TelemetryController.addPendingPing(getPingType(payload), payload, options); + }, + + /** + * Do some shutdown work that is common to all process types. + */ + uninstall: function uninstall() { + this.detachObservers(); + if (this._hasWindowRestoredObserver) { + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + this._hasWindowRestoredObserver = false; + } + if (this._hasXulWindowVisibleObserver) { + Services.obs.removeObserver(this, "xul-window-visible"); + this._hasXulWindowVisibleObserver = false; + } + if (AppConstants.platform === "android") { + Services.obs.removeObserver(this, "application-background", false); + } + GCTelemetry.shutdown(); + }, + + getPayload: function getPayload(reason, clearSubsession) { + this._log.trace("getPayload - clearSubsession: " + clearSubsession); + reason = reason || REASON_GATHER_PAYLOAD; + // This function returns the current Telemetry payload to the caller. + // We only gather startup info once. + if (Object.keys(this._slowSQLStartup).length == 0) { + this._slowSQLStartup = Telemetry.slowSQL; + } + this.gatherMemory(); + return this.getSessionPayload(reason, clearSubsession); + }, + + getChildThreadHangs: function getChildThreadHangs() { + return new Promise((resolve) => { + // Return immediately if there are no child processes to get stats from + if (ppmm.childCount === 0) { + resolve([]); + return; + } + + // Register our promise so it will be resolved when we receive the child thread hang stats on the parent process + // The resolve functions will all be called from "receiveMessage" when a MESSAGE_TELEMETRY_THREAD_HANGS message comes in + this._childThreadHangsResolveFunctions.push((threadHangStats) => { + let hangs = threadHangStats.map(child => child.hangs); + return resolve(hangs); + }); + + // If we (the parent) are not currently in the process of requesting child thread hangs, request them + // If we are, then the resolve function we registered above will receive the results without needing to request them again + if (this._childThreadHangsResolveFunctions.length === 1) { + // We have to cache the number of children we send messages to, in case the child count changes while waiting for messages to arrive + // This handles the case where the child count increases later on, in which case the new processes won't respond since we never sent messages to them + this._childCount = ppmm.childCount; + + this._childThreadHangs = []; // Clear the child hangs + for (let i = 0; i < this._childCount; i++) { + // If a child dies at exactly while we're running this loop, the message sending will fail but we won't get an exception + // In this case, since we won't know this has happened, we will simply rely on the timeout to handle it + ppmm.getChildAt(i).sendAsyncMessage(MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS); + } + + // Set up a timeout in case one or more of the content processes never responds + this._childThreadHangsTimeout = setTimeout(() => { + // Resolve all the promises that are waiting on these thread hang stats + // We resolve here instead of rejecting because the purpose of this function is + // to retrieve the BHR stats from all processes that will give us stats + // As a result, one process failing simply means it doesn't get included in the result. + for (let resolve of this._childThreadHangsResolveFunctions) { + resolve(this._childThreadHangs); + } + this._childThreadHangsResolveFunctions = []; + }, 200); + } + }); + }, + + gatherStartup: function gatherStartup() { + this._log.trace("gatherStartup"); + let counters = processInfo.getCounters(); + if (counters) { + [this._startupIO.startupSessionRestoreReadBytes, + this._startupIO.startupSessionRestoreWriteBytes] = counters; + } + this._slowSQLStartup = Telemetry.slowSQL; + }, + + setAddOns: function setAddOns(aAddOns) { + this._addons = aAddOns; + }, + + testPing: function testPing() { + return this.send(REASON_TEST_PING); + }, + + /** + * This observer drives telemetry. + */ + observe: function (aSubject, aTopic, aData) { + // Prevent the cycle collector begin topic from cluttering the log. + if (aTopic != TOPIC_CYCLE_COLLECTOR_BEGIN) { + this._log.trace("observe - " + aTopic + " notified."); + } + + switch (aTopic) { + case "content-child-shutdown": + // content-child-shutdown is only registered for content processes. + Services.obs.removeObserver(this, "content-child-shutdown"); + this.uninstall(); + Telemetry.flushBatchedChildTelemetry(); + this.sendContentProcessPing(REASON_SAVED_SESSION); + break; + case TOPIC_CYCLE_COLLECTOR_BEGIN: + let now = new Date(); + if (!gLastMemoryPoll + || (TELEMETRY_INTERVAL <= now - gLastMemoryPoll)) { + gLastMemoryPoll = now; + this.gatherMemory(); + } + break; + case "xul-window-visible": + Services.obs.removeObserver(this, "xul-window-visible"); + this._hasXulWindowVisibleObserver = false; + var counters = processInfo.getCounters(); + if (counters) { + [this._startupIO.startupWindowVisibleReadBytes, + this._startupIO.startupWindowVisibleWriteBytes] = counters; + } + break; + case "sessionstore-windows-restored": + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + this._hasWindowRestoredObserver = false; + // Check whether debugger was attached during startup + let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + gWasDebuggerAttached = debugService.isDebuggerAttached; + this.gatherStartup(); + break; + case "idle-daily": + // Enqueue to main-thread, otherwise components may be inited by the + // idle-daily category and miss the gather-telemetry notification. + Services.tm.mainThread.dispatch((function() { + // Notify that data should be gathered now. + // TODO: We are keeping this behaviour for now but it will be removed as soon as + // bug 1127907 lands. + Services.obs.notifyObservers(null, "gather-telemetry", null); + }).bind(this), Ci.nsIThread.DISPATCH_NORMAL); + break; + + case "application-background": + if (AppConstants.platform !== "android") { + break; + } + // On Android, we can get killed without warning once we are in the background, + // but we may also submit data and/or come back into the foreground without getting + // killed. To deal with this, we save the current session data to file when we are + // put into the background. This handles the following post-backgrounding scenarios: + // 1) We are killed immediately. In this case the current session data (which we + // save to a file) will be loaded and submitted on a future run. + // 2) We submit the data while in the background, and then are killed. In this case + // the file that we saved will be deleted by the usual process in + // finishPingRequest after it is submitted. + // 3) We submit the data, and then come back into the foreground. Same as case (2). + // 4) We do not submit the data, but come back into the foreground. In this case + // we have the option of either deleting the file that we saved (since we will either + // send the live data while in the foreground, or create the file again on the next + // backgrounding), or not (in which case we will delete it on submit, or overwrite + // it on the next backgrounding). Not deleting it is faster, so that's what we do. + let payload = this.getSessionPayload(REASON_SAVED_SESSION, false); + let options = { + addClientId: true, + addEnvironment: true, + overwrite: true, + }; + TelemetryController.addPendingPing(getPingType(payload), payload, options); + break; + } + return undefined; + }, + + /** + * This tells TelemetrySession to uninitialize and save any pending pings. + */ + shutdownChromeProcess: function() { + this._log.trace("shutdownChromeProcess"); + + let cleanup = () => { + if (IS_UNIFIED_TELEMETRY) { + TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER); + TelemetryScheduler.shutdown(); + } + this.uninstall(); + + let reset = () => { + this._initStarted = false; + this._initialized = false; + }; + + return Task.spawn(function*() { + yield this.saveShutdownPings(); + + if (IS_UNIFIED_TELEMETRY) { + yield TelemetryController.removeAbortedSessionPing(); + } + + reset(); + }.bind(this)); + }; + + // We can be in one the following states here: + // 1) delayedInit was never called + // or it was called and + // 2) _delayedInitTask is running now. + // 3) _delayedInitTask finished running already. + + // This handles 1). + if (!this._initStarted) { + return Promise.resolve(); + } + + // This handles 3). + if (!this._delayedInitTask) { + // We already ran the delayed initialization. + return cleanup(); + } + + // This handles 2). + return this._delayedInitTask.then(cleanup); + }, + + /** + * Gather and send a daily ping. + * @return {Promise} Resolved when the ping is sent. + */ + _sendDailyPing: function() { + this._log.trace("_sendDailyPing"); + let payload = this.getSessionPayload(REASON_DAILY, true); + + let options = { + addClientId: true, + addEnvironment: true, + }; + + let promise = TelemetryController.submitExternalPing(getPingType(payload), payload, options); + + // Also save the payload as an aborted session. If we delay this, aborted-session can + // lag behind for the profileSubsessionCounter and other state, complicating analysis. + if (IS_UNIFIED_TELEMETRY) { + this._saveAbortedSessionPing(payload) + .catch(e => this._log.error("_sendDailyPing - Failed to save the aborted session ping", e)); + } + + return promise; + }, + + /** Loads session data from the session data file. + * @return {Promise} A promise which is resolved with an object when + * loading has completed, with null otherwise. + */ + _loadSessionData: Task.async(function* () { + let data = yield TelemetryStorage.loadSessionData(); + + if (!data) { + return null; + } + + if (!("profileSubsessionCounter" in data) || + !(typeof(data.profileSubsessionCounter) == "number") || + !("subsessionId" in data) || !("sessionId" in data)) { + this._log.error("_loadSessionData - session data is invalid"); + Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").add(1); + return null; + } + + this._previousSessionId = data.sessionId; + this._previousSubsessionId = data.subsessionId; + // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for + // new subsession while loading still takes place. This will always be exactly + // 1 - the current subsessions. + this._profileSubsessionCounter = data.profileSubsessionCounter + + this._subsessionCounter; + return data; + }), + + /** + * Get the session data object to serialise to disk. + */ + _getSessionDataObject: function() { + return { + sessionId: this._sessionId, + subsessionId: this._subsessionId, + profileSubsessionCounter: this._profileSubsessionCounter, + }; + }, + + _onEnvironmentChange: function(reason, oldEnvironment) { + this._log.trace("_onEnvironmentChange", reason); + + let now = Policy.monotonicNow(); + let timeDelta = now - this._lastEnvironmentChangeDate; + if (timeDelta <= CHANGE_THROTTLE_INTERVAL_MS) { + this._log.trace(`_onEnvironmentChange - throttling; last change was ${Math.round(timeDelta / 1000)}s ago.`); + return; + } + + this._lastEnvironmentChangeDate = now; + let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true); + TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, payload); + + let options = { + addClientId: true, + addEnvironment: true, + overrideEnvironment: oldEnvironment, + }; + TelemetryController.submitExternalPing(getPingType(payload), payload, options); + }, + + _isClassicReason: function(reason) { + const classicReasons = [ + REASON_SAVED_SESSION, + REASON_GATHER_PAYLOAD, + REASON_TEST_PING, + ]; + return classicReasons.includes(reason); + }, + + /** + * Get an object describing the current state of this module for AsyncShutdown diagnostics. + */ + _getState: function() { + return { + initialized: this._initialized, + initStarted: this._initStarted, + haveDelayedInitTask: !!this._delayedInitTask, + }; + }, + + /** + * Saves the aborted session ping to disk. + * @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted + * session ping. The reason of this payload is changed to aborted-session. + * If not provided, a new payload is gathered. + */ + _saveAbortedSessionPing: function(aProvidedPayload = null) { + this._log.trace("_saveAbortedSessionPing"); + + let payload = null; + if (aProvidedPayload) { + payload = Cu.cloneInto(aProvidedPayload, myScope); + // Overwrite the original reason. + payload.info.reason = REASON_ABORTED_SESSION; + } else { + payload = this.getSessionPayload(REASON_ABORTED_SESSION, false); + } + + return TelemetryController.saveAbortedSessionPing(payload); + }, +}; diff --git a/toolkit/components/telemetry/TelemetryStartup.js b/toolkit/components/telemetry/TelemetryStartup.js new file mode 100644 index 000000000..28041b36b --- /dev/null +++ b/toolkit/components/telemetry/TelemetryStartup.js @@ -0,0 +1,49 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); + +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryController", + "resource://gre/modules/TelemetryController.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment", + "resource://gre/modules/TelemetryEnvironment.jsm"); + +/** + * TelemetryStartup is needed to forward the "profile-after-change" notification + * to TelemetryController.jsm. + */ +function TelemetryStartup() { +} + +TelemetryStartup.prototype.classID = Components.ID("{117b219f-92fe-4bd2-a21b-95a342a9d474}"); +TelemetryStartup.prototype.QueryInterface = XPCOMUtils.generateQI([Components.interfaces.nsIObserver]); +TelemetryStartup.prototype.observe = function(aSubject, aTopic, aData) { + if (aTopic == "profile-after-change" || aTopic == "app-startup") { + TelemetryController.observe(null, aTopic, null); + } + if (aTopic == "profile-after-change") { + annotateEnvironment(); + TelemetryEnvironment.registerChangeListener("CrashAnnotator", annotateEnvironment); + TelemetryEnvironment.onInitialized().then(() => annotateEnvironment()); + } +} + +function annotateEnvironment() { + try { + let cr = Cc["@mozilla.org/toolkit/crash-reporter;1"]; + if (cr) { + let env = JSON.stringify(TelemetryEnvironment.currentEnvironment); + cr.getService(Ci.nsICrashReporter).annotateCrashReport("TelemetryEnvironment", env); + } + } catch (e) { + // crash reporting not built or disabled? Ignore errors + } +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TelemetryStartup]); diff --git a/toolkit/components/telemetry/TelemetryStartup.manifest b/toolkit/components/telemetry/TelemetryStartup.manifest new file mode 100644 index 000000000..f1638530b --- /dev/null +++ b/toolkit/components/telemetry/TelemetryStartup.manifest @@ -0,0 +1,4 @@ +component {117b219f-92fe-4bd2-a21b-95a342a9d474} TelemetryStartup.js +contract @mozilla.org/base/telemetry-startup;1 {117b219f-92fe-4bd2-a21b-95a342a9d474} +category profile-after-change TelemetryStartup @mozilla.org/base/telemetry-startup;1 process=main +category app-startup TelemetryStartup @mozilla.org/base/telemetry-startup;1 process=content diff --git a/toolkit/components/telemetry/TelemetryStopwatch.jsm b/toolkit/components/telemetry/TelemetryStopwatch.jsm new file mode 100644 index 000000000..ab6c6eafb --- /dev/null +++ b/toolkit/components/telemetry/TelemetryStopwatch.jsm @@ -0,0 +1,335 @@ +/* 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/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +this.EXPORTED_SYMBOLS = ["TelemetryStopwatch"]; + +Cu.import("resource://gre/modules/Log.jsm", this); +var Telemetry = Cc["@mozilla.org/base/telemetry;1"] + .getService(Ci.nsITelemetry); + +// Weak map does not allow using null objects as keys. These objects are used +// as 'null' placeholders. +const NULL_OBJECT = {}; +const NULL_KEY = {}; + +/** + * Timers is a variation of a Map used for storing information about running + * Stopwatches. Timers has the following data structure: + * + * { + * "HISTOGRAM_NAME": WeakMap { + * Object || NULL_OBJECT: Map { + * "KEY" || NULL_KEY: startTime + * ... + * } + * ... + * } + * ... + * } + * + * + * @example + * // Stores current time for a keyed histogram "PLAYING_WITH_CUTE_ANIMALS". + * Timers.put("PLAYING_WITH_CUTE_ANIMALS", null, "CATS", Date.now()); + * + * @example + * // Returns information about a simple Stopwatch. + * let startTime = Timers.get("PLAYING_WITH_CUTE_ANIMALS", null, "CATS"); + */ +let Timers = { + _timers: new Map(), + + _validTypes: function(histogram, obj, key) { + let nonEmptyString = value => { + return typeof value === "string" && value !== "" && value.length > 0; + }; + return nonEmptyString(histogram) && + typeof obj == "object" && + (key === NULL_KEY || nonEmptyString(key)); + }, + + get: function(histogram, obj, key) { + key = key === null ? NULL_KEY : key; + obj = obj || NULL_OBJECT; + + if (!this.has(histogram, obj, key)) { + return null; + } + + return this._timers.get(histogram).get(obj).get(key); + }, + + put: function(histogram, obj, key, startTime) { + key = key === null ? NULL_KEY : key; + obj = obj || NULL_OBJECT; + + if (!this._validTypes(histogram, obj, key)) { + return false; + } + + let objectMap = this._timers.get(histogram) || new WeakMap(); + let keyedInfo = objectMap.get(obj) || new Map(); + keyedInfo.set(key, startTime); + objectMap.set(obj, keyedInfo); + this._timers.set(histogram, objectMap); + return true; + }, + + has: function(histogram, obj, key) { + key = key === null ? NULL_KEY : key; + obj = obj || NULL_OBJECT; + + return this._timers.has(histogram) && + this._timers.get(histogram).has(obj) && + this._timers.get(histogram).get(obj).has(key); + }, + + delete: function(histogram, obj, key) { + key = key === null ? NULL_KEY : key; + obj = obj || NULL_OBJECT; + + if (!this.has(histogram, obj, key)) { + return false; + } + let objectMap = this._timers.get(histogram); + let keyedInfo = objectMap.get(obj); + if (keyedInfo.size > 1) { + keyedInfo.delete(key); + return true; + } + objectMap.delete(obj); + // NOTE: + // We never delete empty objecMaps from this._timers because there is no + // nice solution for tracking the number of objects in a WeakMap. + // WeakMap is not enumerable, so we can't deterministically say when it's + // empty. We accept that trade-off here, given that entries for short-lived + // objects will go away when they are no longer referenced + return true; + } +}; + +this.TelemetryStopwatch = { + /** + * Starts a timer associated with a telemetry histogram. The timer can be + * directly associated with a histogram, or with a pair of a histogram and + * an object. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * + * @param {Object} aObj - Optional parameter. If specified, the timer is + * associated with this object, meaning that multiple + * timers for the same histogram may be run + * concurrently, as long as they are associated with + * different objects. + * + * @returns {Boolean} True if the timer was successfully started, false + * otherwise. If a timer already exists, it can't be + * started again, and the existing one will be cleared in + * order to avoid measurements errors. + */ + start: function(aHistogram, aObj) { + return TelemetryStopwatchImpl.start(aHistogram, aObj, null); + }, + + /** + * Deletes the timer associated with a telemetry histogram. The timer can be + * directly associated with a histogram, or with a pair of a histogram and + * an object. Important: Only use this method when a legitimate cancellation + * should be done. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * + * @param {Object} aObj - Optional parameter. If specified, the timer is + * associated with this object, meaning that multiple + * timers or a same histogram may be run concurrently, + * as long as they are associated with different + * objects. + * + * @returns {Boolean} True if the timer exist and it was cleared, False + * otherwise. + */ + cancel: function(aHistogram, aObj) { + return TelemetryStopwatchImpl.cancel(aHistogram, aObj, null); + }, + + /** + * Returns the elapsed time for a particular stopwatch. Primarily for + * debugging purposes. Must be called prior to finish. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * If an invalid name is given, the function will + * throw. + * + * @param (Object) aObj - Optional parameter which associates the histogram + * timer with the given object. + * + * @returns {Integer} time in milliseconds or -1 if the stopwatch was not + * found. + */ + timeElapsed: function(aHistogram, aObj) { + return TelemetryStopwatchImpl.timeElapsed(aHistogram, aObj, null); + }, + + /** + * Stops the timer associated with the given histogram (and object), + * calculates the time delta between start and finish, and adds the value + * to the histogram. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * + * @param {Object} aObj - Optional parameter which associates the histogram + * timer with the given object. + * + * @returns {Boolean} True if the timer was succesfully stopped and the data + * was added to the histogram, False otherwise. + */ + finish: function(aHistogram, aObj) { + return TelemetryStopwatchImpl.finish(aHistogram, aObj, null); + }, + + /** + * Starts a timer associated with a keyed telemetry histogram. The timer can + * be directly associated with a histogram and its key. Similarly to + * @see{TelemetryStopwatch.stat} the histogram and its key can be associated + * with an object. Each key may have multiple associated objects and each + * object can be associated with multiple keys. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * + * @param {String} aKey - a string which must be a valid histgram key. + * + * @param {Object} aObj - Optional parameter. If specified, the timer is + * associated with this object, meaning that multiple + * timers for the same histogram may be run + * concurrently,as long as they are associated with + * different objects. + * + * @returns {Boolean} True if the timer was successfully started, false + * otherwise. If a timer already exists, it can't be + * started again, and the existing one will be cleared in + * order to avoid measurements errors. + */ + startKeyed: function(aHistogram, aKey, aObj) { + return TelemetryStopwatchImpl.start(aHistogram, aObj, aKey); + }, + + /** + * Deletes the timer associated with a keyed histogram. Important: Only use + * this method when a legitimate cancellation should be done. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * + * @param {String} aKey - a string which must be a valid histgram key. + * + * @param {Object} aObj - Optional parameter. If specified, the timer + * associated with this object is deleted. + * + * @return {Boolean} True if the timer exist and it was cleared, False + * otherwise. + */ + cancelKeyed: function(aHistogram, aKey, aObj) { + return TelemetryStopwatchImpl.cancel(aHistogram, aObj, aKey); + }, + + /** + * Returns the elapsed time for a particular stopwatch. Primarily for + * debugging purposes. Must be called prior to finish. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * + * @param {String} aKey - a string which must be a valid histgram key. + * + * @param {Object} aObj - Optional parameter. If specified, the timer + * associated with this object is used to calculate + * the elapsed time. + * + * @return {Integer} time in milliseconds or -1 if the stopwatch was not + * found. + */ + timeElapsedKeyed: function(aHistogram, aKey, aObj) { + return TelemetryStopwatchImpl.timeElapsed(aHistogram, aObj, aKey); + }, + + /** + * Stops the timer associated with the given keyed histogram (and object), + * calculates the time delta between start and finish, and adds the value + * to the keyed histogram. + * + * @param {String} aHistogram - a string which must be a valid histogram name. + * + * @param {String} aKey - a string which must be a valid histgram key. + * + * @param {Object} aObj - optional parameter which associates the histogram + * timer with the given object. + * + * @returns {Boolean} True if the timer was succesfully stopped and the data + * was added to the histogram, False otherwise. + */ + finishKeyed: function(aHistogram, aKey, aObj) { + return TelemetryStopwatchImpl.finish(aHistogram, aObj, aKey); + } +}; + +this.TelemetryStopwatchImpl = { + start: function(histogram, object, key) { + if (Timers.has(histogram, object, key)) { + Timers.delete(histogram, object, key); + Cu.reportError(`TelemetryStopwatch: key "${histogram}" was already ` + + "initialized"); + return false; + } + + return Timers.put(histogram, object, key, Components.utils.now()); + }, + + cancel: function (histogram, object, key) { + return Timers.delete(histogram, object, key); + }, + + timeElapsed: function(histogram, object, key) { + let startTime = Timers.get(histogram, object, key); + if (startTime === null) { + Cu.reportError("TelemetryStopwatch: requesting elapsed time for " + + `nonexisting stopwatch. Histogram: "${histogram}", ` + + `key: "${key}"`); + return -1; + } + + try { + let delta = Components.utils.now() - startTime + return Math.round(delta); + } catch (e) { + Cu.reportError("TelemetryStopwatch: failed to calculate elapsed time " + + `for Histogram: "${histogram}", key: "${key}", ` + + `exception: ${Log.exceptionStr(e)}`); + return -1; + } + }, + + finish: function(histogram, object, key) { + let delta = this.timeElapsed(histogram, object, key); + if (delta == -1) { + return false; + } + + try { + if (key) { + Telemetry.getKeyedHistogramById(histogram).add(key, delta); + } else { + Telemetry.getHistogramById(histogram).add(delta); + } + } catch (e) { + Cu.reportError("TelemetryStopwatch: failed to update the Histogram " + + `"${histogram}", using key: "${key}", ` + + `exception: ${Log.exceptionStr(e)}`); + return false; + } + + return Timers.delete(histogram, object, key); + } +} diff --git a/toolkit/components/telemetry/TelemetryStorage.jsm b/toolkit/components/telemetry/TelemetryStorage.jsm new file mode 100644 index 000000000..91cfc993d --- /dev/null +++ b/toolkit/components/telemetry/TelemetryStorage.jsm @@ -0,0 +1,1882 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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 = ["TelemetryStorage"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/AppConstants.jsm", this); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm", this); + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryStorage::"; + +const Telemetry = Services.telemetry; +const Utils = TelemetryUtils; + +// Compute the path of the pings archive on the first use. +const DATAREPORTING_DIR = "datareporting"; +const PINGS_ARCHIVE_DIR = "archived"; +const ABORTED_SESSION_FILE_NAME = "aborted-session-ping"; +const DELETION_PING_FILE_NAME = "pending-deletion-ping"; +const SESSION_STATE_FILE_NAME = "session-state.json"; + +XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() { + return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR); +}); +XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() { + return OS.Path.join(gDataReportingDir, PINGS_ARCHIVE_DIR); +}); +XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() { + return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME); +}); +XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() { + return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME); +}); +XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", + "resource://services-common/utils.js"); +// Maxmimum time, in milliseconds, archive pings should be retained. +const MAX_ARCHIVED_PINGS_RETENTION_MS = 60 * 24 * 60 * 60 * 1000; // 60 days + +// Maximum space the archive can take on disk (in Bytes). +const ARCHIVE_QUOTA_BYTES = 120 * 1024 * 1024; // 120 MB +// Maximum space the outgoing pings can take on disk, for Desktop (in Bytes). +const PENDING_PINGS_QUOTA_BYTES_DESKTOP = 15 * 1024 * 1024; // 15 MB +// Maximum space the outgoing pings can take on disk, for Mobile (in Bytes). +const PENDING_PINGS_QUOTA_BYTES_MOBILE = 1024 * 1024; // 1 MB + +// The maximum size a pending/archived ping can take on disk. +const PING_FILE_MAXIMUM_SIZE_BYTES = 1024 * 1024; // 1 MB + +// This special value is submitted when the archive is outside of the quota. +const ARCHIVE_SIZE_PROBE_SPECIAL_VALUE = 300; + +// This special value is submitted when the pending pings is outside of the quota, as +// we don't know the size of the pings above the quota. +const PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE = 17; + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * This is thrown by |TelemetryStorage.loadPingFile| when reading the ping + * from the disk fails. + */ +function PingReadError(message="Error reading the ping file", becauseNoSuchFile = false) { + Error.call(this, message); + let error = new Error(); + this.name = "PingReadError"; + this.message = message; + this.stack = error.stack; + this.becauseNoSuchFile = becauseNoSuchFile; +} +PingReadError.prototype = Object.create(Error.prototype); +PingReadError.prototype.constructor = PingReadError; + +/** + * This is thrown by |TelemetryStorage.loadPingFile| when parsing the ping JSON + * content fails. + */ +function PingParseError(message="Error parsing ping content") { + Error.call(this, message); + let error = new Error(); + this.name = "PingParseError"; + this.message = message; + this.stack = error.stack; +} +PingParseError.prototype = Object.create(Error.prototype); +PingParseError.prototype.constructor = PingParseError; + +/** + * This is a policy object used to override behavior for testing. + */ +var Policy = { + now: () => new Date(), + getArchiveQuota: () => ARCHIVE_QUOTA_BYTES, + getPendingPingsQuota: () => (AppConstants.platform in ["android", "gonk"]) + ? PENDING_PINGS_QUOTA_BYTES_MOBILE + : PENDING_PINGS_QUOTA_BYTES_DESKTOP, +}; + +/** + * Wait for all promises in iterable to resolve or reject. This function + * always resolves its promise with undefined, and never rejects. + */ +function waitForAll(it) { + let dummy = () => {}; + let promises = Array.from(it, p => p.catch(dummy)); + return Promise.all(promises); +} + +/** + * Permanently intern the given string. This is mainly used for the ping.type + * strings that can be excessively duplicated in the _archivedPings map. Do not + * pass large or temporary strings to this function. + */ +function internString(str) { + return Symbol.keyFor(Symbol.for(str)); +} + +this.TelemetryStorage = { + get pingDirectoryPath() { + return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings"); + }, + + /** + * The maximum size a ping can have, in bytes. + */ + get MAXIMUM_PING_SIZE() { + return PING_FILE_MAXIMUM_SIZE_BYTES; + }, + /** + * Shutdown & block on any outstanding async activity in this module. + * + * @return {Promise} Promise that is resolved when shutdown is complete. + */ + shutdown: function() { + return TelemetryStorageImpl.shutdown(); + }, + + /** + * Save an archived ping to disk. + * + * @param {object} ping The ping data to archive. + * @return {promise} Promise that is resolved when the ping is successfully archived. + */ + saveArchivedPing: function(ping) { + return TelemetryStorageImpl.saveArchivedPing(ping); + }, + + /** + * Load an archived ping from disk. + * + * @param {string} id The pings id. + * @return {promise} Promise that is resolved with the ping data. + */ + loadArchivedPing: function(id) { + return TelemetryStorageImpl.loadArchivedPing(id); + }, + + /** + * Get a list of info on the archived pings. + * This will scan the archive directory and grab basic data about the existing + * pings out of their filename. + * + * @return {promise>} + */ + loadArchivedPingList: function() { + return TelemetryStorageImpl.loadArchivedPingList(); + }, + + /** + * Clean the pings archive by removing old pings. + * This will scan the archive directory. + * + * @return {Promise} Resolved when the cleanup task completes. + */ + runCleanPingArchiveTask: function() { + return TelemetryStorageImpl.runCleanPingArchiveTask(); + }, + + /** + * Run the task to enforce the pending pings quota. + * + * @return {Promise} Resolved when the cleanup task completes. + */ + runEnforcePendingPingsQuotaTask: function() { + return TelemetryStorageImpl.runEnforcePendingPingsQuotaTask(); + }, + + /** + * Run the task to remove all the pending pings (except the deletion ping). + * + * @return {Promise} Resolved when the pings are removed. + */ + runRemovePendingPingsTask: function() { + return TelemetryStorageImpl.runRemovePendingPingsTask(); + }, + + /** + * Reset the storage state in tests. + */ + reset: function() { + return TelemetryStorageImpl.reset(); + }, + + /** + * Test method that allows waiting on the archive clean task to finish. + */ + testCleanupTaskPromise: function() { + return (TelemetryStorageImpl._cleanArchiveTask || Promise.resolve()); + }, + + /** + * Test method that allows waiting on the pending pings quota task to finish. + */ + testPendingQuotaTaskPromise: function() { + return (TelemetryStorageImpl._enforcePendingPingsQuotaTask || Promise.resolve()); + }, + + /** + * Save a pending - outgoing - ping to disk and track it. + * + * @param {Object} ping The ping data. + * @return {Promise} Resolved when the ping was saved. + */ + savePendingPing: function(ping) { + return TelemetryStorageImpl.savePendingPing(ping); + }, + + /** + * Saves session data to disk. + * @param {Object} sessionData The session data. + * @return {Promise} Resolved when the data was saved. + */ + saveSessionData: function(sessionData) { + return TelemetryStorageImpl.saveSessionData(sessionData); + }, + + /** + * Loads session data from a session data file. + * @return {Promise} Resolved with the session data in object form. + */ + loadSessionData: function() { + return TelemetryStorageImpl.loadSessionData(); + }, + + /** + * Load a pending ping from disk by id. + * + * @param {String} id The pings id. + * @return {Promise} Resolved with the loaded ping data. + */ + loadPendingPing: function(id) { + return TelemetryStorageImpl.loadPendingPing(id); + }, + + /** + * Remove a pending ping from disk by id. + * + * @param {String} id The pings id. + * @return {Promise} Resolved when the ping was removed. + */ + removePendingPing: function(id) { + return TelemetryStorageImpl.removePendingPing(id); + }, + + /** + * Returns a list of the currently pending pings in the format: + * { + * id: , // The pings UUID. + * lastModificationDate: , // Timestamp of the pings last modification. + * } + * This populates the list by scanning the disk. + * + * @return {Promise} Resolved with the ping list. + */ + loadPendingPingList: function() { + return TelemetryStorageImpl.loadPendingPingList(); + }, + + /** + * Returns a list of the currently pending pings in the format: + * { + * id: , // The pings UUID. + * lastModificationDate: , // Timestamp of the pings last modification. + * } + * This does not scan pending pings on disk. + * + * @return {sequence} The current pending ping list. + */ + getPendingPingList: function() { + return TelemetryStorageImpl.getPendingPingList(); + }, + + /** + * Save an aborted-session ping to disk. This goes to a special location so + * it is not picked up as a pending ping. + * + * @param {object} ping The ping data to save. + * @return {promise} Promise that is resolved when the ping is successfully saved. + */ + saveAbortedSessionPing: function(ping) { + return TelemetryStorageImpl.saveAbortedSessionPing(ping); + }, + + /** + * Load the aborted-session ping from disk if present. + * + * @return {promise} Promise that is resolved with the ping data if found. + * Otherwise returns null. + */ + loadAbortedSessionPing: function() { + return TelemetryStorageImpl.loadAbortedSessionPing(); + }, + + /** + * Save the deletion ping. + * @param ping The deletion ping. + * @return {Promise} A promise resolved when the ping is saved. + */ + saveDeletionPing: function(ping) { + return TelemetryStorageImpl.saveDeletionPing(ping); + }, + + /** + * Remove the deletion ping. + * @return {Promise} Resolved when the ping is deleted from the disk. + */ + removeDeletionPing: function() { + return TelemetryStorageImpl.removeDeletionPing(); + }, + + /** + * Check if the ping id identifies a deletion ping. + */ + isDeletionPing: function(aPingId) { + return TelemetryStorageImpl.isDeletionPing(aPingId); + }, + + /** + * Remove the aborted-session ping if present. + * + * @return {promise} Promise that is resolved once the ping is removed. + */ + removeAbortedSessionPing: function() { + return TelemetryStorageImpl.removeAbortedSessionPing(); + }, + + /** + * Save a single ping to a file. + * + * @param {object} ping The content of the ping to save. + * @param {string} file The destination file. + * @param {bool} overwrite If |true|, the file will be overwritten if it exists, + * if |false| the file will not be overwritten and no error will be reported if + * the file exists. + * @returns {promise} + */ + savePingToFile: function(ping, file, overwrite) { + return TelemetryStorageImpl.savePingToFile(ping, file, overwrite); + }, + + /** + * Save a ping to its file. + * + * @param {object} ping The content of the ping to save. + * @param {bool} overwrite If |true|, the file will be overwritten + * if it exists. + * @returns {promise} + */ + savePing: function(ping, overwrite) { + return TelemetryStorageImpl.savePing(ping, overwrite); + }, + + /** + * Add a ping to the saved pings directory so that it gets saved + * and sent along with other pings. + * + * @param {Object} pingData The ping object. + * @return {Promise} A promise resolved when the ping is saved to the pings directory. + */ + addPendingPing: function(pingData) { + return TelemetryStorageImpl.addPendingPing(pingData); + }, + + /** + * Remove the file for a ping + * + * @param {object} ping The ping. + * @returns {promise} + */ + cleanupPingFile: function(ping) { + return TelemetryStorageImpl.cleanupPingFile(ping); + }, + + /** + * The number of pending pings on disk. + */ + get pendingPingCount() { + return TelemetryStorageImpl.pendingPingCount; + }, + + /** + * Loads a ping file. + * @param {String} aFilePath The path of the ping file. + * @return {Promise} A promise resolved with the ping content or rejected if the + * ping contains invalid data. + */ + loadPingFile: Task.async(function* (aFilePath) { + return TelemetryStorageImpl.loadPingFile(aFilePath); + }), + + /** + * Remove FHR database files. This is temporary and will be dropped in + * the future. + * @return {Promise} Resolved when the database files are deleted. + */ + removeFHRDatabase: function() { + return TelemetryStorageImpl.removeFHRDatabase(); + }, + + /** + * Only used in tests, builds an archived ping path from the ping metadata. + * @param {String} aPingId The ping id. + * @param {Object} aDate The ping creation date. + * @param {String} aType The ping type. + * @return {String} The full path to the archived ping. + */ + _testGetArchivedPingPath: function(aPingId, aDate, aType) { + return getArchivedPingPath(aPingId, aDate, aType); + }, + + /** + * Only used in tests, this helper extracts ping metadata from a given filename. + * + * @param fileName {String} The filename. + * @return {Object} Null if the filename didn't match the expected form. + * Otherwise an object with the extracted data in the form: + * { timestamp: , + * id: , + * type: } + */ + _testGetArchivedPingDataFromFileName: function(aFileName) { + return TelemetryStorageImpl._getArchivedPingDataFromFileName(aFileName); + }, + + /** + * Only used in tests, this helper allows cleaning up the pending ping storage. + */ + testClearPendingPings: function() { + return TelemetryStorageImpl.runRemovePendingPingsTask(); + } +}; + +/** + * This object allows the serialisation of asynchronous tasks. This is particularly + * useful to serialise write access to the disk in order to prevent race conditions + * to corrupt the data being written. + * We are using this to synchronize saving to the file that TelemetrySession persists + * its state in. + */ +function SaveSerializer() { + this._queuedOperations = []; + this._queuedInProgress = false; + this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); +} + +SaveSerializer.prototype = { + /** + * Enqueues an operation to a list to serialise their execution in order to prevent race + * conditions. Useful to serialise access to disk. + * + * @param {Function} aFunction The task function to enqueue. It must return a promise. + * @return {Promise} A promise resolved when the enqueued task completes. + */ + enqueueTask: function (aFunction) { + let promise = new Promise((resolve, reject) => + this._queuedOperations.push([aFunction, resolve, reject])); + + if (this._queuedOperations.length == 1) { + this._popAndPerformQueuedOperation(); + } + return promise; + }, + + /** + * Make sure to flush all the pending operations. + * @return {Promise} A promise resolved when all the pending operations have completed. + */ + flushTasks: function () { + let dummyTask = () => new Promise(resolve => resolve()); + return this.enqueueTask(dummyTask); + }, + + /** + * Pop a task from the queue, executes it and continue to the next one. + * This function recursively pops all the tasks. + */ + _popAndPerformQueuedOperation: function () { + if (!this._queuedOperations.length || this._queuedInProgress) { + return; + } + + this._log.trace("_popAndPerformQueuedOperation - Performing queued operation."); + let [func, resolve, reject] = this._queuedOperations.shift(); + let promise; + + try { + this._queuedInProgress = true; + promise = func(); + } catch (ex) { + this._log.warn("_popAndPerformQueuedOperation - Queued operation threw during execution. ", + ex); + this._queuedInProgress = false; + reject(ex); + this._popAndPerformQueuedOperation(); + return; + } + + if (!promise || typeof(promise.then) != "function") { + let msg = "Queued operation did not return a promise: " + func; + this._log.warn("_popAndPerformQueuedOperation - " + msg); + + this._queuedInProgress = false; + reject(new Error(msg)); + this._popAndPerformQueuedOperation(); + return; + } + + promise.then(result => { + this._queuedInProgress = false; + resolve(result); + this._popAndPerformQueuedOperation(); + }, + error => { + this._log.warn("_popAndPerformQueuedOperation - Failure when performing queued operation.", + error); + this._queuedInProgress = false; + reject(error); + this._popAndPerformQueuedOperation(); + }); + }, +}; + +var TelemetryStorageImpl = { + _logger: null, + // Used to serialize aborted session ping writes to disk. + _abortedSessionSerializer: new SaveSerializer(), + // Used to serialize deletion ping writes to disk. + _deletionPingSerializer: new SaveSerializer(), + // Used to serialize session state writes to disk. + _stateSaveSerializer: new SaveSerializer(), + + // Tracks the archived pings in a Map of (id -> {timestampCreated, type}). + // We use this to cache info on archived pings to avoid scanning the disk more than once. + _archivedPings: new Map(), + // A set of promises for pings currently being archived + _activelyArchiving: new Set(), + // Track the archive loading task to prevent multiple tasks from being executed. + _scanArchiveTask: null, + // Track the archive cleanup task. + _cleanArchiveTask: null, + // Whether we already scanned the archived pings on disk. + _scannedArchiveDirectory: false, + + // Track the pending ping removal task. + _removePendingPingsTask: null, + + // This tracks all the pending async ping save activity. + _activePendingPingSaves: new Set(), + + // Tracks the pending pings in a Map of (id -> {timestampCreated, type}). + // We use this to cache info on pending pings to avoid scanning the disk more than once. + _pendingPings: new Map(), + + // Track the pending pings enforce quota task. + _enforcePendingPingsQuotaTask: null, + + // Track the shutdown process to bail out of the clean up task quickly. + _shutdown: false, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + + return this._logger; + }, + + /** + * Shutdown & block on any outstanding async activity in this module. + * + * @return {Promise} Promise that is resolved when shutdown is complete. + */ + shutdown: Task.async(function*() { + this._shutdown = true; + + // If the following tasks are still running, block on them. They will bail out as soon + // as possible. + yield this._abortedSessionSerializer.flushTasks().catch(ex => { + this._log.error("shutdown - failed to flush aborted-session writes", ex); + }); + + yield this._deletionPingSerializer.flushTasks().catch(ex => { + this._log.error("shutdown - failed to flush deletion ping writes", ex); + }); + + if (this._cleanArchiveTask) { + yield this._cleanArchiveTask.catch(ex => { + this._log.error("shutdown - the archive cleaning task failed", ex); + }); + } + + if (this._enforcePendingPingsQuotaTask) { + yield this._enforcePendingPingsQuotaTask.catch(ex => { + this._log.error("shutdown - the pending pings quota task failed", ex); + }); + } + + if (this._removePendingPingsTask) { + yield this._removePendingPingsTask.catch(ex => { + this._log.error("shutdown - the pending pings removal task failed", ex); + }); + } + + // Wait on pending pings still being saved. While OS.File should have shutdown + // blockers in place, we a) have seen weird errors being reported that might + // indicate a bad shutdown path and b) might have completion handlers hanging + // off the save operations that don't expect to be late in shutdown. + yield this.promisePendingPingSaves(); + }), + + /** + * Save an archived ping to disk. + * + * @param {object} ping The ping data to archive. + * @return {promise} Promise that is resolved when the ping is successfully archived. + */ + saveArchivedPing: function(ping) { + let promise = this._saveArchivedPingTask(ping); + this._activelyArchiving.add(promise); + promise.then((r) => { this._activelyArchiving.delete(promise); }, + (e) => { this._activelyArchiving.delete(promise); }); + return promise; + }, + + _saveArchivedPingTask: Task.async(function*(ping) { + const creationDate = new Date(ping.creationDate); + if (this._archivedPings.has(ping.id)) { + const data = this._archivedPings.get(ping.id); + if (data.timestampCreated > creationDate.getTime()) { + this._log.error("saveArchivedPing - trying to overwrite newer ping with the same id"); + return Promise.reject(new Error("trying to overwrite newer ping with the same id")); + } + this._log.warn("saveArchivedPing - overwriting older ping with the same id"); + } + + // Get the archived ping path and append the lz4 suffix to it (so we have 'jsonlz4'). + const filePath = getArchivedPingPath(ping.id, creationDate, ping.type) + "lz4"; + yield OS.File.makeDir(OS.Path.dirname(filePath), { ignoreExisting: true, + from: OS.Constants.Path.profileDir }); + yield this.savePingToFile(ping, filePath, /* overwrite*/ true, /* compressed*/ true); + + this._archivedPings.set(ping.id, { + timestampCreated: creationDate.getTime(), + type: internString(ping.type), + }); + + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").add(); + return undefined; + }), + + /** + * Load an archived ping from disk. + * + * @param {string} id The pings id. + * @return {promise} Promise that is resolved with the ping data. + */ + loadArchivedPing: Task.async(function*(id) { + this._log.trace("loadArchivedPing - id: " + id); + + const data = this._archivedPings.get(id); + if (!data) { + this._log.trace("loadArchivedPing - no ping with id: " + id); + return Promise.reject(new Error("TelemetryStorage.loadArchivedPing - no ping with id " + id)); + } + + const path = getArchivedPingPath(id, new Date(data.timestampCreated), data.type); + const pathCompressed = path + "lz4"; + + // Purge pings which are too big. + let checkSize = function*(path) { + const fileSize = (yield OS.File.stat(path)).size; + if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) { + Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB") + .add(Math.floor(fileSize / 1024 / 1024)); + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").add(); + yield OS.File.remove(path, {ignoreAbsent: true}); + throw new Error("loadArchivedPing - exceeded the maximum ping size: " + fileSize); + } + }; + + try { + // Try to load a compressed version of the archived ping first. + this._log.trace("loadArchivedPing - loading ping from: " + pathCompressed); + yield* checkSize(pathCompressed); + return yield this.loadPingFile(pathCompressed, /* compressed*/ true); + } catch (ex) { + if (!ex.becauseNoSuchFile) { + throw ex; + } + // If that fails, look for the uncompressed version. + this._log.trace("loadArchivedPing - compressed ping not found, loading: " + path); + yield* checkSize(path); + return yield this.loadPingFile(path, /* compressed*/ false); + } + }), + + /** + * Saves session data to disk. + */ + saveSessionData: function(sessionData) { + return this._stateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData)); + }, + + _saveSessionData: Task.async(function* (sessionData) { + let dataDir = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR); + yield OS.File.makeDir(dataDir); + + let filePath = OS.Path.join(gDataReportingDir, SESSION_STATE_FILE_NAME); + try { + yield CommonUtils.writeJSON(sessionData, filePath); + } catch (e) { + this._log.error("_saveSessionData - Failed to write session data to " + filePath, e); + Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_SAVE").add(1); + } + }), + + /** + * Loads session data from the session data file. + * @return {Promise} A promise resolved with an object on success, + * with null otherwise. + */ + loadSessionData: function() { + return this._stateSaveSerializer.enqueueTask(() => this._loadSessionData()); + }, + + _loadSessionData: Task.async(function* () { + const dataFile = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR, + SESSION_STATE_FILE_NAME); + let content; + try { + content = yield OS.File.read(dataFile, { encoding: "utf-8" }); + } catch (ex) { + this._log.info("_loadSessionData - can not load session data file", ex); + Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_LOAD").add(1); + return null; + } + + let data; + try { + data = JSON.parse(content); + } catch (ex) { + this._log.error("_loadSessionData - failed to parse session data", ex); + Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_PARSE").add(1); + return null; + } + + return data; + }), + + /** + * Remove an archived ping from disk. + * + * @param {string} id The pings id. + * @param {number} timestampCreated The pings creation timestamp. + * @param {string} type The pings type. + * @return {promise} Promise that is resolved when the pings is removed. + */ + _removeArchivedPing: Task.async(function*(id, timestampCreated, type) { + this._log.trace("_removeArchivedPing - id: " + id + ", timestampCreated: " + timestampCreated + ", type: " + type); + const path = getArchivedPingPath(id, new Date(timestampCreated), type); + const pathCompressed = path + "lz4"; + + this._log.trace("_removeArchivedPing - removing ping from: " + path); + yield OS.File.remove(path, {ignoreAbsent: true}); + yield OS.File.remove(pathCompressed, {ignoreAbsent: true}); + // Remove the ping from the cache. + this._archivedPings.delete(id); + }), + + /** + * Clean the pings archive by removing old pings. + * + * @return {Promise} Resolved when the cleanup task completes. + */ + runCleanPingArchiveTask: function() { + // If there's an archive cleaning task already running, return it. + if (this._cleanArchiveTask) { + return this._cleanArchiveTask; + } + + // Make sure to clear |_cleanArchiveTask| once done. + let clear = () => this._cleanArchiveTask = null; + // Since there's no archive cleaning task running, start it. + this._cleanArchiveTask = this._cleanArchive().then(clear, clear); + return this._cleanArchiveTask; + }, + + /** + * Removes pings which are too old from the pings archive. + * @return {Promise} Resolved when the ping age check is complete. + */ + _purgeOldPings: Task.async(function*() { + this._log.trace("_purgeOldPings"); + + const nowDate = Policy.now(); + const startTimeStamp = nowDate.getTime(); + let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath); + let subdirs = (yield dirIterator.nextBatch()).filter(e => e.isDir); + dirIterator.close(); + + // Keep track of the newest removed month to update the cache, if needed. + let newestRemovedMonthTimestamp = null; + let evictedDirsCount = 0; + let maxDirAgeInMonths = 0; + + // Walk through the monthly subdirs of the form / + for (let dir of subdirs) { + if (this._shutdown) { + this._log.trace("_purgeOldPings - Terminating the clean up task due to shutdown"); + return; + } + + if (!isValidArchiveDir(dir.name)) { + this._log.warn("_purgeOldPings - skipping invalidly named subdirectory " + dir.path); + continue; + } + + const archiveDate = getDateFromArchiveDir(dir.name); + if (!archiveDate) { + this._log.warn("_purgeOldPings - skipping invalid subdirectory date " + dir.path); + continue; + } + + // If this archive directory is older than 180 days, remove it. + if ((startTimeStamp - archiveDate.getTime()) > MAX_ARCHIVED_PINGS_RETENTION_MS) { + try { + yield OS.File.removeDir(dir.path); + evictedDirsCount++; + + // Update the newest removed month. + newestRemovedMonthTimestamp = Math.max(archiveDate, newestRemovedMonthTimestamp); + } catch (ex) { + this._log.error("_purgeOldPings - Unable to remove " + dir.path, ex); + } + } else { + // We're not removing this directory, so record the age for the oldest directory. + const dirAgeInMonths = Utils.getElapsedTimeInMonths(archiveDate, nowDate); + maxDirAgeInMonths = Math.max(dirAgeInMonths, maxDirAgeInMonths); + } + } + + // Trigger scanning of the archived pings. + yield this.loadArchivedPingList(); + + // Refresh the cache: we could still skip this, but it's cheap enough to keep it + // to avoid introducing task dependencies. + if (newestRemovedMonthTimestamp) { + // Scan the archive cache for pings older than the newest directory pruned above. + for (let [id, info] of this._archivedPings) { + const timestampCreated = new Date(info.timestampCreated); + if (timestampCreated.getTime() > newestRemovedMonthTimestamp) { + continue; + } + // Remove outdated pings from the cache. + this._archivedPings.delete(id); + } + } + + const endTimeStamp = Policy.now().getTime(); + + // Save the time it takes to evict old directories and the eviction count. + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS") + .add(evictedDirsCount); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_DIRS_MS") + .add(Math.ceil(endTimeStamp - startTimeStamp)); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE") + .add(maxDirAgeInMonths); + }), + + /** + * Enforce a disk quota for the pings archive. + * @return {Promise} Resolved when the quota check is complete. + */ + _enforceArchiveQuota: Task.async(function*() { + this._log.trace("_enforceArchiveQuota"); + let startTimeStamp = Policy.now().getTime(); + + // Build an ordered list, from newer to older, of archived pings. + let pingList = Array.from(this._archivedPings, p => ({ + id: p[0], + timestampCreated: p[1].timestampCreated, + type: p[1].type, + })); + + pingList.sort((a, b) => b.timestampCreated - a.timestampCreated); + + // If our archive is too big, we should reduce it to reach 90% of the quota. + const SAFE_QUOTA = Policy.getArchiveQuota() * 0.9; + // The index of the last ping to keep. Pings older than this one will be deleted if + // the archive exceeds the quota. + let lastPingIndexToKeep = null; + let archiveSizeInBytes = 0; + + // Find the disk size of the archive. + for (let i = 0; i < pingList.length; i++) { + if (this._shutdown) { + this._log.trace("_enforceArchiveQuota - Terminating the clean up task due to shutdown"); + return; + } + + let ping = pingList[i]; + + // Get the size for this ping. + const fileSize = + yield getArchivedPingSize(ping.id, new Date(ping.timestampCreated), ping.type); + if (!fileSize) { + this._log.warn("_enforceArchiveQuota - Unable to find the size of ping " + ping.id); + continue; + } + + // Enforce a maximum file size limit on archived pings. + if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) { + this._log.error("_enforceArchiveQuota - removing file exceeding size limit, size: " + fileSize); + // We just remove the ping from the disk, we don't bother removing it from pingList + // since it won't contribute to the quota. + yield this._removeArchivedPing(ping.id, ping.timestampCreated, ping.type) + .catch(e => this._log.error("_enforceArchiveQuota - failed to remove archived ping" + ping.id)); + Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB") + .add(Math.floor(fileSize / 1024 / 1024)); + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").add(); + continue; + } + + archiveSizeInBytes += fileSize; + + if (archiveSizeInBytes < SAFE_QUOTA) { + // We save the index of the last ping which is ok to keep in order to speed up ping + // pruning. + lastPingIndexToKeep = i; + } else if (archiveSizeInBytes > Policy.getArchiveQuota()) { + // Ouch, our ping archive is too big. Bail out and start pruning! + break; + } + } + + // Save the time it takes to check if the archive is over-quota. + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_CHECKING_OVER_QUOTA_MS") + .add(Math.round(Policy.now().getTime() - startTimeStamp)); + + let submitProbes = (sizeInMB, evictedPings, elapsedMs) => { + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").add(sizeInMB); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").add(evictedPings); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").add(elapsedMs); + }; + + // Check if we're using too much space. If not, submit the archive size and bail out. + if (archiveSizeInBytes < Policy.getArchiveQuota()) { + submitProbes(Math.round(archiveSizeInBytes / 1024 / 1024), 0, 0); + return; + } + + this._log.info("_enforceArchiveQuota - archive size: " + archiveSizeInBytes + "bytes" + + ", safety quota: " + SAFE_QUOTA + "bytes"); + + startTimeStamp = Policy.now().getTime(); + let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1); + + // Remove all the pings older than the last one which we are safe to keep. + for (let ping of pingsToPurge) { + if (this._shutdown) { + this._log.trace("_enforceArchiveQuota - Terminating the clean up task due to shutdown"); + return; + } + + // This list is guaranteed to be in order, so remove the pings at its + // beginning (oldest). + yield this._removeArchivedPing(ping.id, ping.timestampCreated, ping.type); + } + + const endTimeStamp = Policy.now().getTime(); + submitProbes(ARCHIVE_SIZE_PROBE_SPECIAL_VALUE, pingsToPurge.length, + Math.ceil(endTimeStamp - startTimeStamp)); + }), + + _cleanArchive: Task.async(function*() { + this._log.trace("cleanArchiveTask"); + + if (!(yield OS.File.exists(gPingsArchivePath))) { + return; + } + + // Remove pings older than 180 days. + try { + yield this._purgeOldPings(); + } catch (ex) { + this._log.error("_cleanArchive - There was an error removing old directories", ex); + } + + // Make sure we respect the archive disk quota. + yield this._enforceArchiveQuota(); + }), + + /** + * Run the task to enforce the pending pings quota. + * + * @return {Promise} Resolved when the cleanup task completes. + */ + runEnforcePendingPingsQuotaTask: Task.async(function*() { + // If there's a cleaning task already running, return it. + if (this._enforcePendingPingsQuotaTask) { + return this._enforcePendingPingsQuotaTask; + } + + // Since there's no quota enforcing task running, start it. + try { + this._enforcePendingPingsQuotaTask = this._enforcePendingPingsQuota(); + yield this._enforcePendingPingsQuotaTask; + } finally { + this._enforcePendingPingsQuotaTask = null; + } + return undefined; + }), + + /** + * Enforce a disk quota for the pending pings. + * @return {Promise} Resolved when the quota check is complete. + */ + _enforcePendingPingsQuota: Task.async(function*() { + this._log.trace("_enforcePendingPingsQuota"); + let startTimeStamp = Policy.now().getTime(); + + // Build an ordered list, from newer to older, of pending pings. + let pingList = Array.from(this._pendingPings, p => ({ + id: p[0], + lastModificationDate: p[1].lastModificationDate, + })); + + pingList.sort((a, b) => b.lastModificationDate - a.lastModificationDate); + + // If our pending pings directory is too big, we should reduce it to reach 90% of the quota. + const SAFE_QUOTA = Policy.getPendingPingsQuota() * 0.9; + // The index of the last ping to keep. Pings older than this one will be deleted if + // the pending pings directory size exceeds the quota. + let lastPingIndexToKeep = null; + let pendingPingsSizeInBytes = 0; + + // Find the disk size of the pending pings directory. + for (let i = 0; i < pingList.length; i++) { + if (this._shutdown) { + this._log.trace("_enforcePendingPingsQuota - Terminating the clean up task due to shutdown"); + return; + } + + let ping = pingList[i]; + + // Get the size for this ping. + const fileSize = yield getPendingPingSize(ping.id); + if (!fileSize) { + this._log.warn("_enforcePendingPingsQuota - Unable to find the size of ping " + ping.id); + continue; + } + + pendingPingsSizeInBytes += fileSize; + if (pendingPingsSizeInBytes < SAFE_QUOTA) { + // We save the index of the last ping which is ok to keep in order to speed up ping + // pruning. + lastPingIndexToKeep = i; + } else if (pendingPingsSizeInBytes > Policy.getPendingPingsQuota()) { + // Ouch, our pending pings directory size is too big. Bail out and start pruning! + break; + } + } + + // Save the time it takes to check if the pending pings are over-quota. + Telemetry.getHistogramById("TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS") + .add(Math.round(Policy.now().getTime() - startTimeStamp)); + + let recordHistograms = (sizeInMB, evictedPings, elapsedMs) => { + Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").add(sizeInMB); + Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").add(evictedPings); + Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").add(elapsedMs); + }; + + // Check if we're using too much space. If not, bail out. + if (pendingPingsSizeInBytes < Policy.getPendingPingsQuota()) { + recordHistograms(Math.round(pendingPingsSizeInBytes / 1024 / 1024), 0, 0); + return; + } + + this._log.info("_enforcePendingPingsQuota - size: " + pendingPingsSizeInBytes + "bytes" + + ", safety quota: " + SAFE_QUOTA + "bytes"); + + startTimeStamp = Policy.now().getTime(); + let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1); + + // Remove all the pings older than the last one which we are safe to keep. + for (let ping of pingsToPurge) { + if (this._shutdown) { + this._log.trace("_enforcePendingPingsQuota - Terminating the clean up task due to shutdown"); + return; + } + + // This list is guaranteed to be in order, so remove the pings at its + // beginning (oldest). + yield this.removePendingPing(ping.id); + } + + const endTimeStamp = Policy.now().getTime(); + // We don't know the size of the pending pings directory if we are above the quota, + // since we stop scanning once we reach the quota. We use a special value to show + // this condition. + recordHistograms(PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE, pingsToPurge.length, + Math.ceil(endTimeStamp - startTimeStamp)); + }), + + /** + * Reset the storage state in tests. + */ + reset: function() { + this._shutdown = false; + this._scannedArchiveDirectory = false; + this._archivedPings = new Map(); + this._scannedPendingDirectory = false; + this._pendingPings = new Map(); + }, + + /** + * Get a list of info on the archived pings. + * This will scan the archive directory and grab basic data about the existing + * pings out of their filename. + * + * @return {promise>} + */ + loadArchivedPingList: Task.async(function*() { + // If there's an archive loading task already running, return it. + if (this._scanArchiveTask) { + return this._scanArchiveTask; + } + + yield waitForAll(this._activelyArchiving); + + if (this._scannedArchiveDirectory) { + this._log.trace("loadArchivedPingList - Archive already scanned, hitting cache."); + return this._archivedPings; + } + + // Since there's no archive loading task running, start it. + let result; + try { + this._scanArchiveTask = this._scanArchive(); + result = yield this._scanArchiveTask; + } finally { + this._scanArchiveTask = null; + } + return result; + }), + + _scanArchive: Task.async(function*() { + this._log.trace("_scanArchive"); + + let submitProbes = (pingCount, dirCount) => { + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT") + .add(pingCount); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT") + .add(dirCount); + }; + + if (!(yield OS.File.exists(gPingsArchivePath))) { + submitProbes(0, 0); + return new Map(); + } + + let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath); + let subdirs = + (yield dirIterator.nextBatch()).filter(e => e.isDir).filter(e => isValidArchiveDir(e.name)); + dirIterator.close(); + + // Walk through the monthly subdirs of the form / + for (let dir of subdirs) { + this._log.trace("_scanArchive - checking in subdir: " + dir.path); + let pingIterator = new OS.File.DirectoryIterator(dir.path); + let pings = (yield pingIterator.nextBatch()).filter(e => !e.isDir); + pingIterator.close(); + + // Now process any ping files of the form "...[json|jsonlz4]". + for (let p of pings) { + // data may be null if the filename doesn't match the above format. + let data = this._getArchivedPingDataFromFileName(p.name); + if (!data) { + continue; + } + + // In case of conflicts, overwrite only with newer pings. + if (this._archivedPings.has(data.id)) { + const overwrite = data.timestamp > this._archivedPings.get(data.id).timestampCreated; + this._log.warn("_scanArchive - have seen this id before: " + data.id + + ", overwrite: " + overwrite); + if (!overwrite) { + continue; + } + + yield this._removeArchivedPing(data.id, data.timestampCreated, data.type) + .catch((e) => this._log.warn("_scanArchive - failed to remove ping", e)); + } + + this._archivedPings.set(data.id, { + timestampCreated: data.timestamp, + type: internString(data.type), + }); + } + } + + // Mark the archive as scanned, so we no longer hit the disk. + this._scannedArchiveDirectory = true; + // Update the ping and directories count histograms. + submitProbes(this._archivedPings.size, subdirs.length); + return this._archivedPings; + }), + + /** + * Save a single ping to a file. + * + * @param {object} ping The content of the ping to save. + * @param {string} file The destination file. + * @param {bool} overwrite If |true|, the file will be overwritten if it exists, + * if |false| the file will not be overwritten and no error will be reported if + * the file exists. + * @param {bool} [compress=false] If |true|, the file will use lz4 compression. Otherwise no + * compression will be used. + * @returns {promise} + */ + savePingToFile: Task.async(function*(ping, filePath, overwrite, compress = false) { + try { + this._log.trace("savePingToFile - path: " + filePath); + let pingString = JSON.stringify(ping); + let options = { tmpPath: filePath + ".tmp", noOverwrite: !overwrite }; + if (compress) { + options.compression = "lz4"; + } + yield OS.File.writeAtomic(filePath, pingString, options); + } catch (e) { + if (!e.becauseExists) { + throw e; + } + } + }), + + /** + * Save a ping to its file. + * + * @param {object} ping The content of the ping to save. + * @param {bool} overwrite If |true|, the file will be overwritten + * if it exists. + * @returns {promise} + */ + savePing: Task.async(function*(ping, overwrite) { + yield getPingDirectory(); + let file = pingFilePath(ping); + yield this.savePingToFile(ping, file, overwrite); + return file; + }), + + /** + * Add a ping to the saved pings directory so that it gets saved + * and sent along with other pings. + * Note: that the original ping file will not be modified. + * + * @param {Object} ping The ping object. + * @return {Promise} A promise resolved when the ping is saved to the pings directory. + */ + addPendingPing: function(ping) { + return this.savePendingPing(ping); + }, + + /** + * Remove the file for a ping + * + * @param {object} ping The ping. + * @returns {promise} + */ + cleanupPingFile: function(ping) { + return OS.File.remove(pingFilePath(ping)); + }, + + savePendingPing: function(ping) { + let p = this.savePing(ping, true).then((path) => { + this._pendingPings.set(ping.id, { + path: path, + lastModificationDate: Policy.now().getTime(), + }); + this._log.trace("savePendingPing - saved ping with id " + ping.id); + }); + this._trackPendingPingSaveTask(p); + return p; + }, + + loadPendingPing: Task.async(function*(id) { + this._log.trace("loadPendingPing - id: " + id); + let info = this._pendingPings.get(id); + if (!info) { + this._log.trace("loadPendingPing - unknown id " + id); + throw new Error("TelemetryStorage.loadPendingPing - no ping with id " + id); + } + + // Try to get the dimension of the ping. If that fails, update the histograms. + let fileSize = 0; + try { + fileSize = (yield OS.File.stat(info.path)).size; + } catch (e) { + if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile) { + throw e; + } + // Fall through and let |loadPingFile| report the error. + } + + // Purge pings which are too big. + if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) { + yield this.removePendingPing(id); + Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB") + .add(Math.floor(fileSize / 1024 / 1024)); + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").add(); + throw new Error("loadPendingPing - exceeded the maximum ping size: " + fileSize); + } + + // Try to load the ping file. Update the related histograms on failure. + let ping; + try { + ping = yield this.loadPingFile(info.path, false); + } catch (e) { + // If we failed to load the ping, check what happened and update the histogram. + if (e instanceof PingReadError) { + Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").add(); + } else if (e instanceof PingParseError) { + Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").add(); + } + // Remove the ping from the cache, so we don't try to load it again. + this._pendingPings.delete(id); + // Then propagate the rejection. + throw e; + } + + return ping; + }), + + removePendingPing: function(id) { + let info = this._pendingPings.get(id); + if (!info) { + this._log.trace("removePendingPing - unknown id " + id); + return Promise.resolve(); + } + + this._log.trace("removePendingPing - deleting ping with id: " + id + + ", path: " + info.path); + this._pendingPings.delete(id); + return OS.File.remove(info.path).catch((ex) => + this._log.error("removePendingPing - failed to remove ping", ex)); + }, + + /** + * Track any pending ping save tasks through the promise passed here. + * This is needed to block on any outstanding ping save activity. + * + * @param {Object} The save promise to track. + */ + _trackPendingPingSaveTask: function (promise) { + let clear = () => this._activePendingPingSaves.delete(promise); + promise.then(clear, clear); + this._activePendingPingSaves.add(promise); + }, + + /** + * Return a promise that allows to wait on pending pings being saved. + * @return {Object} A promise resolved when all the pending pings save promises + * are resolved. + */ + promisePendingPingSaves: function () { + // Make sure to wait for all the promises, even if they reject. We don't need to log + // the failures here, as they are already logged elsewhere. + return waitForAll(this._activePendingPingSaves); + }, + + /** + * Run the task to remove all the pending pings (except the deletion ping). + * + * @return {Promise} Resolved when the pings are removed. + */ + runRemovePendingPingsTask: Task.async(function*() { + // If we already have a pending pings removal task active, return that. + if (this._removePendingPingsTask) { + return this._removePendingPingsTask; + } + + // Start the task to remove all pending pings. Also make sure to clear the task once done. + try { + this._removePendingPingsTask = this.removePendingPings(); + yield this._removePendingPingsTask; + } finally { + this._removePendingPingsTask = null; + } + return undefined; + }), + + removePendingPings: Task.async(function*() { + this._log.trace("removePendingPings - removing all pending pings"); + + // Wait on pending pings still being saved, so so we don't miss removing them. + yield this.promisePendingPingSaves(); + + // Individually remove existing pings, so we don't interfere with operations expecting + // the pending pings directory to exist. + const directory = TelemetryStorage.pingDirectoryPath; + let iter = new OS.File.DirectoryIterator(directory); + + try { + if (!(yield iter.exists())) { + this._log.trace("removePendingPings - the pending pings directory doesn't exist"); + return; + } + + let files = (yield iter.nextBatch()).filter(e => !e.isDir); + for (let file of files) { + try { + yield OS.File.remove(file.path); + } catch (ex) { + this._log.error("removePendingPings - failed to remove file " + file.path, ex); + continue; + } + } + } finally { + yield iter.close(); + } + }), + + loadPendingPingList: function() { + // If we already have a pending scanning task active, return that. + if (this._scanPendingPingsTask) { + return this._scanPendingPingsTask; + } + + if (this._scannedPendingDirectory) { + this._log.trace("loadPendingPingList - Pending already scanned, hitting cache."); + return Promise.resolve(this._buildPingList()); + } + + // Since there's no pending pings scan task running, start it. + // Also make sure to clear the task once done. + this._scanPendingPingsTask = this._scanPendingPings().then(pings => { + this._scanPendingPingsTask = null; + return pings; + }, ex => { + this._scanPendingPingsTask = null; + throw ex; + }); + return this._scanPendingPingsTask; + }, + + getPendingPingList: function() { + return this._buildPingList(); + }, + + _scanPendingPings: Task.async(function*() { + this._log.trace("_scanPendingPings"); + + let directory = TelemetryStorage.pingDirectoryPath; + let iter = new OS.File.DirectoryIterator(directory); + let exists = yield iter.exists(); + + try { + if (!exists) { + return []; + } + + let files = (yield iter.nextBatch()).filter(e => !e.isDir); + + for (let file of files) { + if (this._shutdown) { + return []; + } + + let info; + try { + info = yield OS.File.stat(file.path); + } catch (ex) { + this._log.error("_scanPendingPings - failed to stat file " + file.path, ex); + continue; + } + + // Enforce a maximum file size limit on pending pings. + if (info.size > PING_FILE_MAXIMUM_SIZE_BYTES) { + this._log.error("_scanPendingPings - removing file exceeding size limit " + file.path); + try { + yield OS.File.remove(file.path); + } catch (ex) { + this._log.error("_scanPendingPings - failed to remove file " + file.path, ex); + } finally { + Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB") + .add(Math.floor(info.size / 1024 / 1024)); + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").add(); + continue; + } + } + + let id = OS.Path.basename(file.path); + if (!UUID_REGEX.test(id)) { + this._log.trace("_scanPendingPings - filename is not a UUID: " + id); + id = Utils.generateUUID(); + } + + this._pendingPings.set(id, { + path: file.path, + lastModificationDate: info.lastModificationDate.getTime(), + }); + } + } finally { + yield iter.close(); + } + + // Explicitly load the deletion ping from its known path, if it's there. + if (yield OS.File.exists(gDeletionPingFilePath)) { + this._log.trace("_scanPendingPings - Adding pending deletion ping."); + // We can't get the ping id or the last modification date without hitting the disk. + // Since deletion has a special handling, we don't really need those. + this._pendingPings.set(Utils.generateUUID(), { + path: gDeletionPingFilePath, + lastModificationDate: Date.now(), + }); + } + + this._scannedPendingDirectory = true; + return this._buildPingList(); + }), + + _buildPingList: function() { + const list = Array.from(this._pendingPings, p => ({ + id: p[0], + lastModificationDate: p[1].lastModificationDate, + })); + + list.sort((a, b) => b.lastModificationDate - a.lastModificationDate); + return list; + }, + + get pendingPingCount() { + return this._pendingPings.size; + }, + + /** + * Loads a ping file. + * @param {String} aFilePath The path of the ping file. + * @param {Boolean} [aCompressed=false] If |true|, expects the file to be compressed using lz4. + * @return {Promise} A promise resolved with the ping content or rejected if the + * ping contains invalid data. + * @throws {PingReadError} There was an error while reading the ping file from the disk. + * @throws {PingParseError} There was an error while parsing the JSON content of the ping file. + */ + loadPingFile: Task.async(function* (aFilePath, aCompressed = false) { + let options = {}; + if (aCompressed) { + options.compression = "lz4"; + } + + let array; + try { + array = yield OS.File.read(aFilePath, options); + } catch (e) { + this._log.trace("loadPingfile - unreadable ping " + aFilePath, e); + throw new PingReadError(e.message, e.becauseNoSuchFile); + } + + let decoder = new TextDecoder(); + let string = decoder.decode(array); + let ping; + try { + ping = JSON.parse(string); + } catch (e) { + this._log.trace("loadPingfile - unparseable ping " + aFilePath, e); + yield OS.File.remove(aFilePath).catch((ex) => { + this._log.error("loadPingFile - failed removing unparseable ping file", ex); + }); + throw new PingParseError(e.message); + } + + return ping; + }), + + /** + * Archived pings are saved with file names of the form: + * "...[json|jsonlz4]" + * This helper extracts that data from a given filename. + * + * @param fileName {String} The filename. + * @return {Object} Null if the filename didn't match the expected form. + * Otherwise an object with the extracted data in the form: + * { timestamp: , + * id: , + * type: } + */ + _getArchivedPingDataFromFileName: function(fileName) { + // Extract the parts. + let parts = fileName.split("."); + if (parts.length != 4) { + this._log.trace("_getArchivedPingDataFromFileName - should have 4 parts"); + return null; + } + + let [timestamp, uuid, type, extension] = parts; + if (extension != "json" && extension != "jsonlz4") { + this._log.trace("_getArchivedPingDataFromFileName - should have 'json' or 'jsonlz4' extension"); + return null; + } + + // Check for a valid timestamp. + timestamp = parseInt(timestamp); + if (Number.isNaN(timestamp)) { + this._log.trace("_getArchivedPingDataFromFileName - should have a valid timestamp"); + return null; + } + + // Check for a valid UUID. + if (!UUID_REGEX.test(uuid)) { + this._log.trace("_getArchivedPingDataFromFileName - should have a valid id"); + return null; + } + + // Check for a valid type string. + const typeRegex = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i; + if (!typeRegex.test(type)) { + this._log.trace("_getArchivedPingDataFromFileName - should have a valid type"); + return null; + } + + return { + timestamp: timestamp, + id: uuid, + type: type, + }; + }, + + saveAbortedSessionPing: Task.async(function*(ping) { + this._log.trace("saveAbortedSessionPing - ping path: " + gAbortedSessionFilePath); + yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true }); + + return this._abortedSessionSerializer.enqueueTask(() => + this.savePingToFile(ping, gAbortedSessionFilePath, true)); + }), + + loadAbortedSessionPing: Task.async(function*() { + let ping = null; + try { + ping = yield this.loadPingFile(gAbortedSessionFilePath); + } catch (ex) { + if (ex.becauseNoSuchFile) { + this._log.trace("loadAbortedSessionPing - no such file"); + } else { + this._log.error("loadAbortedSessionPing - error loading ping", ex) + } + } + return ping; + }), + + removeAbortedSessionPing: function() { + return this._abortedSessionSerializer.enqueueTask(Task.async(function*() { + try { + yield OS.File.remove(gAbortedSessionFilePath, { ignoreAbsent: false }); + this._log.trace("removeAbortedSessionPing - success"); + } catch (ex) { + if (ex.becauseNoSuchFile) { + this._log.trace("removeAbortedSessionPing - no such file"); + } else { + this._log.error("removeAbortedSessionPing - error removing ping", ex) + } + } + }.bind(this))); + }, + + /** + * Save the deletion ping. + * @param ping The deletion ping. + * @return {Promise} Resolved when the ping is saved. + */ + saveDeletionPing: Task.async(function*(ping) { + this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath); + yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true }); + + let p = this._deletionPingSerializer.enqueueTask(() => + this.savePingToFile(ping, gDeletionPingFilePath, true)); + this._trackPendingPingSaveTask(p); + return p; + }), + + /** + * Remove the deletion ping. + * @return {Promise} Resolved when the ping is deleted from the disk. + */ + removeDeletionPing: Task.async(function*() { + return this._deletionPingSerializer.enqueueTask(Task.async(function*() { + try { + yield OS.File.remove(gDeletionPingFilePath, { ignoreAbsent: false }); + this._log.trace("removeDeletionPing - success"); + } catch (ex) { + if (ex.becauseNoSuchFile) { + this._log.trace("removeDeletionPing - no such file"); + } else { + this._log.error("removeDeletionPing - error removing ping", ex) + } + } + }.bind(this))); + }), + + isDeletionPing: function(aPingId) { + this._log.trace("isDeletionPing - id: " + aPingId); + let pingInfo = this._pendingPings.get(aPingId); + if (!pingInfo) { + return false; + } + + if (pingInfo.path != gDeletionPingFilePath) { + return false; + } + + return true; + }, + + /** + * Remove FHR database files. This is temporary and will be dropped in + * the future. + * @return {Promise} Resolved when the database files are deleted. + */ + removeFHRDatabase: Task.async(function*() { + this._log.trace("removeFHRDatabase"); + + // Let's try to remove the FHR DB with the default filename first. + const FHR_DB_DEFAULT_FILENAME = "healthreport.sqlite"; + + // Even if it's uncommon, there may be 2 additional files: - a "write ahead log" + // (-wal) file and a "shared memory file" (-shm). We need to remove them as well. + let FILES_TO_REMOVE = [ + OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME), + OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME + "-wal"), + OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME + "-shm"), + ]; + + // FHR could have used either the default DB file name or a custom one + // through this preference. + const FHR_DB_CUSTOM_FILENAME = + Preferences.get("datareporting.healthreport.dbName", undefined); + if (FHR_DB_CUSTOM_FILENAME) { + FILES_TO_REMOVE.push( + OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME), + OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME + "-wal"), + OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME + "-shm")); + } + + for (let f of FILES_TO_REMOVE) { + yield OS.File.remove(f, {ignoreAbsent: true}) + .catch(e => this._log.error("removeFHRDatabase - failed to remove " + f, e)); + } + }), +}; + +// Utility functions + +function pingFilePath(ping) { + // Support legacy ping formats, who don't have an "id" field, but a "slug" field. + let pingIdentifier = (ping.slug) ? ping.slug : ping.id; + return OS.Path.join(TelemetryStorage.pingDirectoryPath, pingIdentifier); +} + +function getPingDirectory() { + return Task.spawn(function*() { + let directory = TelemetryStorage.pingDirectoryPath; + + if (!(yield OS.File.exists(directory))) { + yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU }); + } + + return directory; + }); +} + +/** + * Build the path to the archived ping. + * @param {String} aPingId The ping id. + * @param {Object} aDate The ping creation date. + * @param {String} aType The ping type. + * @return {String} The full path to the archived ping. + */ +function getArchivedPingPath(aPingId, aDate, aType) { + // Helper to pad the month to 2 digits, if needed (e.g. "1" -> "01"). + let addLeftPadding = value => (value < 10) ? ("0" + value) : value; + // Get the ping creation date and generate the archive directory to hold it. Note + // that getMonth returns a 0-based month, so we need to add an offset. + let archivedPingDir = OS.Path.join(gPingsArchivePath, + aDate.getFullYear() + '-' + addLeftPadding(aDate.getMonth() + 1)); + // Generate the archived ping file path as YYYY-MM/.UUID.type.json + let fileName = [aDate.getTime(), aPingId, aType, "json"].join("."); + return OS.Path.join(archivedPingDir, fileName); +} + +/** + * Get the size of the ping file on the disk. + * @return {Integer} The file size, in bytes, of the ping file or 0 on errors. + */ +var getArchivedPingSize = Task.async(function*(aPingId, aDate, aType) { + const path = getArchivedPingPath(aPingId, aDate, aType); + let filePaths = [ path + "lz4", path ]; + + for (let path of filePaths) { + try { + return (yield OS.File.stat(path)).size; + } catch (e) {} + } + + // That's odd, this ping doesn't seem to exist. + return 0; +}); + +/** + * Get the size of the pending ping file on the disk. + * @return {Integer} The file size, in bytes, of the ping file or 0 on errors. + */ +var getPendingPingSize = Task.async(function*(aPingId) { + const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, aPingId) + try { + return (yield OS.File.stat(path)).size; + } catch (e) {} + + // That's odd, this ping doesn't seem to exist. + return 0; +}); + +/** + * Check if a directory name is in the "YYYY-MM" format. + * @param {String} aDirName The name of the pings archive directory. + * @return {Boolean} True if the directory name is in the right format, false otherwise. + */ +function isValidArchiveDir(aDirName) { + const dirRegEx = /^[0-9]{4}-[0-9]{2}$/; + return dirRegEx.test(aDirName); +} + +/** + * Gets a date object from an archive directory name. + * @param {String} aDirName The name of the pings archive directory. Must be in the YYYY-MM + * format. + * @return {Object} A Date object or null if the dir name is not valid. + */ +function getDateFromArchiveDir(aDirName) { + let [year, month] = aDirName.split("-"); + year = parseInt(year); + month = parseInt(month); + // Make sure to have sane numbers. + if (!Number.isFinite(month) || !Number.isFinite(year) || month < 1 || month > 12) { + return null; + } + return new Date(year, month - 1, 1, 0, 0, 0); +} diff --git a/toolkit/components/telemetry/TelemetryTimestamps.jsm b/toolkit/components/telemetry/TelemetryTimestamps.jsm new file mode 100644 index 000000000..e49d7453c --- /dev/null +++ b/toolkit/components/telemetry/TelemetryTimestamps.jsm @@ -0,0 +1,54 @@ +/* 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/. */ + +this.EXPORTED_SYMBOLS = ["TelemetryTimestamps"]; + +const Cu = Components.utils; + +/** + * This module's purpose is to collect timestamps for important + * application-specific events. + * + * The TelemetryController component attaches the timestamps stored by this module to + * the telemetry submission, substracting the process lifetime so that the times + * are relative to process startup. The overall goal is to produce a basic + * timeline of the startup process. + */ +var timeStamps = {}; + +this.TelemetryTimestamps = { + /** + * Adds a timestamp to the list. The addition of TimeStamps that already have + * a value stored is ignored. + * + * @param name must be a unique, generally "camelCase" descriptor of what the + * timestamp represents. e.g.: "delayedStartupStarted" + * @param value is a timeStamp in milliseconds since the epoch. If omitted, + * defaults to Date.now(). + */ + add: function TT_add(name, value) { + // Default to "now" if not specified + if (value == null) + value = Date.now(); + + if (isNaN(value)) + throw new Error("Value must be a timestamp"); + + // If there's an existing value, just ignore the new value. + if (timeStamps.hasOwnProperty(name)) + return; + + timeStamps[name] = value; + }, + + /** + * Returns a JS object containing all of the timeStamps as properties (can be + * easily serialized to JSON). Used by TelemetryController to retrieve the data + * to attach to the telemetry submission. + */ + get: function TT_get() { + // Return a copy of the object. + return Cu.cloneInto(timeStamps, {}); + } +}; diff --git a/toolkit/components/telemetry/TelemetryUtils.jsm b/toolkit/components/telemetry/TelemetryUtils.jsm new file mode 100644 index 000000000..4d934c9c1 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryUtils.jsm @@ -0,0 +1,152 @@ +/* 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 = [ + "TelemetryUtils" +]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Preferences.jsm", this); + +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; + +const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; + +const IS_CONTENT_PROCESS = (function() { + // We cannot use Services.appinfo here because in telemetry xpcshell tests, + // appinfo is initially unavailable, and becomes available only later on. + let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + return runtime.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; +})(); + +this.TelemetryUtils = { + /** + * True if this is a content process. + */ + get isContentProcess() { + return IS_CONTENT_PROCESS; + }, + + /** + * Returns the state of the Telemetry enabled preference, making sure + * it correctly evaluates to a boolean type. + */ + get isTelemetryEnabled() { + return Preferences.get(PREF_TELEMETRY_ENABLED, false) === true; + }, + + /** + * Turn a millisecond timestamp into a day timestamp. + * + * @param aMsec A number of milliseconds since Unix epoch. + * @return The number of whole days since Unix epoch. + */ + millisecondsToDays: function(aMsec) { + return Math.floor(aMsec / MILLISECONDS_PER_DAY); + }, + + /** + * Takes a date and returns it trunctated to a date with daily precision. + */ + truncateToDays: function(date) { + return new Date(date.getFullYear(), + date.getMonth(), + date.getDate(), + 0, 0, 0, 0); + }, + + /** + * Check if the difference between the times is within the provided tolerance. + * @param {Number} t1 A time in milliseconds. + * @param {Number} t2 A time in milliseconds. + * @param {Number} tolerance The tolerance, in milliseconds. + * @return {Boolean} True if the absolute time difference is within the tolerance, false + * otherwise. + */ + areTimesClose: function(t1, t2, tolerance) { + return Math.abs(t1 - t2) <= tolerance; + }, + + /** + * Get the next midnight for a date. + * @param {Object} date The date object to check. + * @return {Object} The Date object representing the next midnight. + */ + getNextMidnight: function(date) { + let nextMidnight = new Date(this.truncateToDays(date)); + nextMidnight.setDate(nextMidnight.getDate() + 1); + return nextMidnight; + }, + + /** + * Get the midnight which is closer to the provided date. + * @param {Object} date The date object to check. + * @param {Number} tolerance The tolerance within we find the closest midnight. + * @return {Object} The Date object representing the closes midnight, or null if midnight + * is not within the midnight tolerance. + */ + getNearestMidnight: function(date, tolerance) { + let lastMidnight = this.truncateToDays(date); + if (this.areTimesClose(date.getTime(), lastMidnight.getTime(), tolerance)) { + return lastMidnight; + } + + const nextMidnightDate = this.getNextMidnight(date); + if (this.areTimesClose(date.getTime(), nextMidnightDate.getTime(), tolerance)) { + return nextMidnightDate; + } + return null; + }, + + generateUUID: function() { + let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + // strip {} + return str.substring(1, str.length - 1); + }, + + /** + * Find how many months passed between two dates. + * @param {Object} aStartDate The starting date. + * @param {Object} aEndDate The ending date. + * @return {Integer} The number of months between the two dates. + */ + getElapsedTimeInMonths: function(aStartDate, aEndDate) { + return (aEndDate.getMonth() - aStartDate.getMonth()) + + 12 * (aEndDate.getFullYear() - aStartDate.getFullYear()); + }, + + /** + * Date.toISOString() gives us UTC times, this gives us local times in + * the ISO date format. See http://www.w3.org/TR/NOTE-datetime + * @param {Object} date The input date. + * @return {String} The local time ISO string. + */ + toLocalTimeISOString: function(date) { + function padNumber(number, places) { + number = number.toString(); + while (number.length < places) { + number = "0" + number; + } + return number; + } + + let sign = (n) => n >= 0 ? "+" : "-"; + // getTimezoneOffset counter-intuitively returns -60 for UTC+1. + let tzOffset = - date.getTimezoneOffset(); + + // YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) + return padNumber(date.getFullYear(), 4) + + "-" + padNumber(date.getMonth() + 1, 2) + + "-" + padNumber(date.getDate(), 2) + + "T" + padNumber(date.getHours(), 2) + + ":" + padNumber(date.getMinutes(), 2) + + ":" + padNumber(date.getSeconds(), 2) + + "." + date.getMilliseconds() + + sign(tzOffset) + padNumber(Math.floor(Math.abs(tzOffset / 60)), 2) + + ":" + padNumber(Math.abs(tzOffset % 60), 2); + }, +}; diff --git a/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm b/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm new file mode 100644 index 000000000..fedac1710 --- /dev/null +++ b/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm @@ -0,0 +1,181 @@ +/* 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"; + +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); + +this.EXPORTED_SYMBOLS = ["ThirdPartyCookieProbe"]; + +const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24; + +/** + * A probe implementing the measurements detailed at + * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry + * + * This implementation uses only in-memory data. + */ +this.ThirdPartyCookieProbe = function() { + /** + * A set of third-party sites that have caused cookies to be + * rejected. These sites are trimmed down to ETLD + 1 + * (i.e. "x.y.com" and "z.y.com" are both trimmed down to "y.com", + * "x.y.co.uk" is trimmed down to "y.co.uk"). + * + * Used to answer the following question: "For each third-party + * site, how many other first parties embed them and result in + * cookie traffic?" (see + * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry#Breadth + * ) + * + * @type Map A mapping from third-party site + * to rejection statistics. + */ + this._thirdPartyCookies = new Map(); + /** + * Timestamp of the latest call to flush() in milliseconds since the Epoch. + */ + this._latestFlush = Date.now(); +}; + +this.ThirdPartyCookieProbe.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + init: function() { + Services.obs.addObserver(this, "profile-before-change", false); + Services.obs.addObserver(this, "third-party-cookie-accepted", false); + Services.obs.addObserver(this, "third-party-cookie-rejected", false); + }, + dispose: function() { + Services.obs.removeObserver(this, "profile-before-change"); + Services.obs.removeObserver(this, "third-party-cookie-accepted"); + Services.obs.removeObserver(this, "third-party-cookie-rejected"); + }, + /** + * Observe either + * - "profile-before-change" (no meaningful subject or data) - time to flush statistics and unregister; or + * - "third-party-cookie-accepted"/"third-party-cookie-rejected" with + * subject: the nsIURI of the third-party that attempted to set the cookie; + * data: a string holding the uri of the page seen by the user. + */ + observe: function(docURI, topic, referrer) { + try { + if (topic == "profile-before-change") { + // A final flush, then unregister + this.flush(); + this.dispose(); + } + if (topic != "third-party-cookie-accepted" + && topic != "third-party-cookie-rejected") { + // Not a third-party cookie + return; + } + // Add host to this._thirdPartyCookies + // Note: nsCookieService passes "?" if the issuer is unknown. Avoid + // normalizing in this case since its not a valid URI. + let firstParty = (referrer === "?") ? referrer : normalizeHost(referrer); + let thirdParty = normalizeHost(docURI.QueryInterface(Ci.nsIURI).host); + let data = this._thirdPartyCookies.get(thirdParty); + if (!data) { + data = new RejectStats(); + this._thirdPartyCookies.set(thirdParty, data); + } + if (topic == "third-party-cookie-accepted") { + data.addAccepted(firstParty); + } else { + data.addRejected(firstParty); + } + } catch (ex) { + if (ex instanceof Ci.nsIXPCException) { + if (ex.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS || + ex.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { + return; + } + } + // Other errors should not remain silent. + Services.console.logStringMessage("ThirdPartyCookieProbe: Uncaught error " + ex + "\n" + ex.stack); + } + }, + + /** + * Clear internal data, fill up corresponding histograms. + * + * @param {number} aNow (optional, used for testing purposes only) + * The current instant. Used to make tests time-independent. + */ + flush: function(aNow = Date.now()) { + let updays = (aNow - this._latestFlush) / MILLISECONDS_PER_DAY; + if (updays <= 0) { + // Unlikely, but regardless, don't risk division by zero + // or weird stuff. + return; + } + this._latestFlush = aNow; + this._thirdPartyCookies.clear(); + } +}; + +/** + * Data gathered on cookies that a third party site has attempted to set. + * + * Privacy note: the only data actually sent to the server is the size of + * the sets. + * + * @constructor + */ +var RejectStats = function() { + /** + * The set of all sites for which we have accepted third-party cookies. + */ + this._acceptedSites = new Set(); + /** + * The set of all sites for which we have rejected third-party cookies. + */ + this._rejectedSites = new Set(); + /** + * Total number of attempts to set a third-party cookie that have + * been accepted. Two accepted attempts on the same site will both + * augment this count. + */ + this._acceptedRequests = 0; + /** + * Total number of attempts to set a third-party cookie that have + * been rejected. Two rejected attempts on the same site will both + * augment this count. + */ + this._rejectedRequests = 0; +}; +RejectStats.prototype = { + addAccepted: function(firstParty) { + this._acceptedSites.add(firstParty); + this._acceptedRequests++; + }, + addRejected: function(firstParty) { + this._rejectedSites.add(firstParty); + this._rejectedRequests++; + }, + get countAcceptedSites() { + return this._acceptedSites.size; + }, + get countRejectedSites() { + return this._rejectedSites.size; + }, + get countAcceptedRequests() { + return this._acceptedRequests; + }, + get countRejectedRequests() { + return this._rejectedRequests; + } +}; + +/** + * Normalize a host to its eTLD + 1. + */ +function normalizeHost(host) { + return Services.eTLD.getBaseDomainFromHost(host); +} diff --git a/toolkit/components/telemetry/ThreadHangStats.h b/toolkit/components/telemetry/ThreadHangStats.h new file mode 100644 index 000000000..60aa680c8 --- /dev/null +++ b/toolkit/components/telemetry/ThreadHangStats.h @@ -0,0 +1,230 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef mozilla_BackgroundHangTelemetry_h +#define mozilla_BackgroundHangTelemetry_h + +#include "mozilla/Array.h" +#include "mozilla/Assertions.h" +#include "mozilla/HangAnnotations.h" +#include "mozilla/Move.h" +#include "mozilla/Mutex.h" +#include "mozilla/PodOperations.h" +#include "mozilla/Vector.h" + +#include "nsString.h" +#include "prinrval.h" + +namespace mozilla { +namespace Telemetry { + +static const size_t kTimeHistogramBuckets = 8 * sizeof(PRIntervalTime); + +/* TimeHistogram is an efficient histogram that puts time durations into + exponential (base 2) buckets; times are accepted in PRIntervalTime and + stored in milliseconds. */ +class TimeHistogram : public mozilla::Array +{ +public: + TimeHistogram() + { + mozilla::PodArrayZero(*this); + } + // Get minimum (inclusive) range of bucket in milliseconds + uint32_t GetBucketMin(size_t aBucket) const { + MOZ_ASSERT(aBucket < ArrayLength(*this)); + return (1u << aBucket) & ~1u; // Bucket 0 starts at 0, not 1 + } + // Get maximum (inclusive) range of bucket in milliseconds + uint32_t GetBucketMax(size_t aBucket) const { + MOZ_ASSERT(aBucket < ArrayLength(*this)); + return (1u << (aBucket + 1u)) - 1u; + } + void Add(PRIntervalTime aTime); +}; + +/* HangStack stores an array of const char pointers, + with optional internal storage for strings. */ +class HangStack +{ +public: + static const size_t sMaxInlineStorage = 8; + +private: + typedef mozilla::Vector Impl; + Impl mImpl; + + // Stack entries can either be a static const char* + // or a pointer to within this buffer. + mozilla::Vector mBuffer; + +public: + HangStack() { } + + HangStack(HangStack&& aOther) + : mImpl(mozilla::Move(aOther.mImpl)) + , mBuffer(mozilla::Move(aOther.mBuffer)) + { + } + + bool operator==(const HangStack& aOther) const { + for (size_t i = 0; i < length(); i++) { + if (!IsSameAsEntry(operator[](i), aOther[i])) { + return false; + } + } + return true; + } + + bool operator!=(const HangStack& aOther) const { + return !operator==(aOther); + } + + const char*& operator[](size_t aIndex) { + return mImpl[aIndex]; + } + + const char* const& operator[](size_t aIndex) const { + return mImpl[aIndex]; + } + + size_t capacity() const { return mImpl.capacity(); } + size_t length() const { return mImpl.length(); } + bool empty() const { return mImpl.empty(); } + bool canAppendWithoutRealloc(size_t aNeeded) const { + return mImpl.canAppendWithoutRealloc(aNeeded); + } + void infallibleAppend(const char* aEntry) { mImpl.infallibleAppend(aEntry); } + bool reserve(size_t aRequest) { return mImpl.reserve(aRequest); } + const char** begin() { return mImpl.begin(); } + const char* const* begin() const { return mImpl.begin(); } + const char** end() { return mImpl.end(); } + const char* const* end() const { return mImpl.end(); } + const char*& back() { return mImpl.back(); } + void erase(const char** aEntry) { mImpl.erase(aEntry); } + void erase(const char** aBegin, const char** aEnd) { + mImpl.erase(aBegin, aEnd); + } + + void clear() { + mImpl.clear(); + mBuffer.clear(); + } + + bool IsInBuffer(const char* aEntry) const { + return aEntry >= mBuffer.begin() && aEntry < mBuffer.end(); + } + + bool IsSameAsEntry(const char* aEntry, const char* aOther) const { + // If the entry came from the buffer, we need to compare its content; + // otherwise we only need to compare its pointer. + return IsInBuffer(aEntry) ? !strcmp(aEntry, aOther) : (aEntry == aOther); + } + + size_t AvailableBufferSize() const { + return mBuffer.capacity() - mBuffer.length(); + } + + bool EnsureBufferCapacity(size_t aCapacity) { + // aCapacity is the minimal capacity and Vector may make the actual + // capacity larger, in which case we want to use up all the space. + return mBuffer.reserve(aCapacity) && + mBuffer.reserve(mBuffer.capacity()); + } + + const char* InfallibleAppendViaBuffer(const char* aText, size_t aLength); + const char* AppendViaBuffer(const char* aText, size_t aLength); +}; + +/* A hang histogram consists of a stack associated with the + hang, along with a time histogram of the hang times. */ +class HangHistogram : public TimeHistogram +{ +private: + static uint32_t GetHash(const HangStack& aStack); + + HangStack mStack; + // Native stack that corresponds to the pseudostack in mStack + HangStack mNativeStack; + // Use a hash to speed comparisons + const uint32_t mHash; + // Annotations attributed to this stack + HangMonitor::HangAnnotationsVector mAnnotations; + +public: + explicit HangHistogram(HangStack&& aStack) + : mStack(mozilla::Move(aStack)) + , mHash(GetHash(mStack)) + { + } + HangHistogram(HangHistogram&& aOther) + : TimeHistogram(mozilla::Move(aOther)) + , mStack(mozilla::Move(aOther.mStack)) + , mNativeStack(mozilla::Move(aOther.mNativeStack)) + , mHash(mozilla::Move(aOther.mHash)) + , mAnnotations(mozilla::Move(aOther.mAnnotations)) + { + } + bool operator==(const HangHistogram& aOther) const; + bool operator!=(const HangHistogram& aOther) const + { + return !operator==(aOther); + } + const HangStack& GetStack() const { + return mStack; + } + HangStack& GetNativeStack() { + return mNativeStack; + } + const HangStack& GetNativeStack() const { + return mNativeStack; + } + const HangMonitor::HangAnnotationsVector& GetAnnotations() const { + return mAnnotations; + } + void Add(PRIntervalTime aTime, HangMonitor::HangAnnotationsPtr aAnnotations) { + TimeHistogram::Add(aTime); + if (aAnnotations) { + if (!mAnnotations.append(Move(aAnnotations))) { + MOZ_CRASH(); + } + } + } +}; + +/* Thread hang stats consist of + - thread name + - time histogram of all task run times + - hang histograms of individual hangs + - annotations for each hang +*/ +class ThreadHangStats +{ +private: + nsCString mName; + +public: + TimeHistogram mActivity; + mozilla::Vector mHangs; + + explicit ThreadHangStats(const char* aName) + : mName(aName) + { + } + ThreadHangStats(ThreadHangStats&& aOther) + : mName(mozilla::Move(aOther.mName)) + , mActivity(mozilla::Move(aOther.mActivity)) + , mHangs(mozilla::Move(aOther.mHangs)) + { + } + const char* GetName() const { + return mName.get(); + } +}; + +} // namespace Telemetry +} // namespace mozilla + +#endif // mozilla_BackgroundHangTelemetry_h diff --git a/toolkit/components/telemetry/UITelemetry.jsm b/toolkit/components/telemetry/UITelemetry.jsm new file mode 100644 index 000000000..bd7a34b72 --- /dev/null +++ b/toolkit/components/telemetry/UITelemetry.jsm @@ -0,0 +1,235 @@ +/* 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"; + +const Cu = Components.utils; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_ENABLED = PREF_BRANCH + "enabled"; + +this.EXPORTED_SYMBOLS = [ + "UITelemetry", +]; + +Cu.import("resource://gre/modules/Services.jsm", this); + +/** + * UITelemetry is a helper JSM used to record UI specific telemetry events. + * + * It implements nsIUITelemetryObserver, defined in nsIAndroidBridge.idl. + */ +this.UITelemetry = { + _enabled: undefined, + _activeSessions: {}, + _measurements: [], + + // Lazily decide whether telemetry is enabled. + get enabled() { + if (this._enabled !== undefined) { + return this._enabled; + } + + // Set an observer to watch for changes at runtime. + Services.prefs.addObserver(PREF_ENABLED, this, false); + Services.obs.addObserver(this, "profile-before-change", false); + + // Pick up the current value. + try { + this._enabled = Services.prefs.getBoolPref(PREF_ENABLED); + } catch (e) { + this._enabled = false; + } + + return this._enabled; + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "profile-before-change") { + Services.obs.removeObserver(this, "profile-before-change"); + Services.prefs.removeObserver(PREF_ENABLED, this); + this._enabled = undefined; + return; + } + + if (aTopic == "nsPref:changed") { + switch (aData) { + case PREF_ENABLED: + let on = Services.prefs.getBoolPref(PREF_ENABLED); + this._enabled = on; + + // Wipe ourselves if we were just disabled. + if (!on) { + this._activeSessions = {}; + this._measurements = []; + } + break; + } + } + }, + + /** + * This exists exclusively for testing -- our events are not intended to + * be retrieved via an XPCOM interface. + */ + get wrappedJSObject() { + return this; + }, + + /** + * Holds the functions that provide UITelemetry's simple + * measurements. Those functions are mapped to unique names, + * and should be registered with addSimpleMeasureFunction. + */ + _simpleMeasureFunctions: {}, + + /** + * A hack to generate the relative timestamp from start when we don't have + * access to the Java timer. + * XXX: Bug 1007647 - Support realtime and/or uptime in JavaScript. + */ + uptimeMillis: function() { + return Date.now() - Services.startup.getStartupInfo().process; + }, + + /** + * Adds a single event described by a timestamp, an action, and the calling + * method. + * + * Optionally provide a string 'extras', which will be recorded as part of + * the event. + * + * All extant sessions will be recorded by name for each event. + */ + addEvent: function(aAction, aMethod, aTimestamp, aExtras) { + if (!this.enabled) { + return; + } + + let sessions = Object.keys(this._activeSessions); + let aEvent = { + type: "event", + action: aAction, + method: aMethod, + sessions: sessions, + timestamp: (aTimestamp == undefined) ? this.uptimeMillis() : aTimestamp, + }; + + if (aExtras) { + aEvent.extras = aExtras; + } + + this._recordEvent(aEvent); + }, + + /** + * Begins tracking a session by storing a timestamp for session start. + */ + startSession: function(aName, aTimestamp) { + if (!this.enabled) { + return; + } + + if (this._activeSessions[aName]) { + // Do not overwrite a previous event start if it already exists. + return; + } + this._activeSessions[aName] = (aTimestamp == undefined) ? this.uptimeMillis() : aTimestamp; + }, + + /** + * Tracks the end of a session with a timestamp. + */ + stopSession: function(aName, aReason, aTimestamp) { + if (!this.enabled) { + return; + } + + let sessionStart = this._activeSessions[aName]; + delete this._activeSessions[aName]; + + if (!sessionStart) { + return; + } + + let aEvent = { + type: "session", + name: aName, + reason: aReason, + start: sessionStart, + end: (aTimestamp == undefined) ? this.uptimeMillis() : aTimestamp, + }; + + this._recordEvent(aEvent); + }, + + _recordEvent: function(aEvent) { + this._measurements.push(aEvent); + }, + + /** + * Called by TelemetrySession to populate the simple measurement + * blob. This function will iterate over all functions added + * via addSimpleMeasureFunction and return an object with the + * results of those functions. + */ + getSimpleMeasures: function() { + if (!this.enabled) { + return {}; + } + + let result = {}; + for (let name in this._simpleMeasureFunctions) { + result[name] = this._simpleMeasureFunctions[name](); + } + return result; + }, + + /** + * Allows the caller to register functions that will get called + * for simple measures during a Telemetry ping. aName is a unique + * identifier used as they key for the simple measurement in the + * object that getSimpleMeasures returns. + * + * This function throws an exception if aName already has a function + * registered for it. + */ + addSimpleMeasureFunction: function(aName, aFunction) { + if (!this.enabled) { + return; + } + + if (aName in this._simpleMeasureFunctions) { + throw new Error("A simple measurement function is already registered for " + aName); + } + + if (!aFunction || typeof aFunction !== 'function') { + throw new Error("addSimpleMeasureFunction called with non-function argument."); + } + + this._simpleMeasureFunctions[aName] = aFunction; + }, + + removeSimpleMeasureFunction: function(aName) { + delete this._simpleMeasureFunctions[aName]; + }, + + /** + * Called by TelemetrySession to populate the UI measurement + * blob. + * + * Optionally clears the set of measurements based on aClear. + */ + getUIMeasurements: function(aClear) { + if (!this.enabled) { + return []; + } + + let measurements = this._measurements.slice(); + if (aClear) { + this._measurements = []; + } + return measurements; + } +}; diff --git a/toolkit/components/telemetry/WebrtcTelemetry.cpp b/toolkit/components/telemetry/WebrtcTelemetry.cpp new file mode 100644 index 000000000..29c22be23 --- /dev/null +++ b/toolkit/components/telemetry/WebrtcTelemetry.cpp @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + + +#include "Telemetry.h" +#include "TelemetryCommon.h" +#include "WebrtcTelemetry.h" +#include "jsapi.h" +#include "nsPrintfCString.h" +#include "nsTHashtable.h" + +using mozilla::Telemetry::Common::AutoHashtable; + +void +WebrtcTelemetry::RecordIceCandidateMask(const uint32_t iceCandidateBitmask, + const bool success) +{ + WebrtcIceCandidateType *entry = mWebrtcIceCandidates.GetEntry(iceCandidateBitmask); + if (!entry) { + entry = mWebrtcIceCandidates.PutEntry(iceCandidateBitmask); + if (MOZ_UNLIKELY(!entry)) + return; + } + + if (success) { + entry->mData.webrtc.successCount++; + } else { + entry->mData.webrtc.failureCount++; + } +} + +bool +ReflectIceEntry(const WebrtcTelemetry::WebrtcIceCandidateType *entry, + const WebrtcTelemetry::WebrtcIceCandidateStats *stat, JSContext *cx, + JS::Handle obj) +{ + if ((stat->successCount == 0) && (stat->failureCount == 0)) + return true; + + const uint32_t &bitmask = entry->GetKey(); + + JS::Rooted statsObj(cx, JS_NewPlainObject(cx)); + if (!statsObj) + return false; + if (!JS_DefineProperty(cx, obj, + nsPrintfCString("%lu", bitmask).BeginReading(), + statsObj, JSPROP_ENUMERATE)) { + return false; + } + if (stat->successCount && !JS_DefineProperty(cx, statsObj, "successCount", + stat->successCount, + JSPROP_ENUMERATE)) { + return false; + } + if (stat->failureCount && !JS_DefineProperty(cx, statsObj, "failureCount", + stat->failureCount, + JSPROP_ENUMERATE)) { + return false; + } + return true; +} + +bool +ReflectIceWebrtc(WebrtcTelemetry::WebrtcIceCandidateType *entry, JSContext *cx, + JS::Handle obj) +{ + return ReflectIceEntry(entry, &entry->mData.webrtc, cx, obj); +} + +bool +WebrtcTelemetry::AddIceInfo(JSContext *cx, JS::Handle iceObj) +{ + JS::Rooted statsObj(cx, JS_NewPlainObject(cx)); + if (!statsObj) + return false; + + if (!mWebrtcIceCandidates.ReflectIntoJS(ReflectIceWebrtc, cx, statsObj)) { + return false; + } + + return JS_DefineProperty(cx, iceObj, "webrtc", + statsObj, JSPROP_ENUMERATE); +} + +bool +WebrtcTelemetry::GetWebrtcStats(JSContext *cx, JS::MutableHandle ret) +{ + JS::Rooted root_obj(cx, JS_NewPlainObject(cx)); + if (!root_obj) + return false; + ret.setObject(*root_obj); + + JS::Rooted ice_obj(cx, JS_NewPlainObject(cx)); + if (!ice_obj) + return false; + JS_DefineProperty(cx, root_obj, "IceCandidatesStats", ice_obj, + JSPROP_ENUMERATE); + + if (!AddIceInfo(cx, ice_obj)) + return false; + + return true; +} + +size_t +WebrtcTelemetry::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + return mWebrtcIceCandidates.ShallowSizeOfExcludingThis(aMallocSizeOf); +} diff --git a/toolkit/components/telemetry/WebrtcTelemetry.h b/toolkit/components/telemetry/WebrtcTelemetry.h new file mode 100644 index 000000000..ed87c7107 --- /dev/null +++ b/toolkit/components/telemetry/WebrtcTelemetry.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef WebrtcTelemetry_h__ +#define WebrtcTelemetry_h__ + +#include "nsBaseHashtable.h" +#include "nsHashKeys.h" +#include "TelemetryCommon.h" + +class WebrtcTelemetry { +public: + struct WebrtcIceCandidateStats { + uint32_t successCount; + uint32_t failureCount; + WebrtcIceCandidateStats() : + successCount(0), + failureCount(0) + { + } + }; + struct WebrtcIceStatsCategory { + struct WebrtcIceCandidateStats webrtc; + }; + typedef nsBaseHashtableET WebrtcIceCandidateType; + + void RecordIceCandidateMask(const uint32_t iceCandidateBitmask, bool success); + + bool GetWebrtcStats(JSContext *cx, JS::MutableHandle ret); + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + +private: + + bool AddIceInfo(JSContext *cx, JS::Handle rootObj); + + mozilla::Telemetry::Common::AutoHashtable mWebrtcIceCandidates; +}; + +#endif // WebrtcTelemetry_h__ diff --git a/toolkit/components/telemetry/datareporting-prefs.js b/toolkit/components/telemetry/datareporting-prefs.js new file mode 100644 index 000000000..6a61f1853 --- /dev/null +++ b/toolkit/components/telemetry/datareporting-prefs.js @@ -0,0 +1,12 @@ +/* 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/. */ + +pref("datareporting.policy.dataSubmissionEnabled", true); +pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0"); +pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0); +pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false); +pref("datareporting.policy.currentPolicyVersion", 2); +pref("datareporting.policy.minimumPolicyVersion", 1); +pref("datareporting.policy.minimumPolicyVersion.channel-beta", 2); +pref("datareporting.policy.firstRunURL", ""); diff --git a/toolkit/components/telemetry/docs/collection/custom-pings.rst b/toolkit/components/telemetry/docs/collection/custom-pings.rst new file mode 100644 index 000000000..daad87bfe --- /dev/null +++ b/toolkit/components/telemetry/docs/collection/custom-pings.rst @@ -0,0 +1,74 @@ +======================= +Submitting custom pings +======================= + +Custom pings can be submitted from JavaScript using: + +.. code-block:: js + + TelemetryController.submitExternalPing(type, payload, options) + +- ``type`` - a ``string`` that is the type of the ping, limited to ``/^[a-z0-9][a-z0-9-]+[a-z0-9]$/i``. +- ``payload`` - the actual payload data for the ping, has to be a JSON style object. +- ``options`` - optional, an object containing additional options: + - ``addClientId``- whether to add the client id to the ping, defaults to ``false`` + - ``addEnvironment`` - whether to add the environment data to the ping, defaults to ``false`` + - ``overrideEnvironment`` - a JSON style object that overrides the environment data + +``TelemetryController`` will assemble a ping with the passed payload and the specified options. +That ping will be archived locally for use with Shield and inspection in ``about:telemetry``. +If the preferences allow upload of Telemetry pings, the ping will be uploaded at the next opportunity (this is subject to throttling, retry-on-failure, etc.). + +Submission constraints +---------------------- + +When submitting pings on shutdown, they should not be submitted after Telemetry shutdown. +Pings should be submitted at the latest within: + +- the `observer notification `_ ``"profile-before-change"`` +- the :ref:`AsyncShutdown phase ` ``sendTelemetry`` + +There are other constraints that can lead to a ping submission getting dropped: + +- invalid ping type strings +- invalid payload types: E.g. strings instead of objects. +- oversized payloads: We currently only drop pings >1MB, but targetting sizes of <=10KB is recommended. + +Tools +===== + +Helpful tools for designing new pings include: + +- `gzipServer `_ - a Python script that can run locally and receives and saves Telemetry pings. Making Firefox send to it allows inspecting outgoing pings easily. +- ``about:telemetry`` - allows inspecting submitted pings from the local archive, including all custom ones. + +Designing custom pings +====================== + +In general, creating a new custom ping means you don't benefit automatically from the existing tooling. Further work is needed to make data show up in re:dash or other analysis tools. + +In addition to the `data collection review `_, questions to guide a new pings design are: + +- Submission interval & triggers: + - What events trigger ping submission? + - What interval is the ping submitted in? + - Is there a throttling mechanism? + - What is the desired latency? (submitting "at least daily" still leads to certain latency tails) + - Are pings submitted on a clock schedule? Or based on "time since session start", "time since last ping" etc.? (I.e. will we get sharp spikes in submission volume?) +- Size and volume: + - What’s the size of the submitted payload? + - What's the full ping size including metadata in the pipeline? + - What’s the target population? + - What's the overall estimated volume? +- Dataset: + - Is it opt-out? + - Does it need to be opt-out? + - Does it need to be in a separate ping? (why can’t the data live in probes?) +- Privacy: + - Is there risk to leak PII? + - How is that risk mitigated? +- Data contents: + - Does the submitted data answer the posed product questions? + - Does the shape of the data allow to answer the questions efficiently? + - Is the data limited to whats needed to answer the questions? + - Does the data use common formats? (i.e. can we re-use tooling or analysis know-how) diff --git a/toolkit/components/telemetry/docs/collection/histograms.rst b/toolkit/components/telemetry/docs/collection/histograms.rst new file mode 100644 index 000000000..8d0233dbf --- /dev/null +++ b/toolkit/components/telemetry/docs/collection/histograms.rst @@ -0,0 +1,5 @@ +========== +Histograms +========== + +Recording into histograms is currently documented in `a MDN article `_. diff --git a/toolkit/components/telemetry/docs/collection/index.rst b/toolkit/components/telemetry/docs/collection/index.rst new file mode 100644 index 000000000..e4084e62a --- /dev/null +++ b/toolkit/components/telemetry/docs/collection/index.rst @@ -0,0 +1,35 @@ +=============== +Data collection +=============== + +There are different APIs and formats to collect data in Firefox, all suiting different use cases. + +In general, we aim to submit data in a common format where possible. This has several advantages; from common code and tooling to sharing analysis know-how. + +In cases where this isn't possible and more flexibility is needed, we can submit custom pings or consider adding different data formats to existing pings. + +*Note:* Every new data collection must go through a `data collection review `_. + +The current data collection possibilities include: + +* :doc:`scalars` allow recording of a single value (string, boolean, a number) +* :doc:`histograms` can efficiently record multiple data points +* ``environment`` data records information about the system and settings a session occurs in +* ``TelemetryLog`` allows collecting ordered event entries (note: this does not have supporting analysis tools) +* :doc:`measuring elapsed time ` +* :doc:`custom pings ` + +.. toctree:: + :maxdepth: 2 + :titlesonly: + :hidden: + :glob: + + scalars + histograms + measuring-time + custom-pings + +Browser Usage Telemetry +~~~~~~~~~~~~~~~~~~~~~~~ +For more information, see :ref:`browserusagetelemetry`. diff --git a/toolkit/components/telemetry/docs/collection/measuring-time.rst b/toolkit/components/telemetry/docs/collection/measuring-time.rst new file mode 100644 index 000000000..918c8a85a --- /dev/null +++ b/toolkit/components/telemetry/docs/collection/measuring-time.rst @@ -0,0 +1,74 @@ +====================== +Measuring elapsed time +====================== + +To make it easier to measure how long operations take, we have helpers for both JavaScript and C++. +These helpers record the elapsed time into histograms, so you have to create suitable histograms for them first. + +From JavaScript +=============== +JavaScript can measure elapsed time using `TelemetryStopwatch.jsm `_. + +``TelemetryStopwatch`` is a helper that simplifies recording elapsed time (in milliseconds) into histograms (plain or keyed). + +API: + +.. code-block:: js + + TelemetryStopwatch = { + // Start, cancel & finish recording elapsed time into a histogram. + // |aObject| is optional. If specificied, the timer is associated with this + // object, so multiple time measurements can be done concurrently. + start(histogramId, aObject); + cancel(histogramId, aObject); + finish(histogramId, aObject); + // Start, cancel & finished recording elapsed time into a keyed histogram. + // |key| specificies the key to record into. + // |aObject| is optional and used as above. + startKeyed(histogramId, key, aObject); + cancelKeyed(histogramId, key, aObject); + finishKeyed(histogramId, key, aObject); + }; + +Example: + +.. code-block:: js + + TelemetryStopwatch.start("SAMPLE_FILE_LOAD_TIME_MS"); + // ... start loading file. + if (failedToOpenFile) { + // Cancel this if the operation failed early etc. + TelemetryStopwatch.cancel("SAMPLE_FILE_LOAD_TIME_MS"); + return; + } + // ... do more work. + TelemetryStopwatch.finish("SAMPLE_FILE_LOAD_TIME_MS"); + +From C++ +======== + +API: + +.. code-block:: cpp + + // This helper class is the preferred way to record elapsed time. + template + class AutoTimer { + // Record into a plain histogram. + explicit AutoTimer(TimeStamp aStart = TimeStamp::Now()); + // Record into a keyed histogram, with key |aKey|. + explicit AutoTimer(const nsCString& aKey, + TimeStamp aStart = TimeStamp::Now()); + }; + + void AccumulateTimeDelta(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()); + +Example: + +.. code-block:: cpp + + { + Telemetry::AutoTimer telemetry; + // ... scan disk for plugins. + } + // When leaving the scope, AutoTimers destructor will record the time that passed. diff --git a/toolkit/components/telemetry/docs/collection/scalars.rst b/toolkit/components/telemetry/docs/collection/scalars.rst new file mode 100644 index 000000000..2c48601a4 --- /dev/null +++ b/toolkit/components/telemetry/docs/collection/scalars.rst @@ -0,0 +1,140 @@ +======= +Scalars +======= + +Historically we started to overload our histogram mechanism to also collect scalar data, +such as flag values, counts, labels and others. +The scalar measurement types are the suggested way to collect that kind of scalar data. +We currently only support recording of scalars from the parent process. +The serialized scalar data is submitted with the :doc:`main pings <../data/main-ping>`. + +The API +======= +Scalar probes can be managed either through the `nsITelemetry interface `_ +or the `C++ API `_. + +JS API +------ +Probes in privileged JavaScript code can use the following functions to manipulate scalars: + +.. code-block:: js + + Services.telemetry.scalarAdd(aName, aValue); + Services.telemetry.scalarSet(aName, aValue); + Services.telemetry.scalarSetMaximum(aName, aValue); + + Services.telemetry.keyedScalarAdd(aName, aKey, aValue); + Services.telemetry.keyedScalarSet(aName, aKey, aValue); + Services.telemetry.keyedScalarSetMaximum(aName, aKey, aValue); + +These functions can throw if, for example, an operation is performed on a scalar type that doesn't support it +(e.g. calling scalarSetMaximum on a scalar of the string kind). Please look at the `code documentation `_ for +additional information. + +C++ API +------- +Probes in native code can use the more convenient helper functions declared in `Telemetry.h `_: + +.. code-block:: cpp + + void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue); + void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue); + void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue); + void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue); + void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue); + + void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); + void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); + void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue); + void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue); + +The YAML definition file +======================== +Scalar probes are required to be registered, both for validation and transparency reasons, +in the `Scalars.yaml `_ +definition file. + +The probes in the definition file are represented in a fixed-depth, two-level structure: + +.. code-block:: yaml + + # The following is a group. + a.group.hierarchy: + a_probe_name: + kind: uint + ... + another_probe: + kind: string + ... + ... + group2: + probe: + kind: int + ... + +Group and probe names need to follow a few rules: + +- they cannot exceed 40 characters each; +- group names must be alpha-numeric + ``.``, with no leading/trailing digit or ``.``; +- probe names must be alpha-numeric + ``_``, with no leading/trailing digit or ``_``. + +A probe can be defined as follows: + +.. code-block:: yaml + + a.group.hierarchy: + a_scalar: + bug_numbers: + - 1276190 + description: A nice one-line description. + expires: never + kind: uint + notification_emails: + - telemetry-client-dev@mozilla.com + +Required Fields +--------------- + +- ``bug_numbers``: A list of unsigned integers representing the number of the bugs the probe was introduced in. +- ``description``: A single or multi-line string describing what data the probe collects and when it gets collected. +- ``expires``: The version number in which the scalar expires, e.g. "30"; a version number of type "N" and "N.0" is automatically converted to "N.0a1" in order to expire the scalar also in the development channels. A telemetry probe acting on an expired scalar will print a warning into the browser console. For scalars that never expire the value ``never`` can be used. +- ``kind``: A string representing the scalar type. Allowed values are ``uint``, ``string`` and ``boolean``. +- ``notification_emails``: A list of email addresses to notify with alerts of expiring probes. More importantly, these are used by the data steward to verify that the probe is still useful. + +Optional Fields +--------------- + +- ``cpp_guard``: A string that gets inserted as an ``#ifdef`` directive around the automatically generated C++ declaration. This is typically used for platform-specific scalars, e.g. ``ANDROID``. +- ``release_channel_collection``: This can be either ``opt-in`` (default) or ``opt-out``. With the former the scalar is submitted by default on pre-release channels; on the release channel only if the user opted into additional data collection. With the latter the scalar is submitted by default on release and pre-release channels, unless the user opted out. +- ``keyed``: A boolean that determines whether this is a keyed scalar. It defaults to ``False``. + +String type restrictions +------------------------ +To prevent abuses, the content of a string scalar is limited to 50 characters in length. Trying +to set a longer string will result in an error and no string being set. + +Keyed Scalars +------------- +Keyed scalars are collections of one of the available scalar types, indexed by a string key that can contain UTF8 characters and cannot be longer than 70 characters. Keyed scalars can contain up to 100 keys. This scalar type is for example useful when you want to break down certain counts by a name, like how often searches happen with which search engine. + +Keyed scalars should only be used if the set of keys are not known beforehand. If the keys are from a known set of strings, other options are preferred if suitable, like categorical histograms or splitting measurements up into separate scalars. + +The processor scripts +===================== +The scalar definition file is processed and checked for correctness at compile time. If it +conforms to the specification, the processor scripts generate two C++ headers files, included +by the Telemetry C++ core. + +gen-scalar-data.py +------------------ +This script is called by the build system to generate the ``TelemetryScalarData.h`` C++ header +file out of the scalar definitions. +This header file contains an array holding the scalar names and version strings, in addition +to an array of ``ScalarInfo`` structures representing all the scalars. + +gen-scalar-enum.py +------------------ +This script is called by the build system to generate the ``TelemetryScalarEnums.h`` C++ header +file out of the scalar definitions. +This header file contains an enum class with all the scalar identifiers used to access them +from code through the C++ API. diff --git a/toolkit/components/telemetry/docs/concepts/archiving.rst b/toolkit/components/telemetry/docs/concepts/archiving.rst new file mode 100644 index 000000000..a2c57de43 --- /dev/null +++ b/toolkit/components/telemetry/docs/concepts/archiving.rst @@ -0,0 +1,12 @@ +========= +Archiving +========= + +When archiving is enabled through the relevant pref (``toolkit.telemetry.archive.enabled``), pings submitted to ``TelemetryController`` are also stored locally in the user profile directory, in ``/datareporting/archived``. + +To allow for cheaper lookup of archived pings, storage follows a specific naming scheme for both the directory and the ping file name: `/...jsonlz4`. + +* ```` - The subdirectory name, generated from the ping creation date. +* ```` - Timestamp of the ping creation date. +* ```` - The ping identifier. +* ```` - The ping type. diff --git a/toolkit/components/telemetry/docs/concepts/crashes.rst b/toolkit/components/telemetry/docs/concepts/crashes.rst new file mode 100644 index 000000000..c9f69a23b --- /dev/null +++ b/toolkit/components/telemetry/docs/concepts/crashes.rst @@ -0,0 +1,23 @@ +======= +Crashes +======= + +There are many different kinds of crashes for Firefox, there is not a single system used to record all of them. + +Main process crashes +==================== + +If the Firefox main process dies, that should be recorded as an aborted session. We would submit a :doc:`main ping <../data/main-ping>` with the reason ``aborted-session``. +If we have a crash dump for that crash, we should also submit a :doc:`crash ping <../data/crash-ping>`. + +The ``aborted-session`` information is first written to disk 60 seconds after startup, any earlier crashes will not trigger an ``aborted-session`` ping. +Also, the ``aborted-session`` is updated at least every 5 minutes, so it may lag behind the last session state. + +Crashes during startup should be recorded in the next sessions main ping in the ``STARTUP_CRASH_DETECTED`` histogram. + +Child process crashes +===================== + +If a Firefox plugin, content or gmplugin process dies unexpectedly, this is recorded in the main pings ``SUBPROCESS_ABNORMAL_ABORT`` keyed histogram. + +If we catch a crash report for this, then additionally the ``SUBPROCESS_CRASHES_WITH_DUMP`` keyed histogram is incremented. diff --git a/toolkit/components/telemetry/docs/concepts/index.rst b/toolkit/components/telemetry/docs/concepts/index.rst new file mode 100644 index 000000000..a49466f8d --- /dev/null +++ b/toolkit/components/telemetry/docs/concepts/index.rst @@ -0,0 +1,23 @@ +======== +Concepts +======== + +There are common concepts used throughout Telemetry: + +* :doc:`pings ` - the packets we use to submit data +* :doc:`sessions & subsessions ` - how we slice a users' time in the browser +* *measurements* - how we :doc:`collect data <../collection/index>` +* *opt-in* & *opt-out* - the different sets of data we collect +* :doc:`submission ` - how we send data to the servers +* :doc:`archiving ` - retaining ping data locally +* :doc:`crashes ` - the different data crashes generate + +.. toctree:: + :maxdepth: 2 + :titlesonly: + :glob: + :hidden: + + pings + crashes + * diff --git a/toolkit/components/telemetry/docs/concepts/pings.rst b/toolkit/components/telemetry/docs/concepts/pings.rst new file mode 100644 index 000000000..db7371b32 --- /dev/null +++ b/toolkit/components/telemetry/docs/concepts/pings.rst @@ -0,0 +1,32 @@ +.. _telemetry_pings: + +===================== +Telemetry pings +===================== + +A *Telemetry ping* is the data that we send to Mozillas Telemetry servers. + +That data is stored as a JSON object client-side and contains common information to all pings and a payload specific to a certain *ping types*. + +The top-level structure is defined by the :doc:`common ping format <../data/common-ping>` format. +It contains: + +* some basic information shared between different ping types +* the :doc:`environment data <../data/environment>` (optional) +* the data specific to the *ping type*, the *payload*. + +Ping types +========== + +We send Telemetry with different ping types. The :doc:`main <../data/main-ping>` ping is the ping that contains the bulk of the Telemetry measurements for Firefox. For more specific use-cases, we send other ping types. + +Pings sent from code that ships with Firefox are listed in the :doc:`data documentation <../data/index>`. + +Important examples are: + +* :doc:`main <../data/main-ping>` - contains the information collected by Telemetry (Histograms, hang stacks, ...) +* :doc:`saved-session <../data/main-ping>` - has the same format as a main ping, but it contains the *"classic"* Telemetry payload with measurements covering the whole browser session. This is only a separate type to make storage of saved-session easier server-side. This is temporary and will be removed soon. +* :doc:`crash <../data/crash-ping>` - a ping that is captured and sent after Firefox crashes. +* ``activation`` - *planned* - sent right after installation or profile creation +* ``upgrade`` - *planned* - sent right after an upgrade +* :doc:`deletion <../data/deletion-ping>` - sent when FHR upload is disabled, requesting deletion of the data associated with this user diff --git a/toolkit/components/telemetry/docs/concepts/sessions.rst b/toolkit/components/telemetry/docs/concepts/sessions.rst new file mode 100644 index 000000000..088556978 --- /dev/null +++ b/toolkit/components/telemetry/docs/concepts/sessions.rst @@ -0,0 +1,40 @@ +======== +Sessions +======== + +A *session* is the time from when Firefox starts until it shut down. +A session can be very long-running. E.g. for Mac users that are used to always put their laptops into sleep-mode, Firefox may run for weeks. +We slice the sessions into smaller logical units called *subsessions*. + +Subsessions +=========== + +The first subsession starts when the browser starts. After that, we split the subsession for different reasons: + +* ``daily``, when crossing local midnight. This keeps latency acceptable by triggering a ping at least daily for most active users. +* ``environment-change``, when a change to the *environment* happens. This happens for important changes to the Firefox settings and when addons activate or deactivate. + +On a subsession split, a :doc:`main ping <../data/main-ping>` with that reason will be submitted. We store the reason in the pings payload, to see what triggered it. + +A session always ends with a subsession with one of two reason: + +* ``shutdown``, when the browser was cleanly shut down. To avoid delaying shutdown, we only save this ping to disk and send it at the next opportunity (typically the next browsing session). +* ``aborted-session``, when the browser crashed. While Firefox is active, we write the current ``main`` ping data to disk every 5 minutes. If the browser crashes, we find this data on disk on the next start and send it with this reason. + +.. image:: subsession_triggers.png + +Subsession data +=============== + +A subsessions data consists of: + +* general information: the date the subsession started, how long it lasted, etc. +* specific measurements: histogram & scalar data, etc. + +This has some advantages: + +* Latency - Sending a ping with all the data of a subsession immediately after it ends means we get the data from installs faster. For ``main`` pings, we aim to send a ping at least daily by starting a new subsession at local midnight. +* Correlation - By starting new subsessions when fundamental settings change (i.e. changes to the *environment*), we can correlate a subsessions data better to those settings. + + + diff --git a/toolkit/components/telemetry/docs/concepts/submission.rst b/toolkit/components/telemetry/docs/concepts/submission.rst new file mode 100644 index 000000000..165917d40 --- /dev/null +++ b/toolkit/components/telemetry/docs/concepts/submission.rst @@ -0,0 +1,34 @@ +========== +Submission +========== + +*Note:* The server-side behaviour is documented in the `HTTP Edge Server specification `_. + +Pings are submitted via a common API on ``TelemetryController``. +If a ping fails to successfully submit to the server immediately (e.g. because +of missing internet connection), Telemetry will store it on disk and retry to +send it until the maximum ping age is exceeded (14 days). + +*Note:* the :doc:`main pings <../data/main-ping>` are kept locally even after successful submission to enable the HealthReport and SelfSupport features. They will be deleted after their retention period of 180 days. + +Submission logic +================ + +Sending of pending pings starts as soon as the delayed startup is finished. They are sent in batches, newest-first, with up +to 10 persisted pings per batch plus all unpersisted pings. +The send logic then waits for each batch to complete. + +If it succeeds we trigger the next send of a ping batch. This is delayed as needed to only trigger one batch send per minute. + +If ping sending encounters an error that means retrying later, a backoff timeout behavior is +triggered, exponentially increasing the timeout for the next try from 1 minute up to a limit of 120 minutes. +Any new ping submissions and "idle-daily" events reset this behavior as a safety mechanism and trigger immediate ping sending. + +Status codes +============ + +The telemetry server team is working towards `the common services status codes `_, but for now the following logic is sufficient for Telemetry: + +* `2XX` - success, don't resubmit +* `4XX` - there was some problem with the request - the client should not try to resubmit as it would just receive the same response +* `5XX` - there was a server-side error, the client should try to resubmit later diff --git a/toolkit/components/telemetry/docs/concepts/subsession_triggers.png b/toolkit/components/telemetry/docs/concepts/subsession_triggers.png new file mode 100644 index 000000000..5717b00a9 Binary files /dev/null and b/toolkit/components/telemetry/docs/concepts/subsession_triggers.png differ diff --git a/toolkit/components/telemetry/docs/data/addons-malware-ping.rst b/toolkit/components/telemetry/docs/data/addons-malware-ping.rst new file mode 100644 index 000000000..18502d748 --- /dev/null +++ b/toolkit/components/telemetry/docs/data/addons-malware-ping.rst @@ -0,0 +1,42 @@ + +Add-ons malware ping +==================== + +This ping is generated by an add-on created by Mozilla and shipped to users on older versions of Firefox (44-46). The ping contains information about the profile that might have been altered by a third party malicious add-on. + +Structure: + +.. code-block:: js + + { + type: "malware-addon-states", + ... + clientId: , + environment: { ... }, + // Common ping data. + payload: { + // True if the blocklist was disabled at startup time. + blocklistDisabled: , + // True if the malicious add-on exists and is enabled. False if it + // exists and is disabled or null if the add-on was not found. + mainAddonActive: , + // A value of the malicious add-on block list state, or null if the + // add-on was not found. + mainAddonBlocked: , + // True if a malicious user.js file was found in the profile. + foundUserJS: , + // If a malicious secmodd.db file was found the extension ID that the // file contained.. + secmoddAddon: , . + // A list of IDs for extensions which were hidden by malicious CSS. + hiddenAddons: [ + , + ... + ], + // A mapping of installed add-on IDs with known malicious + // update URL patterns to their exact update URLs. + updateURLs: { + : , + ... + } + } + } diff --git a/toolkit/components/telemetry/docs/data/common-ping.rst b/toolkit/components/telemetry/docs/data/common-ping.rst new file mode 100644 index 000000000..445557efd --- /dev/null +++ b/toolkit/components/telemetry/docs/data/common-ping.rst @@ -0,0 +1,42 @@ + +Common ping format +================== + +This defines the top-level structure of a Telemetry ping. +It contains basic information shared between different ping types, which enables proper storage and processing of the raw pings server-side. + +It also contains optional further information: + +* the :doc:`environment data <../data/environment>`, which contains important info to correlate the measurements against +* the ``clientId``, a UUID identifying a profile and allowing user-oriented correlation of data + +*Note:* Both are not submitted with all ping types due to privacy concerns. This and the data it that can be correlated against is inspected under the `data collection policy `_. + +Finally, the structure also contains the `payload`, which is the specific data submitted for the respective *ping type*. + +Structure: + +.. code-block:: js + + { + type: , // "main", "activation", "deletion", "saved-session", ... + id: , // a UUID that identifies this ping + creationDate: , // the date the ping was generated + version: , // the version of the ping format, currently 4 + + application: { + architecture: , // build architecture, e.g. x86 + buildId: , // "20141126041045" + name: , // "Firefox" + version: , // "35.0" + displayVersion: , // "35.0b3" + vendor: , // "Mozilla" + platformVersion: , // "35.0" + xpcomAbi: , // e.g. "x86-msvc" + channel: , // "beta" + }, + + clientId: , // optional + environment: { ... }, // optional, not all pings contain the environment + payload: { ... }, // the actual payload data for this ping type + } diff --git a/toolkit/components/telemetry/docs/data/core-ping.rst b/toolkit/components/telemetry/docs/data/core-ping.rst new file mode 100644 index 000000000..7f38f2f7e --- /dev/null +++ b/toolkit/components/telemetry/docs/data/core-ping.rst @@ -0,0 +1,191 @@ + +"core" ping +============ + +This mobile-specific ping is intended to provide the most critical +data in a concise format, allowing for frequent uploads. + +Since this ping is used to measure retention, it should be sent +each time the browser is opened. + +Submission will be per the Edge server specification:: + + /submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID + +* ``docId`` is a UUID for deduping +* ``docType`` is “core” +* ``appName`` is “Fennec” +* ``appVersion`` is the version of the application (e.g. "46.0a1") +* ``appUpdateChannel`` is “release”, “beta”, etc. +* ``appBuildID`` is the build number + +Note: Counts below (e.g. search & usage times) are “since the last +ping”, not total for the whole application lifetime. + +Structure: + +.. code-block:: js + + { + "v": 7, // ping format version + "clientId": , // client id, e.g. + // "c641eacf-c30c-4171-b403-f077724e848a" + "seq": , // running ping counter, e.g. 3 + "locale": , // application locale, e.g. "en-US" + "os": , // OS name. + "osversion": , // OS version. + "device": , // Build.MANUFACTURER + " - " + Build.MODEL + // where manufacturer is truncated to 12 characters + // & model is truncated to 19 characters + "arch": , // e.g. "arm", "x86" + "profileDate": , // Profile creation date in days since + // UNIX epoch. + "defaultSearch": , // Identifier of the default search engine, + // e.g. "yahoo". + "distributionId": , // Distribution identifier (optional) + "created": , // date the ping was created + // in local time, "yyyy-mm-dd" + "tz": , // timezone offset (in minutes) of the + // device when the ping was created + "sessions": , // number of sessions since last upload + "durations": , // combined duration, in seconds, of all + // sessions since last upload + "searches": , // Optional, object of search use counts in the + // format: { "engine.source": } + // e.g.: { "yahoo.suggestion": 3, "other.listitem": 1 } + "experiments": [, /* … */], // Optional, array of identifiers + // for the active experiments + } + +Field details +------------- + +device +~~~~~~ +The ``device`` field is filled in with information specified by the hardware +manufacturer. As such, it could be excessively long and use excessive amounts +of limited user data. To avoid this, we limit the length of the field. We're +more likely have collisions for models within a manufacturer (e.g. "Galaxy S5" +vs. "Galaxy Note") than we are for shortened manufacturer names so we provide +more characters for the model than the manufacturer. + +distributionId +~~~~~~~~~~~~~~ +The ``distributionId`` contains the distribution ID as specified by +preferences.json for a given distribution. More information on distributions +can be found `here `_. + +It is optional. + +defaultSearch +~~~~~~~~~~~~~ +On Android, this field may be ``null``. To get the engine, we rely on +``SearchEngineManager#getDefaultEngine``, which searches in several places in +order to find the search engine identifier: + +* Shared Preferences +* The distribution (if it exists) +* The localized default engine + +If the identifier could not be retrieved, this field is ``null``. If the +identifier is retrieved, we attempt to create an instance of the search +engine from the search plugins (in order): + +* In the distribution +* From the localized plugins shipped with the browser +* The third-party plugins that are installed in the profile directory + +If the plugins fail to create a search engine instance, this field is also +``null``. + +This field can also be ``null`` when a custom search engine is set as the +default. + +sessions & durations +~~~~~~~~~~~~~~~~~~~~ +On Android, a session is the time when Firefox is focused in the foreground. +`sessions` tracks the number of sessions since the last upload and +`durations` is the accumulated duration in seconds of all of these +sessions. Note that showing a dialog (including a Firefox dialog) will +take Firefox out of focus & end the current session. + +An implementation that records a session when Firefox is completely hidden is +preferrable (e.g. to avoid the dialog issue above), however, it's more complex +to implement and so we chose not to, at least for the initial implementation. + +profileDate +~~~~~~~~~~~ +On Android, this value is created at profile creation time and retrieved or, +for legacy profiles, taken from the package install time (note: this is not the +same exact metric as profile creation time but we compromised in favor of ease +of implementation). + +Additionally on Android, this field may be ``null`` in the unlikely event that +all of the following events occur: + +#. The times.json file does not exist +#. The package install date could not be persisted to disk + +The reason we don't just return the package install time even if the date could +not be persisted to disk is to ensure the value doesn't change once we start +sending it: we only want to send consistent values. + +searches +~~~~~~~~ +In the case a search engine is added by a user, the engine identifier "other" is used, e.g. "other.". + +Sources in Android are based on the existing UI telemetry values and are as +follows: + +* actionbar: the user types in the url bar and hits enter to use the default + search engine +* listitem: the user selects a search engine from the list of secondary search + engines at the bottom of the screen +* suggestion: the user clicks on a search suggestion or, in the case that + suggestions are disabled, the row corresponding with the main engine + +Other parameters +---------------- + +HTTP "Date" header +~~~~~~~~~~~~~~~~~~ +This header is used to track the submission date of the core ping in the format +specified by +`rfc 2616 sec 14.18 `_, +et al (e.g. "Tue, 01 Feb 2011 14:00:00 GMT"). + + +Version history +--------------- +* v7: added ``sessionCount`` & ``sessionDuration`` +* v6: added ``searches`` +* v5: added ``created`` & ``tz`` +* v4: ``profileDate`` will return package install time when times.json is not available +* v3: added ``defaultSearch`` +* v2: added ``distributionId`` +* v1: initial version + +Notes +~~~~~ + +* ``distributionId`` (v2) actually landed after ``profileDate`` (v4) but was + uplifted to 46, whereas ``profileDate`` landed on 47. The version numbers in + code were updated to be increasing (bug 1264492) and the version history docs + rearranged accordingly. + +Android implementation notes +---------------------------- +On Android, the uploader has a high probability of delivering the complete data +for a given client but not a 100% probability. This was a conscious decision to +keep the code simple. The cases where we can lose data: + +* Resetting the field measurements (including incrementing the sequence number) + and storing a ping for upload are not atomic. Android can kill our process + for memory pressure in between these distinct operations so we can just lose + a ping's worth of data. That sequence number will be missing on the server. +* If we exceed some number of pings on disk that have not yet been uploaded, + we remove old pings to save storage space. For those pings, we will lose + their data and their sequence numbers will be missing on the server. + +Note: we never expect to drop data without also dropping a sequence number so +we are able to determine when data loss occurs. diff --git a/toolkit/components/telemetry/docs/data/crash-ping.rst b/toolkit/components/telemetry/docs/data/crash-ping.rst new file mode 100644 index 000000000..3cdbc6030 --- /dev/null +++ b/toolkit/components/telemetry/docs/data/crash-ping.rst @@ -0,0 +1,144 @@ + +"crash" ping +============ + +This ping is captured after the main Firefox process crashes, whether or not the crash report is submitted to crash-stats.mozilla.org. It includes non-identifying metadata about the crash. + +The environment block that is sent with this ping varies: if Firefox was running long enough to record the environment block before the crash, then the environment at the time of the crash will be recorded and ``hasCrashEnvironment`` will be true. If Firefox crashed before the environment was recorded, ``hasCrashEnvironment`` will be false and the recorded environment will be the environment at time of submission. + +The client ID is submitted with this ping. + +Structure: + +.. code-block:: js + + { + version: 1, + type: "crash", + ... common ping data + clientId: , + environment: { ... }, + payload: { + crashDate: "YYYY-MM-DD", + sessionId: , // may be missing for crashes that happen early + // in startup. Added in Firefox 48 with the + // intention of uplifting to Firefox 46 + crashId: , // Optional, ID of the associated crash + stackTraces: { ... }, // Optional, see below + metadata: { // Annotations saved while Firefox was running. See nsExceptionHandler.cpp for more information + ProductName: "Firefox", + ReleaseChannel: , + Version: , + BuildID: "YYYYMMDDHHMMSS", + AvailablePageFile: , // Windows-only, available paging file + AvailablePhysicalMemory: , // Windows-only, available physical memory + AvailableVirtualMemory: , // Windows-only, available virtual memory + BlockedDllList: , // Windows-only, see WindowsDllBlocklist.cpp for details + BlocklistInitFailed: 1, // Windows-only, present only if the DLL blocklist initialization failed + CrashTime: