/* -*- 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"); 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; })(); // When modifying the payload in incompatible ways, please bump this version number const PAYLOAD_VERSION = 1; // This is the HG changeset of the Histogram.json file, used to associate // submitted ping data with its histogram definition (bug 832007) #expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__"; const LOGGER_NAME = "Toolkit.Telemetry"; const LOGGER_PREFIX = "TelemetrySession::"; const PREF_BRANCH = "toolkit.telemetry."; const PREF_SERVER = PREF_BRANCH + "server"; const PREF_ENABLED = PREF_BRANCH + "enabled"; const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID"; const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID" const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit"; const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload"; // Do not gather data more than once a minute const TELEMETRY_INTERVAL = 60000; // Delay before intializing telemetry (ms) const TELEMETRY_DELAY = 60000; // Delay before initializing telemetry if we're testing (ms) const TELEMETRY_TEST_DELAY = 100; // 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. On the next idle the data is sent. const IDLE_TIMEOUT_SECONDS = 5 * 60; var gLastMemoryPoll = null; let gWasDebuggerAttached = false; function getLocale() { return Cc["@mozilla.org/chrome/chrome-registry;1"]. getService(Ci.nsIXULChromeRegistry). getSelectedLocale('global'); } 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, "ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageListenerManager"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", "resource://gre/modules/AsyncShutdown.jsm"); #ifndef MOZ_WIDGET_GONK XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"); #endif XPCOMUtils.defineLazyModuleGetter(this, "TelemetryPing", "resource://gre/modules/TelemetryPing.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFile", "resource://gre/modules/TelemetryFile.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, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm"); function generateUUID() { let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); // strip {} return str.substring(1, str.length - 1); } /** * Read current process I/O counters. */ let 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)]; } }; this.EXPORTED_SYMBOLS = ["TelemetrySession"]; this.TelemetrySession = Object.freeze({ Constants: Object.freeze({ PREF_ENABLED: PREF_ENABLED, PREF_SERVER: PREF_SERVER, 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. * @returns Object */ getPayload: function() { return Impl.getPayload(); }, /** * Save histograms to a file. * Used only for testing purposes. * * @param {nsIFile} aFile The file to load from. */ testSaveHistograms: function(aFile) { return Impl.testSaveHistograms(aFile); }, /** * 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); }, /** * Load histograms from a file. * Used only for testing purposes. * * @param aFile - File to load from. */ testLoadHistograms: function(aFile) { return Impl.testLoadHistograms(aFile); }, /** * 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. */ reset: function() { this.uninstall(); return this.setup(); }, /** * Used only for testing purposes. */ shutdown: function() { return Impl.shutdown(true); }, /** * Used only for testing purposes. */ setup: function() { return Impl.setupChromeProcess(true); }, /** * Used only for testing purposes. */ uninstall: function() { try { Impl.uninstall(); } catch (ex) { // Ignore errors } }, /** * Send a notification. */ observe: function (aSubject, aTopic, aData) { return Impl.observe(aSubject, aTopic, aData); }, /** * The client id send with the telemetry ping. * * @return The client id as string, or null. */ get clientID() { return Impl.clientID; }, }); let Impl = { _histograms: {}, _initialized: false, _log: null, _prevValues: {}, // Generate a unique id once per session so the server can cope with // duplicate submissions. _uuid: generateUUID(), // Regex that matches histograms we care about during startup. // Keep this in sync with gen-histogram-bucket-ranges.py. _startupHistogramRegex: /SQLITE|HTTP|SPDY|CACHE|DNS/, _slowSQLStartup: {}, _prevSession: null, _hasWindowRestoredObserver: false, _hasXulWindowVisibleObserver: false, _startupIO : {}, // 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, // 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: [], /** * Gets a series of simple measurements (counters). At the moment, this * only returns startup data from nsIAppStartup.getStartupInfo(). * * @return simple measurements as a dictionary. */ getSimpleMeasurements: function getSimpleMeasurements(forSavedSession) { this._log.trace("getSimpleMeasurements"); let si = Services.startup.getStartupInfo(); // Measurements common to chrome and content processes. var ret = { // uptime in minutes uptime: Math.round((new Date() - si.process) / 60000) } // Look for app-specific timestamps var appTimestamps = {}; try { let o = {}; Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", o); appTimestamps = o.TelemetryTimestamps.get(); } catch (ex) {} try { if (!IS_CONTENT_PROCESS) { ret.addonManager = AddonManagerPrivate.getSimpleMeasures(); } } catch (ex) {} try { if (!IS_CONTENT_PROCESS) { ret.UITelemetry = UITelemetry.getSimpleMeasures(); } } catch (ex) {} if (si.process) { for each (let field in 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 (IS_CONTENT_PROCESS) { 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]; let hasPingBeenSent = false; try { hasPingBeenSent = Telemetry.getHistogramById("TELEMETRY_SUCCESS").snapshot().sum > 0; } catch(e) { } if (!forSavedSession || hasPingBeenSent) { ret.savedPings = TelemetryFile.pingsLoaded; } ret.activeTicks = -1; if ("@mozilla.org/datareporting/service;1" in Cc) { let drs = Cc["@mozilla.org/datareporting/service;1"] .getService(Ci.nsISupports) .wrappedJSObject; let sr = drs.getSessionRecorder(); if (sr) { ret.activeTicks = sr.activeTicks; } } ret.pingsOverdue = TelemetryFile.pingsOverdue; ret.pingsDiscarded = TelemetryFile.pingsDiscarded; 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; * - log_sum, log_sum_squares: doubles; * - 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: , * sum_squares_lo: , * sum_squares_hi: , * log_sum: , log_sum_squares: , * 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 }; if (hgram.histogram_type == Telemetry.HISTOGRAM_EXPONENTIAL) { retgram.log_sum = hgram.log_sum; retgram.log_sum_squares = hgram.log_sum_squares; } else { retgram.sum_squares_lo = hgram.sum_squares_lo; retgram.sum_squares_hi = hgram.sum_squares_hi; } 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; }, getHistograms: function getHistograms(hls) { this._log.trace("getHistograms"); let registered = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []); let ret = {}; for (let name of registered) { for (let n of [name, "STARTUP_" + name]) { if (n in hls) { ret[n] = this.packHistogram(hls[n]); } } } 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() { this._log.trace("getKeyedHistograms"); let registered = Telemetry.registeredKeyedHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []); let ret = {}; for (let id of registered) { ret[id] = {}; let keyed = Telemetry.getKeyedHistogramById(id); let snapshot = keyed.snapshot(); for (let key of Object.keys(snapshot)) { ret[id][key] = this.packHistogram(snapshot[key]); } } return ret; }, /** * 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); let ai = Services.appinfo; let ret = { reason: reason, OS: ai.OS, appID: ai.ID, appVersion: ai.version, appName: ai.name, appBuildID: ai.appBuildID, appUpdateChannel: UpdateChannel.get(), platformBuildID: ai.platformBuildID, revision: HISTOGRAMS_FILE_VERSION, locale: getLocale(), asyncPluginInit: Preferences.get(PREF_ASYNC_PLUGIN_INIT, false) }; // In order to share profile data, the appName used for Metro Firefox is "Firefox", // (the same as desktop Firefox). We set it to "MetroFirefox" here in order to // differentiate telemetry pings sent by desktop vs. metro Firefox. if(Services.metro && Services.metro.immersive) { ret.appName = "MetroFirefox"; } if (this._previousBuildID) { ret.previousBuildID = this._previousBuildID; } // sysinfo fields are not always available, get what we can. let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); let fields = ["cpucount", "memsize", "arch", "version", "kernel_version", "device", "manufacturer", "hardware", "tablet", "hasMMX", "hasSSE", "hasSSE2", "hasSSE3", "hasSSSE3", "hasSSE4A", "hasSSE4_1", "hasSSE4_2", "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON", "isWow64", "profileHDDModel", "profileHDDRevision", "binHDDModel", "binHDDRevision", "winHDDModel", "winHDDRevision"]; for each (let field in fields) { let value; try { value = sysInfo.getProperty(field); } catch (e) { continue; } if (field == "memsize") { // Send RAM size in megabytes. Rounding because sysinfo doesn't // always provide RAM in multiples of 1024. value = Math.round(value / 1024 / 1024); } ret[field] = value; } // gfxInfo fields are not always available, get what we can. let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); let gfxfields = ["adapterDescription", "adapterVendorID", "adapterDeviceID", "adapterSubsysID", "adapterRAM", "adapterDriver", "adapterDriverVersion", "adapterDriverDate", "adapterDescription2", "adapterVendorID2", "adapterDeviceID2", "adapterSubsysID2", "adapterRAM2", "adapterDriver2", "adapterDriverVersion2", "adapterDriverDate2", "isGPU2Active", "D2DEnabled", "DWriteEnabled", "DWriteVersion" ]; if (gfxInfo) { for each (let field in gfxfields) { try { let value = gfxInfo[field]; // bug 940806: We need to do a strict equality comparison here, // otherwise a type conversion will occur and boolean false values // will get filtered out if (value !== "") { ret[field] = value; } } catch (e) { continue } } } #ifndef MOZ_WIDGET_GONK let theme = LightweightThemeManager.currentTheme; if (theme) { ret.persona = theme.id; } #endif if (this._addons) ret.addons = this._addons; let flashVersion = this.getFlashVersion(); if (flashVersion) ret.flashVersion = flashVersion; try { let scope = {}; Cu.import("resource:///modules/experiments/Experiments.jsm", scope); let experiments = scope.Experiments.instance() let activeExperiment = experiments.getActiveExperimentID(); if (activeExperiment) { ret.activeExperiment = activeExperiment; ret.activeExperimentBranch = experiments.getActiveExperimentBranch(); } } catch(e) { // If this is not Firefox, the import will fail. } return ret; }, /** * Pull values from about:memory into corresponding histograms */ gatherMemory: function gatherMemory() { 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_TelemetryPing.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_TelemetryPing.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", "residentFast"); b("MEMORY_HEAP_ALLOCATED", "heapAllocated"); p("MEMORY_HEAP_COMMITTED_UNUSED_RATIO", "heapOverheadRatio"); b("MEMORY_JS_GC_HEAP", "JSMainRuntimeGCHeap"); b("MEMORY_JS_MAIN_RUNTIME_TEMPORARY_PEAK", "JSMainRuntimeTemporaryPeak"); 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("MEMORY_EVENTS_VIRTUAL", "lowMemoryEventsVirtual"); cc("MEMORY_EVENTS_PHYSICAL", "lowMemoryEventsPhysical"); c("GHOST_WINDOWS", "ghostWindows"); cc("PAGE_FAULTS_HARD", "pageFaultsHard"); 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); }, /** * Return true if we're interested in having a STARTUP_* histogram for * the given histogram name. */ isInterestingStartupHistogram: function isInterestingStartupHistogram(name) { return this._startupHistogramRegex.test(name); }, getChildPayloads: function getChildPayloads() { return [for (child of this._childTelemetry) child.payload]; }, /** * Make a copy of interesting histograms at startup. */ gatherStartupHistograms: function gatherStartupHistograms() { this._log.trace("gatherStartupHistograms"); let info = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []); let snapshots = Telemetry.histogramSnapshots; for (let name of info) { // Only duplicate histograms with actual data. if (this.isInterestingStartupHistogram(name) && name in snapshots) { Telemetry.histogramFrom("STARTUP_" + name, name); } } }, /** * 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 assemblePayloadWithMeasurements(simpleMeasurements, info) { this._log.trace("assemblePayloadWithMeasurements"); // Payload common to chrome and content processes. let payloadObj = { ver: PAYLOAD_VERSION, simpleMeasurements: simpleMeasurements, histograms: this.getHistograms(Telemetry.histogramSnapshots), keyedHistograms: this.getKeyedHistograms(), chromeHangs: Telemetry.chromeHangs, log: TelemetryLog.entries(), }; if (IS_CONTENT_PROCESS) { return payloadObj; } // Additional payload for chrome process. payloadObj.info = info; payloadObj.slowSQL = Telemetry.slowSQL; payloadObj.fileIOReports = Telemetry.fileIOReports; payloadObj.lateWrites = Telemetry.lateWrites; payloadObj.addonHistograms = this.getAddonHistograms(); payloadObj.addonDetails = AddonManagerPrivate.getTelemetryDetails(); payloadObj.UIMeasurements = UITelemetry.getUIMeasurements(); if (Object.keys(this._slowSQLStartup).length != 0 && (Object.keys(this._slowSQLStartup.mainThread).length || Object.keys(this._slowSQLStartup.otherThreads).length)) { payloadObj.slowSQLStartup = this._slowSQLStartup; } let clientID = TelemetryPing.clientID; if (clientID && Preferences.get(PREF_FHR_UPLOAD_ENABLED, false)) { payloadObj.clientID = clientID; } if (this._childTelemetry.length) { payloadObj.childPayloads = this.getChildPayloads(); } return payloadObj; }, getSessionPayload: function getSessionPayload(reason) { this._log.trace("getSessionPayload - Reason " + reason); let measurements = this.getSimpleMeasurements(reason == "saved-session"); let info = !IS_CONTENT_PROCESS ? this.getMetadata(reason) : null; return this.assemblePayloadWithMeasurements(measurements, info); }, assemblePing: function assemblePing(payloadObj, reason) { let slug = this._uuid; return { slug: slug, reason: reason, payload: payloadObj }; }, getSessionPayloadAndSlug: function getSessionPayloadAndSlug(reason) { this._log.trace("getSessionPayloadAndSlug - Reason " + reason); return this.assemblePing(this.getSessionPayload(reason), reason); }, /** * 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(); return TelemetryPing.send(reason, this.getSessionPayloadAndSlug(reason)); }, submissionPath: function submissionPath(ping) { let slug; if (!ping) { slug = this._uuid; } else { let info = ping.payload.info; let pathComponents = [ping.slug, info.reason, info.appName, info.appVersion, info.appUpdateChannel, info.appBuildID]; slug = pathComponents.join("/"); } return "/submit/telemetry/" + slug; }, attachObservers: function attachObservers() { if (!this._initialized) return; Services.obs.addObserver(this, "cycle-collector-begin", false); Services.obs.addObserver(this, "idle-daily", false); }, detachObservers: function detachObservers() { if (!this._initialized) return; Services.obs.removeObserver(this, "idle-daily"); Services.obs.removeObserver(this, "cycle-collector-begin"); if (this._isIdleObserver) { idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); this._isIdleObserver = false; } }, /** * Perform telemetry initialization for either chrome or content process. */ enableTelemetryRecording: function enableTelemetryRecording(testing) { #ifdef MOZILLA_OFFICIAL if (!Telemetry.canSend && !testing) { // We can't send data; no point in initializing observers etc. // Only do this for official builds so that e.g. developer builds // still enable Telemetry based on prefs. Telemetry.canRecord = false; this._log.config("enableTelemetryRecording - Can't send data, disabling Telemetry recording."); return false; } #endif let enabled = Preferences.get(PREF_ENABLED, false); this._server = Preferences.get(PREF_SERVER, undefined); if (!enabled) { // Turn off local telemetry if telemetry is disabled. // This may change once about:telemetry is added. Telemetry.canRecord = false; this._log.config("enableTelemetryRecording - Telemetry is disabled, turning off Telemetry recording."); return false; } return true; }, /** * Initializes telemetry within a timer. If there is no PREF_SERVER set, don't turn on telemetry. */ setupChromeProcess: function setupChromeProcess(testing) { if (testing && !this._log) { this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); } this._log.trace("setupChromeProcess"); // 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, undefined); let thisBuildID = Services.appinfo.appBuildID; // If there is no previousBuildID preference, this._previousBuildID remains // undefined so no value is sent in the telemetry metadata. if (previousBuildID != thisBuildID) { this._previousBuildID = previousBuildID; Preferences.set(PREF_PREVIOUS_BUILDID, thisBuildID); } if (!this.enableTelemetryRecording(testing)) { this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup."); return Promise.resolve(); } AsyncShutdown.sendTelemetry.addBlocker( "Telemetry: shutting down", function condition(){ this.uninstall(); if (Telemetry.canSend) { return this.savePendingPings(); } }.bind(this)); Services.obs.addObserver(this, "sessionstore-windows-restored", false); #ifdef MOZ_WIDGET_ANDROID Services.obs.addObserver(this, "application-background", false); #endif Services.obs.addObserver(this, "xul-window-visible", false); this._hasWindowRestoredObserver = true; this._hasXulWindowVisibleObserver = true; ppmm.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this); // 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. let deferred = Promise.defer(); let delayedTask = new DeferredTask(function* () { this._initialized = true; this.attachObservers(); this.gatherMemory(); Telemetry.asyncFetchTelemetryData(function () {}); deferred.resolve(); }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY); delayedTask.arm(); return deferred.promise; }, /** * Initializes telemetry for a content process. */ setupContentProcess: function setupContentProcess() { this._log.trace("setupContentProcess"); if (!this.enableTelemetryRecording()) { return; } Services.obs.addObserver(this, "content-child-shutdown", false); this.gatherStartupHistograms(); let delayedTask = new DeferredTask(function* () { this._initialized = true; this.attachObservers(); this.gatherMemory(); }.bind(this), TELEMETRY_DELAY); delayedTask.arm(); }, testLoadHistograms: function testLoadHistograms(file) { return TelemetryFile.testLoadHistograms(file); }, 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: { let target = message.target; for (let child of this._childTelemetry) { if (child.source.get() === target) { // Update existing telemetry data. child.payload = message.data; return; } } // Did not find existing child in this._childTelemetry. this._childTelemetry.push({ source: Cu.getWeakReference(target), payload: message.data, }); break; } default: throw new Error("Telemetry.receiveMessage: bad message name"); } }, sendContentProcessPing: function sendContentProcessPing(reason) { this._log.trace("sendContentProcessPing - Reason " + reason); let payload = this.getSessionPayload(reason); cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_PAYLOAD, payload); }, savePendingPings: function savePendingPings() { this._log.trace("savePendingPings"); let sessionPing = this.getSessionPayloadAndSlug("saved-session"); return TelemetryFile.savePendingPings(sessionPing); }, testSaveHistograms: function testSaveHistograms(file) { return TelemetryFile.savePingToFile(this.getSessionPayloadAndSlug("saved-session"), file.path, true); }, /** * Remove observers to avoid leaks */ 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; } #ifdef MOZ_WIDGET_ANDROID Services.obs.removeObserver(this, "application-background", false); #endif }, getPayload: function getPayload() { this._log.trace("getPayload"); // This function returns the current Telemetry payload to the caller. // We only gather startup info once. if (Object.keys(this._slowSQLStartup).length == 0) { this.gatherStartupHistograms(); this._slowSQLStartup = Telemetry.slowSQL; } this.gatherMemory(); return this.getSessionPayload("gather-payload"); }, gatherStartup: function gatherStartup() { this._log.trace("gatherStartup"); let counters = processInfo.getCounters(); if (counters) { [this._startupIO.startupSessionRestoreReadBytes, this._startupIO.startupSessionRestoreWriteBytes] = counters; } this.gatherStartupHistograms(); this._slowSQLStartup = Telemetry.slowSQL; }, setAddOns: function setAddOns(aAddOns) { this._addons = aAddOns; }, sendIdlePing: function sendIdlePing(aTest) { this._log.trace("sendIdlePing"); if (this._isIdleObserver) { idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); this._isIdleObserver = false; } if (aTest) { return this.send("test-ping"); } else if (Telemetry.canSend) { return this.send("idle-daily"); } }, testPing: function testPing() { return this.sendIdlePing(true); }, /** * This observer drives telemetry. */ observe: function (aSubject, aTopic, aData) { if (!this._log) { this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); } this._log.trace("observe - " + aTopic + " notified."); switch (aTopic) { case "profile-after-change": // profile-after-change is only registered for chrome processes. return this.setupChromeProcess(); case "app-startup": // app-startup is only registered for content processes. return this.setupContentProcess(); case "content-child-shutdown": // content-child-shutdown is only registered for content processes. Services.obs.removeObserver(this, "content-child-shutdown"); this.uninstall(); if (Telemetry.canSend) { this.sendContentProcessPing("saved-session"); } break; case "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, since ping will happen soon. Services.obs.notifyObservers(null, "gather-telemetry", null); // The ping happens at the first idle of length IDLE_TIMEOUT_SECONDS. idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); this._isIdleObserver = true; }).bind(this), Ci.nsIThread.DISPATCH_NORMAL); break; case "idle": this.sendIdlePing(false, this._server); break; #ifdef MOZ_WIDGET_ANDROID // 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. case "application-background": if (Telemetry.canSend) { let ping = this.getSessionPayloadAndSlug("saved-session"); TelemetryFile.savePing(ping, true); } break; #endif } }, /** * This tells TelemetryPing to uninitialize and save any pending pings. * @param testing Optional. If true, always saves the ping whether Telemetry * can send pings or not, which is used for testing. */ shutdown: function(testing = false) { this.uninstall(); if (Telemetry.canSend || testing) { return this.savePendingPings(); } }, };