michael@0: /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/debug.js", this); michael@0: Cu.import("resource://gre/modules/Services.jsm", this); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: #ifndef MOZ_WIDGET_GONK michael@0: Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this); michael@0: #endif michael@0: Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm", this); michael@0: Cu.import("resource://gre/modules/Promise.jsm", this); michael@0: Cu.import("resource://gre/modules/Task.jsm", this); michael@0: Cu.import("resource://gre/modules/AsyncShutdown.jsm", this); michael@0: michael@0: // When modifying the payload in incompatible ways, please bump this version number michael@0: const PAYLOAD_VERSION = 1; michael@0: michael@0: // This is the HG changeset of the Histogram.json file, used to associate michael@0: // submitted ping data with its histogram definition (bug 832007) michael@0: #expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__"; michael@0: michael@0: const PREF_BRANCH = "toolkit.telemetry."; michael@0: const PREF_SERVER = PREF_BRANCH + "server"; michael@0: const PREF_ENABLED = PREF_BRANCH + "enabled"; michael@0: const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID"; michael@0: michael@0: // Do not gather data more than once a minute michael@0: const TELEMETRY_INTERVAL = 60000; michael@0: // Delay before intializing telemetry (ms) michael@0: const TELEMETRY_DELAY = 60000; michael@0: // Delay before initializing telemetry if we're testing (ms) michael@0: const TELEMETRY_TEST_DELAY = 100; michael@0: michael@0: // Seconds of idle time before pinging. michael@0: // On idle-daily a gather-telemetry notification is fired, during it probes can michael@0: // start asynchronous tasks to gather data. On the next idle the data is sent. michael@0: const IDLE_TIMEOUT_SECONDS = 5 * 60; michael@0: michael@0: var gLastMemoryPoll = null; michael@0: michael@0: let gWasDebuggerAttached = false; michael@0: michael@0: function getLocale() { michael@0: return Cc["@mozilla.org/chrome/chrome-registry;1"]. michael@0: getService(Ci.nsIXULChromeRegistry). michael@0: getSelectedLocale('global'); michael@0: } michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", michael@0: "@mozilla.org/base/telemetry;1", michael@0: "nsITelemetry"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "idleService", michael@0: "@mozilla.org/widget/idleservice;1", michael@0: "nsIIdleService"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", michael@0: "resource://gre/modules/UpdateChannel.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", michael@0: "resource://gre/modules/AddonManager.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFile", michael@0: "resource://gre/modules/TelemetryFile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", michael@0: "resource://gre/modules/UITelemetry.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog", michael@0: "resource://gre/modules/TelemetryLog.jsm"); michael@0: michael@0: function generateUUID() { michael@0: let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); michael@0: // strip {} michael@0: return str.substring(1, str.length - 1); michael@0: } michael@0: michael@0: /** michael@0: * Read current process I/O counters. michael@0: */ michael@0: let processInfo = { michael@0: _initialized: false, michael@0: _IO_COUNTERS: null, michael@0: _kernel32: null, michael@0: _GetProcessIoCounters: null, michael@0: _GetCurrentProcess: null, michael@0: getCounters: function() { michael@0: let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes); michael@0: if (isWindows) michael@0: return this.getCounters_Windows(); michael@0: return null; michael@0: }, michael@0: getCounters_Windows: function() { michael@0: if (!this._initialized){ michael@0: Cu.import("resource://gre/modules/ctypes.jsm"); michael@0: this._IO_COUNTERS = new ctypes.StructType("IO_COUNTERS", [ michael@0: {'readOps': ctypes.unsigned_long_long}, michael@0: {'writeOps': ctypes.unsigned_long_long}, michael@0: {'otherOps': ctypes.unsigned_long_long}, michael@0: {'readBytes': ctypes.unsigned_long_long}, michael@0: {'writeBytes': ctypes.unsigned_long_long}, michael@0: {'otherBytes': ctypes.unsigned_long_long} ]); michael@0: try { michael@0: this._kernel32 = ctypes.open("Kernel32.dll"); michael@0: this._GetProcessIoCounters = this._kernel32.declare("GetProcessIoCounters", michael@0: ctypes.winapi_abi, michael@0: ctypes.bool, // return michael@0: ctypes.voidptr_t, // hProcess michael@0: this._IO_COUNTERS.ptr); // lpIoCounters michael@0: this._GetCurrentProcess = this._kernel32.declare("GetCurrentProcess", michael@0: ctypes.winapi_abi, michael@0: ctypes.voidptr_t); // return michael@0: this._initialized = true; michael@0: } catch (err) { michael@0: return null; michael@0: } michael@0: } michael@0: let io = new this._IO_COUNTERS(); michael@0: if(!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address())) michael@0: return null; michael@0: return [parseInt(io.readBytes), parseInt(io.writeBytes)]; michael@0: } michael@0: }; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["TelemetryPing"]; michael@0: michael@0: this.TelemetryPing = Object.freeze({ michael@0: /** michael@0: * Returns the current telemetry payload. michael@0: * @returns Object michael@0: */ michael@0: getPayload: function() { michael@0: return Impl.getPayload(); michael@0: }, michael@0: /** michael@0: * Save histograms to a file. michael@0: * Used only for testing purposes. michael@0: * michael@0: * @param {nsIFile} aFile The file to load from. michael@0: */ michael@0: testSaveHistograms: function(aFile) { michael@0: return Impl.testSaveHistograms(aFile); michael@0: }, michael@0: /** michael@0: * Collect and store information about startup. michael@0: */ michael@0: gatherStartup: function() { michael@0: return Impl.gatherStartup(); michael@0: }, michael@0: /** michael@0: * Inform the ping which AddOns are installed. michael@0: * michael@0: * @param aAddOns - The AddOns. michael@0: */ michael@0: setAddOns: function(aAddOns) { michael@0: return Impl.setAddOns(aAddOns); michael@0: }, michael@0: /** michael@0: * Send a ping to a test server. Used only for testing. michael@0: * michael@0: * @param aServer - The server. michael@0: */ michael@0: testPing: function(aServer) { michael@0: return Impl.testPing(aServer); michael@0: }, michael@0: /** michael@0: * Load histograms from a file. michael@0: * Used only for testing purposes. michael@0: * michael@0: * @param aFile - File to load from. michael@0: */ michael@0: testLoadHistograms: function(aFile) { michael@0: return Impl.testLoadHistograms(aFile); michael@0: }, michael@0: /** michael@0: * Returns the path component of the current submission URL. michael@0: * @returns String michael@0: */ michael@0: submissionPath: function() { michael@0: return Impl.submissionPath(); michael@0: }, michael@0: Constants: Object.freeze({ michael@0: PREF_ENABLED: PREF_ENABLED, michael@0: PREF_SERVER: PREF_SERVER, michael@0: PREF_PREVIOUS_BUILDID: PREF_PREVIOUS_BUILDID, michael@0: }), michael@0: /** michael@0: * Used only for testing purposes. michael@0: */ michael@0: reset: function() { michael@0: this.uninstall(); michael@0: return this.setup(); michael@0: }, michael@0: /** michael@0: * Used only for testing purposes. michael@0: */ michael@0: setup: function() { michael@0: return Impl.setup(true); michael@0: }, michael@0: /** michael@0: * Used only for testing purposes. michael@0: */ michael@0: uninstall: function() { michael@0: try { michael@0: Impl.uninstall(); michael@0: } catch (ex) { michael@0: // Ignore errors michael@0: } michael@0: }, michael@0: /** michael@0: * Descriptive metadata michael@0: * michael@0: * @param reason michael@0: * The reason for the telemetry ping, this will be included in the michael@0: * returned metadata, michael@0: * @return The metadata as a JS object michael@0: */ michael@0: getMetadata: function(reason) { michael@0: return Impl.getMetadata(reason); michael@0: }, michael@0: /** michael@0: * Send a notification. michael@0: */ michael@0: observe: function (aSubject, aTopic, aData) { michael@0: return Impl.observe(aSubject, aTopic, aData); michael@0: } michael@0: }); michael@0: michael@0: let Impl = { michael@0: _histograms: {}, michael@0: _initialized: false, michael@0: _prevValues: {}, michael@0: // Generate a unique id once per session so the server can cope with michael@0: // duplicate submissions. michael@0: _uuid: generateUUID(), michael@0: // Regex that matches histograms we care about during startup. michael@0: // Keep this in sync with gen-histogram-bucket-ranges.py. michael@0: _startupHistogramRegex: /SQLITE|HTTP|SPDY|CACHE|DNS/, michael@0: _slowSQLStartup: {}, michael@0: _prevSession: null, michael@0: _hasWindowRestoredObserver: false, michael@0: _hasXulWindowVisibleObserver: false, michael@0: _startupIO : {}, michael@0: // The previous build ID, if this is the first run with a new build. michael@0: // Undefined if this is not the first run, or the previous build ID is unknown. michael@0: _previousBuildID: undefined, michael@0: michael@0: /** michael@0: * Gets a series of simple measurements (counters). At the moment, this michael@0: * only returns startup data from nsIAppStartup.getStartupInfo(). michael@0: * michael@0: * @return simple measurements as a dictionary. michael@0: */ michael@0: getSimpleMeasurements: function getSimpleMeasurements(forSavedSession) { michael@0: let si = Services.startup.getStartupInfo(); michael@0: michael@0: var ret = { michael@0: // uptime in minutes michael@0: uptime: Math.round((new Date() - si.process) / 60000) michael@0: } michael@0: michael@0: // Look for app-specific timestamps michael@0: var appTimestamps = {}; michael@0: try { michael@0: let o = {}; michael@0: Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", o); michael@0: appTimestamps = o.TelemetryTimestamps.get(); michael@0: } catch (ex) {} michael@0: try { michael@0: ret.addonManager = AddonManagerPrivate.getSimpleMeasures(); michael@0: } catch (ex) {} michael@0: try { michael@0: ret.UITelemetry = UITelemetry.getSimpleMeasures(); michael@0: } catch (ex) {} michael@0: michael@0: if (si.process) { michael@0: for each (let field in Object.keys(si)) { michael@0: if (field == "process") michael@0: continue; michael@0: ret[field] = si[field] - si.process michael@0: } michael@0: michael@0: for (let p in appTimestamps) { michael@0: if (!(p in ret) && appTimestamps[p]) michael@0: ret[p] = appTimestamps[p] - si.process; michael@0: } michael@0: } michael@0: michael@0: ret.startupInterrupted = Number(Services.startup.interrupted); michael@0: michael@0: // Update debuggerAttached flag michael@0: let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); michael@0: let isDebuggerAttached = debugService.isDebuggerAttached; michael@0: gWasDebuggerAttached = gWasDebuggerAttached || isDebuggerAttached; michael@0: ret.debuggerAttached = Number(gWasDebuggerAttached); michael@0: michael@0: ret.js = Cu.getJSEngineTelemetryValue(); michael@0: michael@0: let shutdownDuration = Telemetry.lastShutdownDuration; michael@0: if (shutdownDuration) michael@0: ret.shutdownDuration = shutdownDuration; michael@0: michael@0: let failedProfileLockCount = Telemetry.failedProfileLockCount; michael@0: if (failedProfileLockCount) michael@0: ret.failedProfileLockCount = failedProfileLockCount; michael@0: michael@0: let maximalNumberOfConcurrentThreads = Telemetry.maximalNumberOfConcurrentThreads; michael@0: if (maximalNumberOfConcurrentThreads) michael@0: ret.maximalNumberOfConcurrentThreads = maximalNumberOfConcurrentThreads; michael@0: michael@0: for (let ioCounter in this._startupIO) michael@0: ret[ioCounter] = this._startupIO[ioCounter]; michael@0: michael@0: let hasPingBeenSent = false; michael@0: try { michael@0: hasPingBeenSent = Telemetry.getHistogramById("TELEMETRY_SUCCESS").snapshot().sum > 0; michael@0: } catch(e) { michael@0: } michael@0: if (!forSavedSession || hasPingBeenSent) { michael@0: ret.savedPings = TelemetryFile.pingsLoaded; michael@0: } michael@0: michael@0: ret.pingsOverdue = TelemetryFile.pingsOverdue; michael@0: ret.pingsDiscarded = TelemetryFile.pingsDiscarded; michael@0: michael@0: return ret; michael@0: }, michael@0: michael@0: /** michael@0: * When reflecting a histogram into JS, Telemetry hands us an object michael@0: * with the following properties: michael@0: * michael@0: * - min, max, histogram_type, sum, sum_squares_{lo,hi}: simple integers; michael@0: * - log_sum, log_sum_squares: doubles; michael@0: * - counts: array of counts for histogram buckets; michael@0: * - ranges: array of calculated bucket sizes. michael@0: * michael@0: * This format is not straightforward to read and potentially bulky michael@0: * with lots of zeros in the counts array. Packing histograms makes michael@0: * raw histograms easier to read and compresses the data a little bit. michael@0: * michael@0: * Returns an object: michael@0: * { range: [min, max], bucket_count: , michael@0: * histogram_type: , sum: , michael@0: * sum_squares_lo: , michael@0: * sum_squares_hi: , michael@0: * log_sum: , log_sum_squares: , michael@0: * values: { bucket1: count1, bucket2: count2, ... } } michael@0: */ michael@0: packHistogram: function packHistogram(hgram) { michael@0: let r = hgram.ranges;; michael@0: let c = hgram.counts; michael@0: let retgram = { michael@0: range: [r[1], r[r.length - 1]], michael@0: bucket_count: r.length, michael@0: histogram_type: hgram.histogram_type, michael@0: values: {}, michael@0: sum: hgram.sum michael@0: }; michael@0: michael@0: if (hgram.histogram_type == Telemetry.HISTOGRAM_EXPONENTIAL) { michael@0: retgram.log_sum = hgram.log_sum; michael@0: retgram.log_sum_squares = hgram.log_sum_squares; michael@0: } else { michael@0: retgram.sum_squares_lo = hgram.sum_squares_lo; michael@0: retgram.sum_squares_hi = hgram.sum_squares_hi; michael@0: } michael@0: michael@0: let first = true; michael@0: let last = 0; michael@0: michael@0: for (let i = 0; i < c.length; i++) { michael@0: let value = c[i]; michael@0: if (!value) michael@0: continue; michael@0: michael@0: // add a lower bound michael@0: if (i && first) { michael@0: retgram.values[r[i - 1]] = 0; michael@0: } michael@0: first = false; michael@0: last = i + 1; michael@0: retgram.values[r[i]] = value; michael@0: } michael@0: michael@0: // add an upper bound michael@0: if (last && last < c.length) michael@0: retgram.values[r[last]] = 0; michael@0: return retgram; michael@0: }, michael@0: michael@0: getHistograms: function getHistograms(hls) { michael@0: let registered = Telemetry.registeredHistograms([]); michael@0: let ret = {}; michael@0: michael@0: for (let name of registered) { michael@0: for (let n of [name, "STARTUP_" + name]) { michael@0: if (n in hls) { michael@0: ret[n] = this.packHistogram(hls[n]); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return ret; michael@0: }, michael@0: michael@0: getAddonHistograms: function getAddonHistograms() { michael@0: let ahs = Telemetry.addonHistogramSnapshots; michael@0: let ret = {}; michael@0: michael@0: for (let addonName in ahs) { michael@0: let addonHistograms = ahs[addonName]; michael@0: let packedHistograms = {}; michael@0: for (let name in addonHistograms) { michael@0: packedHistograms[name] = this.packHistogram(addonHistograms[name]); michael@0: } michael@0: if (Object.keys(packedHistograms).length != 0) michael@0: ret[addonName] = packedHistograms; michael@0: } michael@0: michael@0: return ret; michael@0: }, michael@0: michael@0: getThreadHangStats: function getThreadHangStats(stats) { michael@0: stats.forEach((thread) => { michael@0: thread.activity = this.packHistogram(thread.activity); michael@0: thread.hangs.forEach((hang) => { michael@0: hang.histogram = this.packHistogram(hang.histogram); michael@0: }); michael@0: }); michael@0: return stats; michael@0: }, michael@0: michael@0: /** michael@0: * Descriptive metadata michael@0: * michael@0: * @param reason michael@0: * The reason for the telemetry ping, this will be included in the michael@0: * returned metadata, michael@0: * @return The metadata as a JS object michael@0: */ michael@0: getMetadata: function getMetadata(reason) { michael@0: let ai = Services.appinfo; michael@0: let ret = { michael@0: reason: reason, michael@0: OS: ai.OS, michael@0: appID: ai.ID, michael@0: appVersion: ai.version, michael@0: appName: ai.name, michael@0: appBuildID: ai.appBuildID, michael@0: appUpdateChannel: UpdateChannel.get(), michael@0: platformBuildID: ai.platformBuildID, michael@0: revision: HISTOGRAMS_FILE_VERSION, michael@0: locale: getLocale() michael@0: }; michael@0: michael@0: // In order to share profile data, the appName used for Metro Firefox is "Firefox", michael@0: // (the same as desktop Firefox). We set it to "MetroFirefox" here in order to michael@0: // differentiate telemetry pings sent by desktop vs. metro Firefox. michael@0: if(Services.metro && Services.metro.immersive) { michael@0: ret.appName = "MetroFirefox"; michael@0: } michael@0: michael@0: if (this._previousBuildID) { michael@0: ret.previousBuildID = this._previousBuildID; michael@0: } michael@0: michael@0: // sysinfo fields are not always available, get what we can. michael@0: let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); michael@0: let fields = ["cpucount", "memsize", "arch", "version", "kernel_version", michael@0: "device", "manufacturer", "hardware", "tablet", michael@0: "hasMMX", "hasSSE", "hasSSE2", "hasSSE3", michael@0: "hasSSSE3", "hasSSE4A", "hasSSE4_1", "hasSSE4_2", michael@0: "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON", "isWow64", michael@0: "profileHDDModel", "profileHDDRevision", "binHDDModel", michael@0: "binHDDRevision", "winHDDModel", "winHDDRevision"]; michael@0: for each (let field in fields) { michael@0: let value; michael@0: try { michael@0: value = sysInfo.getProperty(field); michael@0: } catch (e) { michael@0: continue; michael@0: } michael@0: if (field == "memsize") { michael@0: // Send RAM size in megabytes. Rounding because sysinfo doesn't michael@0: // always provide RAM in multiples of 1024. michael@0: value = Math.round(value / 1024 / 1024); michael@0: } michael@0: ret[field] = value; michael@0: } michael@0: michael@0: // gfxInfo fields are not always available, get what we can. michael@0: let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); michael@0: let gfxfields = ["adapterDescription", "adapterVendorID", "adapterDeviceID", michael@0: "adapterRAM", "adapterDriver", "adapterDriverVersion", michael@0: "adapterDriverDate", "adapterDescription2", michael@0: "adapterVendorID2", "adapterDeviceID2", "adapterRAM2", michael@0: "adapterDriver2", "adapterDriverVersion2", michael@0: "adapterDriverDate2", "isGPU2Active", "D2DEnabled", michael@0: "DWriteEnabled", "DWriteVersion" michael@0: ]; michael@0: michael@0: if (gfxInfo) { michael@0: for each (let field in gfxfields) { michael@0: try { michael@0: let value = gfxInfo[field]; michael@0: // bug 940806: We need to do a strict equality comparison here, michael@0: // otherwise a type conversion will occur and boolean false values michael@0: // will get filtered out michael@0: if (value !== "") { michael@0: ret[field] = value; michael@0: } michael@0: } catch (e) { michael@0: continue michael@0: } michael@0: } michael@0: } michael@0: michael@0: #ifndef MOZ_WIDGET_GONK michael@0: let theme = LightweightThemeManager.currentTheme; michael@0: if (theme) michael@0: ret.persona = theme.id; michael@0: #endif michael@0: michael@0: if (this._addons) michael@0: ret.addons = this._addons; michael@0: michael@0: let flashVersion = this.getFlashVersion(); michael@0: if (flashVersion) michael@0: ret.flashVersion = flashVersion; michael@0: michael@0: try { michael@0: let scope = {}; michael@0: Cu.import("resource:///modules/experiments/Experiments.jsm", scope); michael@0: let experiments = scope.Experiments.instance() michael@0: let activeExperiment = experiments.getActiveExperimentID(); michael@0: if (activeExperiment) { michael@0: ret.activeExperiment = activeExperiment; michael@0: ret.activeExperimentBranch = experiments.getActiveExperimentBranch(); michael@0: } michael@0: } catch(e) { michael@0: // If this is not Firefox, the import will fail. michael@0: } michael@0: michael@0: return ret; michael@0: }, michael@0: michael@0: /** michael@0: * Pull values from about:memory into corresponding histograms michael@0: */ michael@0: gatherMemory: function gatherMemory() { michael@0: let mgr; michael@0: try { michael@0: mgr = Cc["@mozilla.org/memory-reporter-manager;1"]. michael@0: getService(Ci.nsIMemoryReporterManager); michael@0: } catch (e) { michael@0: // OK to skip memory reporters in xpcshell michael@0: return; michael@0: } michael@0: michael@0: let histogram = Telemetry.getHistogramById("TELEMETRY_MEMORY_REPORTER_MS"); michael@0: let startTime = new Date(); michael@0: michael@0: // Get memory measurements from distinguished amount attributes. We used michael@0: // to measure "explicit" too, but it could cause hangs, and the data was michael@0: // always really noisy anyway. See bug 859657. michael@0: // michael@0: // test_TelemetryPing.js relies on some of these histograms being michael@0: // here. If you remove any of the following histograms from here, you'll michael@0: // have to modify test_TelemetryPing.js: michael@0: // michael@0: // * MEMORY_JS_GC_HEAP, and michael@0: // * MEMORY_JS_COMPARTMENTS_SYSTEM. michael@0: // michael@0: // The distinguished amount attribute names don't match the telemetry id michael@0: // names in some cases due to a combination of (a) historical reasons, and michael@0: // (b) the fact that we can't change telemetry id names without breaking michael@0: // data continuity. michael@0: // michael@0: let boundHandleMemoryReport = this.handleMemoryReport.bind(this); michael@0: function h(id, units, amountName) { michael@0: try { michael@0: // If mgr[amountName] throws an exception, just move on -- some amounts michael@0: // aren't available on all platforms. But if the attribute simply michael@0: // isn't present, that indicates the distinguished amounts have changed michael@0: // and this file hasn't been updated appropriately. michael@0: let amount = mgr[amountName]; michael@0: NS_ASSERT(amount !== undefined, michael@0: "telemetry accessed an unknown distinguished amount"); michael@0: boundHandleMemoryReport(id, units, amount); michael@0: } catch (e) { michael@0: }; michael@0: } michael@0: let b = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_BYTES, n); michael@0: let c = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT, n); michael@0: let cc= (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE, n); michael@0: let p = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_PERCENTAGE, n); michael@0: michael@0: b("MEMORY_VSIZE", "vsize"); michael@0: b("MEMORY_VSIZE_MAX_CONTIGUOUS", "vsizeMaxContiguous"); michael@0: b("MEMORY_RESIDENT", "residentFast"); michael@0: b("MEMORY_HEAP_ALLOCATED", "heapAllocated"); michael@0: p("MEMORY_HEAP_COMMITTED_UNUSED_RATIO", "heapOverheadRatio"); michael@0: b("MEMORY_JS_GC_HEAP", "JSMainRuntimeGCHeap"); michael@0: b("MEMORY_JS_MAIN_RUNTIME_TEMPORARY_PEAK", "JSMainRuntimeTemporaryPeak"); michael@0: c("MEMORY_JS_COMPARTMENTS_SYSTEM", "JSMainRuntimeCompartmentsSystem"); michael@0: c("MEMORY_JS_COMPARTMENTS_USER", "JSMainRuntimeCompartmentsUser"); michael@0: b("MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED", "imagesContentUsedUncompressed"); michael@0: b("MEMORY_STORAGE_SQLITE", "storageSQLite"); michael@0: cc("MEMORY_EVENTS_VIRTUAL", "lowMemoryEventsVirtual"); michael@0: cc("MEMORY_EVENTS_PHYSICAL", "lowMemoryEventsPhysical"); michael@0: c("GHOST_WINDOWS", "ghostWindows"); michael@0: cc("PAGE_FAULTS_HARD", "pageFaultsHard"); michael@0: michael@0: histogram.add(new Date() - startTime); michael@0: }, michael@0: michael@0: handleMemoryReport: function(id, units, amount) { michael@0: let val; michael@0: if (units == Ci.nsIMemoryReporter.UNITS_BYTES) { michael@0: val = Math.floor(amount / 1024); michael@0: } michael@0: else if (units == Ci.nsIMemoryReporter.UNITS_PERCENTAGE) { michael@0: // UNITS_PERCENTAGE amounts are 100x greater than their raw value. michael@0: val = Math.floor(amount / 100); michael@0: } michael@0: else if (units == Ci.nsIMemoryReporter.UNITS_COUNT) { michael@0: val = amount; michael@0: } michael@0: else if (units == Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE) { michael@0: // If the reporter gives us a cumulative count, we'll report the michael@0: // difference in its value between now and our previous ping. michael@0: michael@0: if (!(id in this._prevValues)) { michael@0: // If this is the first time we're reading this reporter, store its michael@0: // current value but don't report it in the telemetry ping, so we michael@0: // ignore the effect startup had on the reporter. michael@0: this._prevValues[id] = amount; michael@0: return; michael@0: } michael@0: michael@0: val = amount - this._prevValues[id]; michael@0: this._prevValues[id] = amount; michael@0: } michael@0: else { michael@0: NS_ASSERT(false, "Can't handle memory reporter with units " + units); michael@0: return; michael@0: } michael@0: michael@0: let h = this._histograms[id]; michael@0: if (!h) { michael@0: h = Telemetry.getHistogramById(id); michael@0: this._histograms[id] = h; michael@0: } michael@0: h.add(val); michael@0: }, michael@0: michael@0: /** michael@0: * Return true if we're interested in having a STARTUP_* histogram for michael@0: * the given histogram name. michael@0: */ michael@0: isInterestingStartupHistogram: function isInterestingStartupHistogram(name) { michael@0: return this._startupHistogramRegex.test(name); michael@0: }, michael@0: michael@0: /** michael@0: * Make a copy of interesting histograms at startup. michael@0: */ michael@0: gatherStartupHistograms: function gatherStartupHistograms() { michael@0: let info = Telemetry.registeredHistograms([]); michael@0: let snapshots = Telemetry.histogramSnapshots; michael@0: for (let name of info) { michael@0: // Only duplicate histograms with actual data. michael@0: if (this.isInterestingStartupHistogram(name) && name in snapshots) { michael@0: Telemetry.histogramFrom("STARTUP_" + name, name); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get the current session's payload using the provided michael@0: * simpleMeasurements and info, which are typically obtained by a call michael@0: * to |this.getSimpleMeasurements| and |this.getMetadata|, michael@0: * respectively. michael@0: */ michael@0: assemblePayloadWithMeasurements: function assemblePayloadWithMeasurements(simpleMeasurements, info) { michael@0: let payloadObj = { michael@0: ver: PAYLOAD_VERSION, michael@0: simpleMeasurements: simpleMeasurements, michael@0: histograms: this.getHistograms(Telemetry.histogramSnapshots), michael@0: slowSQL: Telemetry.slowSQL, michael@0: fileIOReports: Telemetry.fileIOReports, michael@0: chromeHangs: Telemetry.chromeHangs, michael@0: threadHangStats: this.getThreadHangStats(Telemetry.threadHangStats), michael@0: lateWrites: Telemetry.lateWrites, michael@0: addonHistograms: this.getAddonHistograms(), michael@0: addonDetails: AddonManagerPrivate.getTelemetryDetails(), michael@0: UIMeasurements: UITelemetry.getUIMeasurements(), michael@0: log: TelemetryLog.entries(), michael@0: info: info michael@0: }; michael@0: michael@0: if (Object.keys(this._slowSQLStartup).length != 0 && michael@0: (Object.keys(this._slowSQLStartup.mainThread).length || michael@0: Object.keys(this._slowSQLStartup.otherThreads).length)) { michael@0: payloadObj.slowSQLStartup = this._slowSQLStartup; michael@0: } michael@0: michael@0: return payloadObj; michael@0: }, michael@0: michael@0: getSessionPayload: function getSessionPayload(reason) { michael@0: let measurements = this.getSimpleMeasurements(reason == "saved-session"); michael@0: let info = this.getMetadata(reason); michael@0: return this.assemblePayloadWithMeasurements(measurements, info); michael@0: }, michael@0: michael@0: assemblePing: function assemblePing(payloadObj, reason) { michael@0: let slug = this._uuid; michael@0: return { slug: slug, reason: reason, payload: payloadObj }; michael@0: }, michael@0: michael@0: getSessionPayloadAndSlug: function getSessionPayloadAndSlug(reason) { michael@0: return this.assemblePing(this.getSessionPayload(reason), reason); michael@0: }, michael@0: michael@0: popPayloads: function popPayloads(reason) { michael@0: function payloadIter() { michael@0: if (reason != "overdue-flush") { michael@0: yield this.getSessionPayloadAndSlug(reason); michael@0: } michael@0: let iterator = TelemetryFile.popPendingPings(reason); michael@0: for (let data of iterator) { michael@0: yield data; michael@0: } michael@0: } michael@0: michael@0: let payloadIterWithThis = payloadIter.bind(this); michael@0: return { __iterator__: payloadIterWithThis }; michael@0: }, michael@0: michael@0: /** michael@0: * Send data to the server. Record success/send-time in histograms michael@0: */ michael@0: send: function send(reason, server) { michael@0: // populate histograms one last time michael@0: this.gatherMemory(); michael@0: return this.sendPingsFromIterator(server, reason, michael@0: Iterator(this.popPayloads(reason))); michael@0: }, michael@0: michael@0: sendPingsFromIterator: function sendPingsFromIterator(server, reason, i) { michael@0: let p = [data for (data in i)].map((data) => michael@0: this.doPing(server, data).then(null, () => TelemetryFile.savePing(data, true))); michael@0: michael@0: return Promise.all(p); michael@0: }, michael@0: michael@0: finishPingRequest: function finishPingRequest(success, startTime, ping) { michael@0: let hping = Telemetry.getHistogramById("TELEMETRY_PING"); michael@0: let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS"); michael@0: michael@0: hsuccess.add(success); michael@0: hping.add(new Date() - startTime); michael@0: michael@0: if (success) { michael@0: return TelemetryFile.cleanupPingFile(ping); michael@0: } else { michael@0: return Promise.resolve(); michael@0: } michael@0: }, michael@0: michael@0: submissionPath: function submissionPath(ping) { michael@0: let slug; michael@0: if (!ping) { michael@0: slug = this._uuid; michael@0: } else { michael@0: let info = ping.payload.info; michael@0: let pathComponents = [ping.slug, info.reason, info.appName, michael@0: info.appVersion, info.appUpdateChannel, michael@0: info.appBuildID]; michael@0: slug = pathComponents.join("/"); michael@0: } michael@0: return "/submit/telemetry/" + slug; michael@0: }, michael@0: michael@0: doPing: function doPing(server, ping) { michael@0: let deferred = Promise.defer(); michael@0: let url = server + this.submissionPath(ping); michael@0: let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: request.mozBackgroundRequest = true; michael@0: request.open("POST", url, true); michael@0: request.overrideMimeType("text/plain"); michael@0: request.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); michael@0: michael@0: let startTime = new Date(); michael@0: michael@0: function handler(success) { michael@0: return function(event) { michael@0: this.finishPingRequest(success, startTime, ping).then(() => { michael@0: if (success) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject(event); michael@0: } michael@0: }); michael@0: }; michael@0: } michael@0: request.addEventListener("error", handler(false).bind(this), false); michael@0: request.addEventListener("load", handler(true).bind(this), false); michael@0: michael@0: request.setRequestHeader("Content-Encoding", "gzip"); michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(ping.payload)); michael@0: utf8Payload += converter.Finish(); michael@0: let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"] michael@0: .createInstance(Ci.nsIStringInputStream); michael@0: payloadStream.data = this.gzipCompressString(utf8Payload); michael@0: request.send(payloadStream); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: gzipCompressString: function gzipCompressString(string) { michael@0: let observer = { michael@0: buffer: "", michael@0: onStreamComplete: function(loader, context, status, length, result) { michael@0: this.buffer = String.fromCharCode.apply(this, result); michael@0: } michael@0: }; michael@0: michael@0: let scs = Cc["@mozilla.org/streamConverters;1"] michael@0: .getService(Ci.nsIStreamConverterService); michael@0: let listener = Cc["@mozilla.org/network/stream-loader;1"] michael@0: .createInstance(Ci.nsIStreamLoader); michael@0: listener.init(observer); michael@0: let converter = scs.asyncConvertData("uncompressed", "gzip", michael@0: listener, null); michael@0: let stringStream = Cc["@mozilla.org/io/string-input-stream;1"] michael@0: .createInstance(Ci.nsIStringInputStream); michael@0: stringStream.data = string; michael@0: converter.onStartRequest(null, null); michael@0: converter.onDataAvailable(null, null, stringStream, 0, string.length); michael@0: converter.onStopRequest(null, null, null); michael@0: return observer.buffer; michael@0: }, michael@0: michael@0: attachObservers: function attachObservers() { michael@0: if (!this._initialized) michael@0: return; michael@0: Services.obs.addObserver(this, "cycle-collector-begin", false); michael@0: Services.obs.addObserver(this, "idle-daily", false); michael@0: }, michael@0: michael@0: detachObservers: function detachObservers() { michael@0: if (!this._initialized) michael@0: return; michael@0: Services.obs.removeObserver(this, "idle-daily"); michael@0: Services.obs.removeObserver(this, "cycle-collector-begin"); michael@0: if (this._isIdleObserver) { michael@0: idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); michael@0: this._isIdleObserver = false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Initializes telemetry within a timer. If there is no PREF_SERVER set, don't turn on telemetry. michael@0: */ michael@0: setup: function setup(aTesting) { michael@0: // Initialize some probes that are kept in their own modules michael@0: this._thirdPartyCookies = new ThirdPartyCookieProbe(); michael@0: this._thirdPartyCookies.init(); michael@0: michael@0: // Record old value and update build ID preference if this is the first michael@0: // run with a new build ID. michael@0: let previousBuildID = undefined; michael@0: try { michael@0: previousBuildID = Services.prefs.getCharPref(PREF_PREVIOUS_BUILDID); michael@0: } catch (e) { michael@0: // Preference was not set. michael@0: } michael@0: let thisBuildID = Services.appinfo.appBuildID; michael@0: // If there is no previousBuildID preference, this._previousBuildID remains michael@0: // undefined so no value is sent in the telemetry metadata. michael@0: if (previousBuildID != thisBuildID) { michael@0: this._previousBuildID = previousBuildID; michael@0: Services.prefs.setCharPref(PREF_PREVIOUS_BUILDID, thisBuildID); michael@0: } michael@0: michael@0: #ifdef MOZILLA_OFFICIAL michael@0: if (!Telemetry.canSend) { michael@0: // We can't send data; no point in initializing observers etc. michael@0: // Only do this for official builds so that e.g. developer builds michael@0: // still enable Telemetry based on prefs. michael@0: Telemetry.canRecord = false; michael@0: return; michael@0: } michael@0: #endif michael@0: let enabled = false; michael@0: try { michael@0: enabled = Services.prefs.getBoolPref(PREF_ENABLED); michael@0: this._server = Services.prefs.getCharPref(PREF_SERVER); michael@0: } catch (e) { michael@0: // Prerequesite prefs aren't set michael@0: } michael@0: if (!enabled) { michael@0: // Turn off local telemetry if telemetry is disabled. michael@0: // This may change once about:telemetry is added. michael@0: Telemetry.canRecord = false; michael@0: return; michael@0: } michael@0: michael@0: AsyncShutdown.sendTelemetry.addBlocker( michael@0: "Telemetry: shutting down", michael@0: function condition(){ michael@0: this.uninstall(); michael@0: if (Telemetry.canSend) { michael@0: return this.savePendingPings(); michael@0: } michael@0: }.bind(this)); michael@0: michael@0: Services.obs.addObserver(this, "sessionstore-windows-restored", false); michael@0: Services.obs.addObserver(this, "quit-application-granted", false); michael@0: #ifdef MOZ_WIDGET_ANDROID michael@0: Services.obs.addObserver(this, "application-background", false); michael@0: #endif michael@0: Services.obs.addObserver(this, "xul-window-visible", false); michael@0: this._hasWindowRestoredObserver = true; michael@0: this._hasXulWindowVisibleObserver = true; michael@0: michael@0: // Delay full telemetry initialization to give the browser time to michael@0: // run various late initializers. Otherwise our gathered memory michael@0: // footprint and other numbers would be too optimistic. michael@0: this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: let deferred = Promise.defer(); michael@0: michael@0: function timerCallback() { michael@0: Task.spawn(function*(){ michael@0: this._initialized = true; michael@0: michael@0: yield TelemetryFile.loadSavedPings(); michael@0: // If we have any TelemetryPings lying around, we'll be aggressive michael@0: // and try to send them all off ASAP. michael@0: if (TelemetryFile.pingsOverdue > 0) { michael@0: // It doesn't really matter what we pass to this.send as a reason, michael@0: // since it's never sent to the server. All that this.send does with michael@0: // the reason is check to make sure it's not a test-ping. michael@0: yield this.send("overdue-flush", this._server); michael@0: } michael@0: michael@0: this.attachObservers(); michael@0: this.gatherMemory(); michael@0: michael@0: Telemetry.asyncFetchTelemetryData(function () {}); michael@0: delete this._timer; michael@0: deferred.resolve(); michael@0: }.bind(this)); michael@0: } michael@0: michael@0: this._timer.initWithCallback(timerCallback.bind(this), michael@0: aTesting ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY, michael@0: Ci.nsITimer.TYPE_ONE_SHOT); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: testLoadHistograms: function testLoadHistograms(file) { michael@0: return TelemetryFile.testLoadHistograms(file); michael@0: }, michael@0: michael@0: getFlashVersion: function getFlashVersion() { michael@0: let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); michael@0: let tags = host.getPluginTags(); michael@0: michael@0: for (let i = 0; i < tags.length; i++) { michael@0: if (tags[i].name == "Shockwave Flash") michael@0: return tags[i].version; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: savePendingPings: function savePendingPings() { michael@0: let sessionPing = this.getSessionPayloadAndSlug("saved-session"); michael@0: return TelemetryFile.savePendingPings(sessionPing); michael@0: }, michael@0: michael@0: testSaveHistograms: function testSaveHistograms(file) { michael@0: return TelemetryFile.savePingToFile(this.getSessionPayloadAndSlug("saved-session"), michael@0: file.path, true); michael@0: }, michael@0: michael@0: /** michael@0: * Remove observers to avoid leaks michael@0: */ michael@0: uninstall: function uninstall() { michael@0: this.detachObservers(); michael@0: if (this._hasWindowRestoredObserver) { michael@0: Services.obs.removeObserver(this, "sessionstore-windows-restored"); michael@0: this._hasWindowRestoredObserver = false; michael@0: } michael@0: if (this._hasXulWindowVisibleObserver) { michael@0: Services.obs.removeObserver(this, "xul-window-visible"); michael@0: this._hasXulWindowVisibleObserver = false; michael@0: } michael@0: Services.obs.removeObserver(this, "quit-application-granted"); michael@0: #ifdef MOZ_WIDGET_ANDROID michael@0: Services.obs.removeObserver(this, "application-background", false); michael@0: #endif michael@0: }, michael@0: michael@0: getPayload: function getPayload() { michael@0: // This function returns the current Telemetry payload to the caller. michael@0: // We only gather startup info once. michael@0: if (Object.keys(this._slowSQLStartup).length == 0) { michael@0: this.gatherStartupHistograms(); michael@0: this._slowSQLStartup = Telemetry.slowSQL; michael@0: } michael@0: this.gatherMemory(); michael@0: return this.getSessionPayload("gather-payload"); michael@0: }, michael@0: michael@0: gatherStartup: function gatherStartup() { michael@0: let counters = processInfo.getCounters(); michael@0: if (counters) { michael@0: [this._startupIO.startupSessionRestoreReadBytes, michael@0: this._startupIO.startupSessionRestoreWriteBytes] = counters; michael@0: } michael@0: this.gatherStartupHistograms(); michael@0: this._slowSQLStartup = Telemetry.slowSQL; michael@0: }, michael@0: michael@0: setAddOns: function setAddOns(aAddOns) { michael@0: this._addons = aAddOns; michael@0: }, michael@0: michael@0: sendIdlePing: function sendIdlePing(aTest, aServer) { michael@0: if (this._isIdleObserver) { michael@0: idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); michael@0: this._isIdleObserver = false; michael@0: } michael@0: if (aTest) { michael@0: return this.send("test-ping", aServer); michael@0: } else if (Telemetry.canSend) { michael@0: return this.send("idle-daily", aServer); michael@0: } michael@0: }, michael@0: michael@0: testPing: function testPing(server) { michael@0: return this.sendIdlePing(true, server); michael@0: }, michael@0: michael@0: /** michael@0: * This observer drives telemetry. michael@0: */ michael@0: observe: function (aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "profile-after-change": michael@0: return this.setup(); michael@0: case "cycle-collector-begin": michael@0: let now = new Date(); michael@0: if (!gLastMemoryPoll michael@0: || (TELEMETRY_INTERVAL <= now - gLastMemoryPoll)) { michael@0: gLastMemoryPoll = now; michael@0: this.gatherMemory(); michael@0: } michael@0: break; michael@0: case "xul-window-visible": michael@0: Services.obs.removeObserver(this, "xul-window-visible"); michael@0: this._hasXulWindowVisibleObserver = false; michael@0: var counters = processInfo.getCounters(); michael@0: if (counters) { michael@0: [this._startupIO.startupWindowVisibleReadBytes, michael@0: this._startupIO.startupWindowVisibleWriteBytes] = counters; michael@0: } michael@0: break; michael@0: case "sessionstore-windows-restored": michael@0: Services.obs.removeObserver(this, "sessionstore-windows-restored"); michael@0: this._hasWindowRestoredObserver = false; michael@0: // Check whether debugger was attached during startup michael@0: let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); michael@0: gWasDebuggerAttached = debugService.isDebuggerAttached; michael@0: this.gatherStartup(); michael@0: break; michael@0: case "idle-daily": michael@0: // Enqueue to main-thread, otherwise components may be inited by the michael@0: // idle-daily category and miss the gather-telemetry notification. michael@0: Services.tm.mainThread.dispatch((function() { michael@0: // Notify that data should be gathered now, since ping will happen soon. michael@0: Services.obs.notifyObservers(null, "gather-telemetry", null); michael@0: // The ping happens at the first idle of length IDLE_TIMEOUT_SECONDS. michael@0: idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); michael@0: this._isIdleObserver = true; michael@0: }).bind(this), Ci.nsIThread.DISPATCH_NORMAL); michael@0: break; michael@0: case "idle": michael@0: this.sendIdlePing(false, this._server); michael@0: break; michael@0: michael@0: #ifdef MOZ_WIDGET_ANDROID michael@0: // On Android, we can get killed without warning once we are in the background, michael@0: // but we may also submit data and/or come back into the foreground without getting michael@0: // killed. To deal with this, we save the current session data to file when we are michael@0: // put into the background. This handles the following post-backgrounding scenarios: michael@0: // 1) We are killed immediately. In this case the current session data (which we michael@0: // save to a file) will be loaded and submitted on a future run. michael@0: // 2) We submit the data while in the background, and then are killed. In this case michael@0: // the file that we saved will be deleted by the usual process in michael@0: // finishPingRequest after it is submitted. michael@0: // 3) We submit the data, and then come back into the foreground. Same as case (2). michael@0: // 4) We do not submit the data, but come back into the foreground. In this case michael@0: // we have the option of either deleting the file that we saved (since we will either michael@0: // send the live data while in the foreground, or create the file again on the next michael@0: // backgrounding), or not (in which case we will delete it on submit, or overwrite michael@0: // it on the next backgrounding). Not deleting it is faster, so that's what we do. michael@0: case "application-background": michael@0: if (Telemetry.canSend) { michael@0: let ping = this.getSessionPayloadAndSlug("saved-session"); michael@0: TelemetryFile.savePing(ping, true); michael@0: } michael@0: break; michael@0: #endif michael@0: } michael@0: }, michael@0: };