toolkit/components/telemetry/TelemetryPing.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 "use strict";
     8 const Cc = Components.classes;
     9 const Ci = Components.interfaces;
    10 const Cr = Components.results;
    11 const Cu = Components.utils;
    13 Cu.import("resource://gre/modules/debug.js", this);
    14 Cu.import("resource://gre/modules/Services.jsm", this);
    15 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    16 #ifndef MOZ_WIDGET_GONK
    17 Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
    18 #endif
    19 Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm", this);
    20 Cu.import("resource://gre/modules/Promise.jsm", this);
    21 Cu.import("resource://gre/modules/Task.jsm", this);
    22 Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
    24 // When modifying the payload in incompatible ways, please bump this version number
    25 const PAYLOAD_VERSION = 1;
    27 // This is the HG changeset of the Histogram.json file, used to associate
    28 // submitted ping data with its histogram definition (bug 832007)
    29 #expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__";
    31 const PREF_BRANCH = "toolkit.telemetry.";
    32 const PREF_SERVER = PREF_BRANCH + "server";
    33 const PREF_ENABLED = PREF_BRANCH + "enabled";
    34 const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID";
    36 // Do not gather data more than once a minute
    37 const TELEMETRY_INTERVAL = 60000;
    38 // Delay before intializing telemetry (ms)
    39 const TELEMETRY_DELAY = 60000;
    40 // Delay before initializing telemetry if we're testing (ms)
    41 const TELEMETRY_TEST_DELAY = 100;
    43 // Seconds of idle time before pinging.
    44 // On idle-daily a gather-telemetry notification is fired, during it probes can
    45 // start asynchronous tasks to gather data.  On the next idle the data is sent.
    46 const IDLE_TIMEOUT_SECONDS = 5 * 60;
    48 var gLastMemoryPoll = null;
    50 let gWasDebuggerAttached = false;
    52 function getLocale() {
    53   return Cc["@mozilla.org/chrome/chrome-registry;1"].
    54          getService(Ci.nsIXULChromeRegistry).
    55          getSelectedLocale('global');
    56 }
    58 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
    59                                    "@mozilla.org/base/telemetry;1",
    60                                    "nsITelemetry");
    61 XPCOMUtils.defineLazyServiceGetter(this, "idleService",
    62                                    "@mozilla.org/widget/idleservice;1",
    63                                    "nsIIdleService");
    64 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
    65                                   "resource://gre/modules/UpdateChannel.jsm");
    66 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
    67                                   "resource://gre/modules/AddonManager.jsm");
    68 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFile",
    69                                   "resource://gre/modules/TelemetryFile.jsm");
    70 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
    71                                   "resource://gre/modules/UITelemetry.jsm");
    72 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
    73                                   "resource://gre/modules/TelemetryLog.jsm");
    75 function generateUUID() {
    76   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
    77   // strip {}
    78   return str.substring(1, str.length - 1);
    79 }
    81 /**
    82  * Read current process I/O counters.
    83  */
    84 let processInfo = {
    85   _initialized: false,
    86   _IO_COUNTERS: null,
    87   _kernel32: null,
    88   _GetProcessIoCounters: null,
    89   _GetCurrentProcess: null,
    90   getCounters: function() {
    91     let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
    92     if (isWindows)
    93       return this.getCounters_Windows();
    94     return null;
    95   },
    96   getCounters_Windows: function() {
    97     if (!this._initialized){
    98       Cu.import("resource://gre/modules/ctypes.jsm");
    99       this._IO_COUNTERS = new ctypes.StructType("IO_COUNTERS", [
   100         {'readOps': ctypes.unsigned_long_long},
   101         {'writeOps': ctypes.unsigned_long_long},
   102         {'otherOps': ctypes.unsigned_long_long},
   103         {'readBytes': ctypes.unsigned_long_long},
   104         {'writeBytes': ctypes.unsigned_long_long},
   105         {'otherBytes': ctypes.unsigned_long_long} ]);
   106       try {
   107         this._kernel32 = ctypes.open("Kernel32.dll");
   108         this._GetProcessIoCounters = this._kernel32.declare("GetProcessIoCounters",
   109           ctypes.winapi_abi,
   110           ctypes.bool, // return
   111           ctypes.voidptr_t, // hProcess
   112           this._IO_COUNTERS.ptr); // lpIoCounters
   113         this._GetCurrentProcess = this._kernel32.declare("GetCurrentProcess",
   114           ctypes.winapi_abi,
   115           ctypes.voidptr_t); // return
   116         this._initialized = true;
   117       } catch (err) {
   118         return null;
   119       }
   120     }
   121     let io = new this._IO_COUNTERS();
   122     if(!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address()))
   123       return null;
   124     return [parseInt(io.readBytes), parseInt(io.writeBytes)];
   125   }
   126 };
   128 this.EXPORTED_SYMBOLS = ["TelemetryPing"];
   130 this.TelemetryPing = Object.freeze({
   131   /**
   132    * Returns the current telemetry payload.
   133    * @returns Object
   134    */
   135   getPayload: function() {
   136     return Impl.getPayload();
   137   },
   138   /**
   139    * Save histograms to a file.
   140    * Used only for testing purposes.
   141    *
   142    * @param {nsIFile} aFile The file to load from.
   143    */
   144   testSaveHistograms: function(aFile) {
   145     return Impl.testSaveHistograms(aFile);
   146   },
   147   /**
   148    * Collect and store information about startup.
   149    */
   150   gatherStartup: function() {
   151     return Impl.gatherStartup();
   152   },
   153   /**
   154    * Inform the ping which AddOns are installed.
   155    *
   156    * @param aAddOns - The AddOns.
   157    */
   158   setAddOns: function(aAddOns) {
   159     return Impl.setAddOns(aAddOns);
   160   },
   161   /**
   162    * Send a ping to a test server. Used only for testing.
   163    *
   164    * @param aServer - The server.
   165    */
   166   testPing: function(aServer) {
   167     return Impl.testPing(aServer);
   168   },
   169   /**
   170    * Load histograms from a file.
   171    * Used only for testing purposes.
   172    *
   173    * @param aFile - File to load from.
   174    */
   175   testLoadHistograms: function(aFile) {
   176     return Impl.testLoadHistograms(aFile);
   177   },
   178   /**
   179    * Returns the path component of the current submission URL.
   180    * @returns String
   181    */
   182   submissionPath: function() {
   183     return Impl.submissionPath();
   184   },
   185   Constants: Object.freeze({
   186     PREF_ENABLED: PREF_ENABLED,
   187     PREF_SERVER: PREF_SERVER,
   188     PREF_PREVIOUS_BUILDID: PREF_PREVIOUS_BUILDID,
   189   }),
   190   /**
   191    * Used only for testing purposes.
   192    */
   193   reset: function() {
   194     this.uninstall();
   195     return this.setup();
   196   },
   197   /**
   198    * Used only for testing purposes.
   199    */
   200   setup: function() {
   201     return Impl.setup(true);
   202   },
   203   /**
   204    * Used only for testing purposes.
   205    */
   206   uninstall: function() {
   207     try {
   208       Impl.uninstall();
   209     } catch (ex) {
   210       // Ignore errors
   211     }
   212   },
   213   /**
   214    * Descriptive metadata
   215    *
   216    * @param  reason
   217    *         The reason for the telemetry ping, this will be included in the
   218    *         returned metadata,
   219    * @return The metadata as a JS object
   220    */
   221   getMetadata: function(reason) {
   222     return Impl.getMetadata(reason);
   223   },
   224   /**
   225    * Send a notification.
   226    */
   227   observe: function (aSubject, aTopic, aData) {
   228     return Impl.observe(aSubject, aTopic, aData);
   229   }
   230 });
   232 let Impl = {
   233   _histograms: {},
   234   _initialized: false,
   235   _prevValues: {},
   236   // Generate a unique id once per session so the server can cope with
   237   // duplicate submissions.
   238   _uuid: generateUUID(),
   239   // Regex that matches histograms we care about during startup.
   240   // Keep this in sync with gen-histogram-bucket-ranges.py.
   241   _startupHistogramRegex: /SQLITE|HTTP|SPDY|CACHE|DNS/,
   242   _slowSQLStartup: {},
   243   _prevSession: null,
   244   _hasWindowRestoredObserver: false,
   245   _hasXulWindowVisibleObserver: false,
   246   _startupIO : {},
   247   // The previous build ID, if this is the first run with a new build.
   248   // Undefined if this is not the first run, or the previous build ID is unknown.
   249   _previousBuildID: undefined,
   251   /**
   252    * Gets a series of simple measurements (counters). At the moment, this
   253    * only returns startup data from nsIAppStartup.getStartupInfo().
   254    *
   255    * @return simple measurements as a dictionary.
   256    */
   257   getSimpleMeasurements: function getSimpleMeasurements(forSavedSession) {
   258     let si = Services.startup.getStartupInfo();
   260     var ret = {
   261       // uptime in minutes
   262       uptime: Math.round((new Date() - si.process) / 60000)
   263     }
   265     // Look for app-specific timestamps
   266     var appTimestamps = {};
   267     try {
   268       let o = {};
   269       Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", o);
   270       appTimestamps = o.TelemetryTimestamps.get();
   271     } catch (ex) {}
   272     try {
   273       ret.addonManager = AddonManagerPrivate.getSimpleMeasures();
   274     } catch (ex) {}
   275     try {
   276       ret.UITelemetry = UITelemetry.getSimpleMeasures();
   277     } catch (ex) {}
   279     if (si.process) {
   280       for each (let field in Object.keys(si)) {
   281         if (field == "process")
   282           continue;
   283         ret[field] = si[field] - si.process
   284       }
   286       for (let p in appTimestamps) {
   287         if (!(p in ret) && appTimestamps[p])
   288           ret[p] = appTimestamps[p] - si.process;
   289       }
   290     }
   292     ret.startupInterrupted = Number(Services.startup.interrupted);
   294     // Update debuggerAttached flag
   295     let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
   296     let isDebuggerAttached = debugService.isDebuggerAttached;
   297     gWasDebuggerAttached = gWasDebuggerAttached || isDebuggerAttached;
   298     ret.debuggerAttached = Number(gWasDebuggerAttached);
   300     ret.js = Cu.getJSEngineTelemetryValue();
   302     let shutdownDuration = Telemetry.lastShutdownDuration;
   303     if (shutdownDuration)
   304       ret.shutdownDuration = shutdownDuration;
   306     let failedProfileLockCount = Telemetry.failedProfileLockCount;
   307     if (failedProfileLockCount)
   308       ret.failedProfileLockCount = failedProfileLockCount;
   310     let maximalNumberOfConcurrentThreads = Telemetry.maximalNumberOfConcurrentThreads;
   311     if (maximalNumberOfConcurrentThreads)
   312       ret.maximalNumberOfConcurrentThreads = maximalNumberOfConcurrentThreads;
   314     for (let ioCounter in this._startupIO)
   315       ret[ioCounter] = this._startupIO[ioCounter];
   317     let hasPingBeenSent = false;
   318     try {
   319       hasPingBeenSent = Telemetry.getHistogramById("TELEMETRY_SUCCESS").snapshot().sum > 0;
   320     } catch(e) {
   321     }
   322     if (!forSavedSession || hasPingBeenSent) {
   323       ret.savedPings = TelemetryFile.pingsLoaded;
   324     }
   326     ret.pingsOverdue = TelemetryFile.pingsOverdue;
   327     ret.pingsDiscarded = TelemetryFile.pingsDiscarded;
   329     return ret;
   330   },
   332   /**
   333    * When reflecting a histogram into JS, Telemetry hands us an object
   334    * with the following properties:
   335    *
   336    * - min, max, histogram_type, sum, sum_squares_{lo,hi}: simple integers;
   337    * - log_sum, log_sum_squares: doubles;
   338    * - counts: array of counts for histogram buckets;
   339    * - ranges: array of calculated bucket sizes.
   340    *
   341    * This format is not straightforward to read and potentially bulky
   342    * with lots of zeros in the counts array.  Packing histograms makes
   343    * raw histograms easier to read and compresses the data a little bit.
   344    *
   345    * Returns an object:
   346    * { range: [min, max], bucket_count: <number of buckets>,
   347    *   histogram_type: <histogram_type>, sum: <sum>,
   348    *   sum_squares_lo: <sum_squares_lo>,
   349    *   sum_squares_hi: <sum_squares_hi>,
   350    *   log_sum: <log_sum>, log_sum_squares: <log_sum_squares>,
   351    *   values: { bucket1: count1, bucket2: count2, ... } }
   352    */
   353   packHistogram: function packHistogram(hgram) {
   354     let r = hgram.ranges;;
   355     let c = hgram.counts;
   356     let retgram = {
   357       range: [r[1], r[r.length - 1]],
   358       bucket_count: r.length,
   359       histogram_type: hgram.histogram_type,
   360       values: {},
   361       sum: hgram.sum
   362     };
   364     if (hgram.histogram_type == Telemetry.HISTOGRAM_EXPONENTIAL) {
   365       retgram.log_sum = hgram.log_sum;
   366       retgram.log_sum_squares = hgram.log_sum_squares;
   367     } else {
   368       retgram.sum_squares_lo = hgram.sum_squares_lo;
   369       retgram.sum_squares_hi = hgram.sum_squares_hi;
   370     }
   372     let first = true;
   373     let last = 0;
   375     for (let i = 0; i < c.length; i++) {
   376       let value = c[i];
   377       if (!value)
   378         continue;
   380       // add a lower bound
   381       if (i && first) {
   382         retgram.values[r[i - 1]] = 0;
   383       }
   384       first = false;
   385       last = i + 1;
   386       retgram.values[r[i]] = value;
   387     }
   389     // add an upper bound
   390     if (last && last < c.length)
   391       retgram.values[r[last]] = 0;
   392     return retgram;
   393   },
   395   getHistograms: function getHistograms(hls) {
   396     let registered = Telemetry.registeredHistograms([]);
   397     let ret = {};
   399     for (let name of registered) {
   400       for (let n of [name, "STARTUP_" + name]) {
   401         if (n in hls) {
   402           ret[n] = this.packHistogram(hls[n]);
   403         }
   404       }
   405     }
   407     return ret;
   408   },
   410   getAddonHistograms: function getAddonHistograms() {
   411     let ahs = Telemetry.addonHistogramSnapshots;
   412     let ret = {};
   414     for (let addonName in ahs) {
   415       let addonHistograms = ahs[addonName];
   416       let packedHistograms = {};
   417       for (let name in addonHistograms) {
   418         packedHistograms[name] = this.packHistogram(addonHistograms[name]);
   419       }
   420       if (Object.keys(packedHistograms).length != 0)
   421         ret[addonName] = packedHistograms;
   422     }
   424     return ret;
   425   },
   427   getThreadHangStats: function getThreadHangStats(stats) {
   428     stats.forEach((thread) => {
   429       thread.activity = this.packHistogram(thread.activity);
   430       thread.hangs.forEach((hang) => {
   431         hang.histogram = this.packHistogram(hang.histogram);
   432       });
   433     });
   434     return stats;
   435   },
   437   /**
   438    * Descriptive metadata
   439    *
   440    * @param  reason
   441    *         The reason for the telemetry ping, this will be included in the
   442    *         returned metadata,
   443    * @return The metadata as a JS object
   444    */
   445   getMetadata: function getMetadata(reason) {
   446     let ai = Services.appinfo;
   447     let ret = {
   448       reason: reason,
   449       OS: ai.OS,
   450       appID: ai.ID,
   451       appVersion: ai.version,
   452       appName: ai.name,
   453       appBuildID: ai.appBuildID,
   454       appUpdateChannel: UpdateChannel.get(),
   455       platformBuildID: ai.platformBuildID,
   456       revision: HISTOGRAMS_FILE_VERSION,
   457       locale: getLocale()
   458     };
   460     // In order to share profile data, the appName used for Metro Firefox is "Firefox",
   461     // (the same as desktop Firefox). We set it to "MetroFirefox" here in order to
   462     // differentiate telemetry pings sent by desktop vs. metro Firefox.
   463     if(Services.metro && Services.metro.immersive) {
   464       ret.appName = "MetroFirefox";
   465     }
   467     if (this._previousBuildID) {
   468       ret.previousBuildID = this._previousBuildID;
   469     }
   471     // sysinfo fields are not always available, get what we can.
   472     let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
   473     let fields = ["cpucount", "memsize", "arch", "version", "kernel_version",
   474                   "device", "manufacturer", "hardware", "tablet",
   475                   "hasMMX", "hasSSE", "hasSSE2", "hasSSE3",
   476                   "hasSSSE3", "hasSSE4A", "hasSSE4_1", "hasSSE4_2",
   477                   "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON", "isWow64",
   478                   "profileHDDModel", "profileHDDRevision", "binHDDModel",
   479                   "binHDDRevision", "winHDDModel", "winHDDRevision"];
   480     for each (let field in fields) {
   481       let value;
   482       try {
   483         value = sysInfo.getProperty(field);
   484       } catch (e) {
   485         continue;
   486       }
   487       if (field == "memsize") {
   488         // Send RAM size in megabytes. Rounding because sysinfo doesn't
   489         // always provide RAM in multiples of 1024.
   490         value = Math.round(value / 1024 / 1024);
   491       }
   492       ret[field] = value;
   493     }
   495     // gfxInfo fields are not always available, get what we can.
   496     let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
   497     let gfxfields = ["adapterDescription", "adapterVendorID", "adapterDeviceID",
   498                      "adapterRAM", "adapterDriver", "adapterDriverVersion",
   499                      "adapterDriverDate", "adapterDescription2",
   500                      "adapterVendorID2", "adapterDeviceID2", "adapterRAM2",
   501                      "adapterDriver2", "adapterDriverVersion2",
   502                      "adapterDriverDate2", "isGPU2Active", "D2DEnabled",
   503                      "DWriteEnabled", "DWriteVersion"
   504                     ];
   506     if (gfxInfo) {
   507       for each (let field in gfxfields) {
   508         try {
   509           let value = gfxInfo[field];
   510           // bug 940806: We need to do a strict equality comparison here,
   511           // otherwise a type conversion will occur and boolean false values
   512           // will get filtered out
   513           if (value !== "") {
   514             ret[field] = value;
   515           }
   516         } catch (e) {
   517           continue
   518         }
   519       }
   520     }
   522 #ifndef MOZ_WIDGET_GONK
   523     let theme = LightweightThemeManager.currentTheme;
   524     if (theme)
   525       ret.persona = theme.id;
   526 #endif
   528     if (this._addons)
   529       ret.addons = this._addons;
   531     let flashVersion = this.getFlashVersion();
   532     if (flashVersion)
   533       ret.flashVersion = flashVersion;
   535     try {
   536       let scope = {};
   537       Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
   538       let experiments = scope.Experiments.instance()
   539       let activeExperiment = experiments.getActiveExperimentID();
   540       if (activeExperiment) {
   541         ret.activeExperiment = activeExperiment;
   542 	ret.activeExperimentBranch = experiments.getActiveExperimentBranch();
   543       }
   544     } catch(e) {
   545       // If this is not Firefox, the import will fail.
   546     }
   548     return ret;
   549   },
   551   /**
   552    * Pull values from about:memory into corresponding histograms
   553    */
   554   gatherMemory: function gatherMemory() {
   555     let mgr;
   556     try {
   557       mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
   558             getService(Ci.nsIMemoryReporterManager);
   559     } catch (e) {
   560       // OK to skip memory reporters in xpcshell
   561       return;
   562     }
   564     let histogram = Telemetry.getHistogramById("TELEMETRY_MEMORY_REPORTER_MS");
   565     let startTime = new Date();
   567     // Get memory measurements from distinguished amount attributes.  We used
   568     // to measure "explicit" too, but it could cause hangs, and the data was
   569     // always really noisy anyway.  See bug 859657.
   570     //
   571     // test_TelemetryPing.js relies on some of these histograms being
   572     // here.  If you remove any of the following histograms from here, you'll
   573     // have to modify test_TelemetryPing.js:
   574     //
   575     //   * MEMORY_JS_GC_HEAP, and
   576     //   * MEMORY_JS_COMPARTMENTS_SYSTEM.
   577     //
   578     // The distinguished amount attribute names don't match the telemetry id
   579     // names in some cases due to a combination of (a) historical reasons, and
   580     // (b) the fact that we can't change telemetry id names without breaking
   581     // data continuity.
   582     //
   583     let boundHandleMemoryReport = this.handleMemoryReport.bind(this);
   584     function h(id, units, amountName) {
   585       try {
   586         // If mgr[amountName] throws an exception, just move on -- some amounts
   587         // aren't available on all platforms.  But if the attribute simply
   588         // isn't present, that indicates the distinguished amounts have changed
   589         // and this file hasn't been updated appropriately.
   590         let amount = mgr[amountName];
   591         NS_ASSERT(amount !== undefined,
   592                   "telemetry accessed an unknown distinguished amount");
   593         boundHandleMemoryReport(id, units, amount);
   594       } catch (e) {
   595       };
   596     }
   597     let b = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_BYTES, n);
   598     let c = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT, n);
   599     let cc= (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE, n);
   600     let p = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_PERCENTAGE, n);
   602     b("MEMORY_VSIZE", "vsize");
   603     b("MEMORY_VSIZE_MAX_CONTIGUOUS", "vsizeMaxContiguous");
   604     b("MEMORY_RESIDENT", "residentFast");
   605     b("MEMORY_HEAP_ALLOCATED", "heapAllocated");
   606     p("MEMORY_HEAP_COMMITTED_UNUSED_RATIO", "heapOverheadRatio");
   607     b("MEMORY_JS_GC_HEAP", "JSMainRuntimeGCHeap");
   608     b("MEMORY_JS_MAIN_RUNTIME_TEMPORARY_PEAK", "JSMainRuntimeTemporaryPeak");
   609     c("MEMORY_JS_COMPARTMENTS_SYSTEM", "JSMainRuntimeCompartmentsSystem");
   610     c("MEMORY_JS_COMPARTMENTS_USER", "JSMainRuntimeCompartmentsUser");
   611     b("MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED", "imagesContentUsedUncompressed");
   612     b("MEMORY_STORAGE_SQLITE", "storageSQLite");
   613     cc("MEMORY_EVENTS_VIRTUAL", "lowMemoryEventsVirtual");
   614     cc("MEMORY_EVENTS_PHYSICAL", "lowMemoryEventsPhysical");
   615     c("GHOST_WINDOWS", "ghostWindows");
   616     cc("PAGE_FAULTS_HARD", "pageFaultsHard");
   618     histogram.add(new Date() - startTime);
   619   },
   621   handleMemoryReport: function(id, units, amount) {
   622     let val;
   623     if (units == Ci.nsIMemoryReporter.UNITS_BYTES) {
   624       val = Math.floor(amount / 1024);
   625     }
   626     else if (units == Ci.nsIMemoryReporter.UNITS_PERCENTAGE) {
   627       // UNITS_PERCENTAGE amounts are 100x greater than their raw value.
   628       val = Math.floor(amount / 100);
   629     }
   630     else if (units == Ci.nsIMemoryReporter.UNITS_COUNT) {
   631       val = amount;
   632     }
   633     else if (units == Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE) {
   634       // If the reporter gives us a cumulative count, we'll report the
   635       // difference in its value between now and our previous ping.
   637       if (!(id in this._prevValues)) {
   638         // If this is the first time we're reading this reporter, store its
   639         // current value but don't report it in the telemetry ping, so we
   640         // ignore the effect startup had on the reporter.
   641         this._prevValues[id] = amount;
   642         return;
   643       }
   645       val = amount - this._prevValues[id];
   646       this._prevValues[id] = amount;
   647     }
   648     else {
   649       NS_ASSERT(false, "Can't handle memory reporter with units " + units);
   650       return;
   651     }
   653     let h = this._histograms[id];
   654     if (!h) {
   655       h = Telemetry.getHistogramById(id);
   656       this._histograms[id] = h;
   657     }
   658     h.add(val);
   659   },
   661   /**
   662    * Return true if we're interested in having a STARTUP_* histogram for
   663    * the given histogram name.
   664    */
   665   isInterestingStartupHistogram: function isInterestingStartupHistogram(name) {
   666     return this._startupHistogramRegex.test(name);
   667   },
   669   /**
   670    * Make a copy of interesting histograms at startup.
   671    */
   672   gatherStartupHistograms: function gatherStartupHistograms() {
   673     let info = Telemetry.registeredHistograms([]);
   674     let snapshots = Telemetry.histogramSnapshots;
   675     for (let name of info) {
   676       // Only duplicate histograms with actual data.
   677       if (this.isInterestingStartupHistogram(name) && name in snapshots) {
   678         Telemetry.histogramFrom("STARTUP_" + name, name);
   679       }
   680     }
   681   },
   683   /**
   684    * Get the current session's payload using the provided
   685    * simpleMeasurements and info, which are typically obtained by a call
   686    * to |this.getSimpleMeasurements| and |this.getMetadata|,
   687    * respectively.
   688    */
   689   assemblePayloadWithMeasurements: function assemblePayloadWithMeasurements(simpleMeasurements, info) {
   690     let payloadObj = {
   691       ver: PAYLOAD_VERSION,
   692       simpleMeasurements: simpleMeasurements,
   693       histograms: this.getHistograms(Telemetry.histogramSnapshots),
   694       slowSQL: Telemetry.slowSQL,
   695       fileIOReports: Telemetry.fileIOReports,
   696       chromeHangs: Telemetry.chromeHangs,
   697       threadHangStats: this.getThreadHangStats(Telemetry.threadHangStats),
   698       lateWrites: Telemetry.lateWrites,
   699       addonHistograms: this.getAddonHistograms(),
   700       addonDetails: AddonManagerPrivate.getTelemetryDetails(),
   701       UIMeasurements: UITelemetry.getUIMeasurements(),
   702       log: TelemetryLog.entries(),
   703       info: info
   704     };
   706     if (Object.keys(this._slowSQLStartup).length != 0 &&
   707         (Object.keys(this._slowSQLStartup.mainThread).length ||
   708          Object.keys(this._slowSQLStartup.otherThreads).length)) {
   709       payloadObj.slowSQLStartup = this._slowSQLStartup;
   710     }
   712     return payloadObj;
   713   },
   715   getSessionPayload: function getSessionPayload(reason) {
   716     let measurements = this.getSimpleMeasurements(reason == "saved-session");
   717     let info = this.getMetadata(reason);
   718     return this.assemblePayloadWithMeasurements(measurements, info);
   719   },
   721   assemblePing: function assemblePing(payloadObj, reason) {
   722     let slug = this._uuid;
   723     return { slug: slug, reason: reason, payload: payloadObj };
   724   },
   726   getSessionPayloadAndSlug: function getSessionPayloadAndSlug(reason) {
   727     return this.assemblePing(this.getSessionPayload(reason), reason);
   728   },
   730   popPayloads: function popPayloads(reason) {
   731     function payloadIter() {
   732       if (reason != "overdue-flush") {
   733         yield this.getSessionPayloadAndSlug(reason);
   734       }
   735       let iterator = TelemetryFile.popPendingPings(reason);
   736       for (let data of iterator) {
   737         yield data;
   738       }
   739     }
   741     let payloadIterWithThis = payloadIter.bind(this);
   742     return { __iterator__: payloadIterWithThis };
   743   },
   745   /**
   746    * Send data to the server. Record success/send-time in histograms
   747    */
   748   send: function send(reason, server) {
   749     // populate histograms one last time
   750     this.gatherMemory();
   751     return this.sendPingsFromIterator(server, reason,
   752                                Iterator(this.popPayloads(reason)));
   753   },
   755   sendPingsFromIterator: function sendPingsFromIterator(server, reason, i) {
   756     let p = [data for (data in i)].map((data) =>
   757       this.doPing(server, data).then(null, () => TelemetryFile.savePing(data, true)));
   759     return Promise.all(p);
   760   },
   762   finishPingRequest: function finishPingRequest(success, startTime, ping) {
   763     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
   764     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
   766     hsuccess.add(success);
   767     hping.add(new Date() - startTime);
   769     if (success) {
   770       return TelemetryFile.cleanupPingFile(ping);
   771     } else {
   772       return Promise.resolve();
   773     }
   774   },
   776   submissionPath: function submissionPath(ping) {
   777     let slug;
   778     if (!ping) {
   779       slug = this._uuid;
   780     } else {
   781       let info = ping.payload.info;
   782       let pathComponents = [ping.slug, info.reason, info.appName,
   783                             info.appVersion, info.appUpdateChannel,
   784                             info.appBuildID];
   785       slug = pathComponents.join("/");
   786     }
   787     return "/submit/telemetry/" + slug;
   788   },
   790   doPing: function doPing(server, ping) {
   791     let deferred = Promise.defer();
   792     let url = server + this.submissionPath(ping);
   793     let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
   794                   .createInstance(Ci.nsIXMLHttpRequest);
   795     request.mozBackgroundRequest = true;
   796     request.open("POST", url, true);
   797     request.overrideMimeType("text/plain");
   798     request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
   800     let startTime = new Date();
   802     function handler(success) {
   803       return function(event) {
   804         this.finishPingRequest(success, startTime, ping).then(() => {
   805           if (success) {
   806             deferred.resolve();
   807           } else {
   808             deferred.reject(event);
   809           }
   810         });
   811       };
   812     }
   813     request.addEventListener("error", handler(false).bind(this), false);
   814     request.addEventListener("load", handler(true).bind(this), false);
   816     request.setRequestHeader("Content-Encoding", "gzip");
   817     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
   818                     .createInstance(Ci.nsIScriptableUnicodeConverter);
   819     converter.charset = "UTF-8";
   820     let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(ping.payload));
   821     utf8Payload += converter.Finish();
   822     let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
   823                         .createInstance(Ci.nsIStringInputStream);
   824     payloadStream.data = this.gzipCompressString(utf8Payload);
   825     request.send(payloadStream);
   826     return deferred.promise;
   827   },
   829   gzipCompressString: function gzipCompressString(string) {
   830     let observer = {
   831       buffer: "",
   832       onStreamComplete: function(loader, context, status, length, result) {
   833         this.buffer = String.fromCharCode.apply(this, result);
   834       }
   835     };
   837     let scs = Cc["@mozilla.org/streamConverters;1"]
   838               .getService(Ci.nsIStreamConverterService);
   839     let listener = Cc["@mozilla.org/network/stream-loader;1"]
   840                   .createInstance(Ci.nsIStreamLoader);
   841     listener.init(observer);
   842     let converter = scs.asyncConvertData("uncompressed", "gzip",
   843                                          listener, null);
   844     let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
   845                        .createInstance(Ci.nsIStringInputStream);
   846     stringStream.data = string;
   847     converter.onStartRequest(null, null);
   848     converter.onDataAvailable(null, null, stringStream, 0, string.length);
   849     converter.onStopRequest(null, null, null);
   850     return observer.buffer;
   851   },
   853   attachObservers: function attachObservers() {
   854     if (!this._initialized)
   855       return;
   856     Services.obs.addObserver(this, "cycle-collector-begin", false);
   857     Services.obs.addObserver(this, "idle-daily", false);
   858   },
   860   detachObservers: function detachObservers() {
   861     if (!this._initialized)
   862       return;
   863     Services.obs.removeObserver(this, "idle-daily");
   864     Services.obs.removeObserver(this, "cycle-collector-begin");
   865     if (this._isIdleObserver) {
   866       idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
   867       this._isIdleObserver = false;
   868     }
   869   },
   871   /**
   872    * Initializes telemetry within a timer. If there is no PREF_SERVER set, don't turn on telemetry.
   873    */
   874   setup: function setup(aTesting) {
   875     // Initialize some probes that are kept in their own modules
   876     this._thirdPartyCookies = new ThirdPartyCookieProbe();
   877     this._thirdPartyCookies.init();
   879     // Record old value and update build ID preference if this is the first
   880     // run with a new build ID.
   881     let previousBuildID = undefined;
   882     try {
   883       previousBuildID = Services.prefs.getCharPref(PREF_PREVIOUS_BUILDID);
   884     } catch (e) {
   885       // Preference was not set.
   886     }
   887     let thisBuildID = Services.appinfo.appBuildID;
   888     // If there is no previousBuildID preference, this._previousBuildID remains
   889     // undefined so no value is sent in the telemetry metadata.
   890     if (previousBuildID != thisBuildID) {
   891       this._previousBuildID = previousBuildID;
   892       Services.prefs.setCharPref(PREF_PREVIOUS_BUILDID, thisBuildID);
   893     }
   895 #ifdef MOZILLA_OFFICIAL
   896     if (!Telemetry.canSend) {
   897       // We can't send data; no point in initializing observers etc.
   898       // Only do this for official builds so that e.g. developer builds
   899       // still enable Telemetry based on prefs.
   900       Telemetry.canRecord = false;
   901       return;
   902     }
   903 #endif
   904     let enabled = false;
   905     try {
   906       enabled = Services.prefs.getBoolPref(PREF_ENABLED);
   907       this._server = Services.prefs.getCharPref(PREF_SERVER);
   908     } catch (e) {
   909       // Prerequesite prefs aren't set
   910     }
   911     if (!enabled) {
   912       // Turn off local telemetry if telemetry is disabled.
   913       // This may change once about:telemetry is added.
   914       Telemetry.canRecord = false;
   915       return;
   916     }
   918     AsyncShutdown.sendTelemetry.addBlocker(
   919       "Telemetry: shutting down",
   920       function condition(){
   921         this.uninstall();
   922         if (Telemetry.canSend) {
   923           return this.savePendingPings();
   924         }
   925       }.bind(this));
   927     Services.obs.addObserver(this, "sessionstore-windows-restored", false);
   928     Services.obs.addObserver(this, "quit-application-granted", false);
   929 #ifdef MOZ_WIDGET_ANDROID
   930     Services.obs.addObserver(this, "application-background", false);
   931 #endif
   932     Services.obs.addObserver(this, "xul-window-visible", false);
   933     this._hasWindowRestoredObserver = true;
   934     this._hasXulWindowVisibleObserver = true;
   936     // Delay full telemetry initialization to give the browser time to
   937     // run various late initializers. Otherwise our gathered memory
   938     // footprint and other numbers would be too optimistic.
   939     this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   940     let deferred = Promise.defer();
   942     function timerCallback() {
   943       Task.spawn(function*(){
   944         this._initialized = true;
   946         yield TelemetryFile.loadSavedPings();
   947         // If we have any TelemetryPings lying around, we'll be aggressive
   948         // and try to send them all off ASAP.
   949         if (TelemetryFile.pingsOverdue > 0) {
   950           // It doesn't really matter what we pass to this.send as a reason,
   951           // since it's never sent to the server. All that this.send does with
   952           // the reason is check to make sure it's not a test-ping.
   953           yield this.send("overdue-flush", this._server);
   954         }
   956         this.attachObservers();
   957         this.gatherMemory();
   959         Telemetry.asyncFetchTelemetryData(function () {});
   960         delete this._timer;
   961         deferred.resolve();
   962       }.bind(this));
   963     }
   965     this._timer.initWithCallback(timerCallback.bind(this),
   966                                  aTesting ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY,
   967                                  Ci.nsITimer.TYPE_ONE_SHOT);
   968     return deferred.promise;
   969   },
   971   testLoadHistograms: function testLoadHistograms(file) {
   972     return TelemetryFile.testLoadHistograms(file);
   973   },
   975   getFlashVersion: function getFlashVersion() {
   976     let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
   977     let tags = host.getPluginTags();
   979     for (let i = 0; i < tags.length; i++) {
   980       if (tags[i].name == "Shockwave Flash")
   981         return tags[i].version;
   982     }
   984     return null;
   985   },
   987   savePendingPings: function savePendingPings() {
   988     let sessionPing = this.getSessionPayloadAndSlug("saved-session");
   989     return TelemetryFile.savePendingPings(sessionPing);
   990   },
   992   testSaveHistograms: function testSaveHistograms(file) {
   993     return TelemetryFile.savePingToFile(this.getSessionPayloadAndSlug("saved-session"),
   994       file.path, true);
   995   },
   997   /**
   998    * Remove observers to avoid leaks
   999    */
  1000   uninstall: function uninstall() {
  1001     this.detachObservers();
  1002     if (this._hasWindowRestoredObserver) {
  1003       Services.obs.removeObserver(this, "sessionstore-windows-restored");
  1004       this._hasWindowRestoredObserver = false;
  1006     if (this._hasXulWindowVisibleObserver) {
  1007       Services.obs.removeObserver(this, "xul-window-visible");
  1008       this._hasXulWindowVisibleObserver = false;
  1010     Services.obs.removeObserver(this, "quit-application-granted");
  1011 #ifdef MOZ_WIDGET_ANDROID
  1012     Services.obs.removeObserver(this, "application-background", false);
  1013 #endif
  1014   },
  1016   getPayload: function getPayload() {
  1017     // This function returns the current Telemetry payload to the caller.
  1018     // We only gather startup info once.
  1019     if (Object.keys(this._slowSQLStartup).length == 0) {
  1020       this.gatherStartupHistograms();
  1021       this._slowSQLStartup = Telemetry.slowSQL;
  1023     this.gatherMemory();
  1024     return this.getSessionPayload("gather-payload");
  1025   },
  1027   gatherStartup: function gatherStartup() {
  1028     let counters = processInfo.getCounters();
  1029     if (counters) {
  1030       [this._startupIO.startupSessionRestoreReadBytes,
  1031         this._startupIO.startupSessionRestoreWriteBytes] = counters;
  1033     this.gatherStartupHistograms();
  1034     this._slowSQLStartup = Telemetry.slowSQL;
  1035   },
  1037   setAddOns: function setAddOns(aAddOns) {
  1038     this._addons = aAddOns;
  1039   },
  1041   sendIdlePing: function sendIdlePing(aTest, aServer) {
  1042     if (this._isIdleObserver) {
  1043       idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
  1044       this._isIdleObserver = false;
  1046     if (aTest) {
  1047       return this.send("test-ping", aServer);
  1048     } else if (Telemetry.canSend) {
  1049       return this.send("idle-daily", aServer);
  1051   },
  1053   testPing: function testPing(server) {
  1054     return this.sendIdlePing(true, server);
  1055   },
  1057   /**
  1058    * This observer drives telemetry.
  1059    */
  1060   observe: function (aSubject, aTopic, aData) {
  1061     switch (aTopic) {
  1062     case "profile-after-change":
  1063       return this.setup();
  1064     case "cycle-collector-begin":
  1065       let now = new Date();
  1066       if (!gLastMemoryPoll
  1067           || (TELEMETRY_INTERVAL <= now - gLastMemoryPoll)) {
  1068         gLastMemoryPoll = now;
  1069         this.gatherMemory();
  1071       break;
  1072     case "xul-window-visible":
  1073       Services.obs.removeObserver(this, "xul-window-visible");
  1074       this._hasXulWindowVisibleObserver = false;
  1075       var counters = processInfo.getCounters();
  1076       if (counters) {
  1077         [this._startupIO.startupWindowVisibleReadBytes,
  1078           this._startupIO.startupWindowVisibleWriteBytes] = counters;
  1080       break;
  1081     case "sessionstore-windows-restored":
  1082       Services.obs.removeObserver(this, "sessionstore-windows-restored");
  1083       this._hasWindowRestoredObserver = false;
  1084       // Check whether debugger was attached during startup
  1085       let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
  1086       gWasDebuggerAttached = debugService.isDebuggerAttached;
  1087       this.gatherStartup();
  1088       break;
  1089     case "idle-daily":
  1090       // Enqueue to main-thread, otherwise components may be inited by the
  1091       // idle-daily category and miss the gather-telemetry notification.
  1092       Services.tm.mainThread.dispatch((function() {
  1093         // Notify that data should be gathered now, since ping will happen soon.
  1094         Services.obs.notifyObservers(null, "gather-telemetry", null);
  1095         // The ping happens at the first idle of length IDLE_TIMEOUT_SECONDS.
  1096         idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
  1097         this._isIdleObserver = true;
  1098       }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
  1099       break;
  1100     case "idle":
  1101       this.sendIdlePing(false, this._server);
  1102       break;
  1104 #ifdef MOZ_WIDGET_ANDROID
  1105     // On Android, we can get killed without warning once we are in the background,
  1106     // but we may also submit data and/or come back into the foreground without getting
  1107     // killed. To deal with this, we save the current session data to file when we are
  1108     // put into the background. This handles the following post-backgrounding scenarios:
  1109     // 1) We are killed immediately. In this case the current session data (which we
  1110     //    save to a file) will be loaded and submitted on a future run.
  1111     // 2) We submit the data while in the background, and then are killed. In this case
  1112     //    the file that we saved will be deleted by the usual process in
  1113     //    finishPingRequest after it is submitted.
  1114     // 3) We submit the data, and then come back into the foreground. Same as case (2).
  1115     // 4) We do not submit the data, but come back into the foreground. In this case
  1116     //    we have the option of either deleting the file that we saved (since we will either
  1117     //    send the live data while in the foreground, or create the file again on the next
  1118     //    backgrounding), or not (in which case we will delete it on submit, or overwrite
  1119     //    it on the next backgrounding). Not deleting it is faster, so that's what we do.
  1120     case "application-background":
  1121       if (Telemetry.canSend) {
  1122         let ping = this.getSessionPayloadAndSlug("saved-session");
  1123         TelemetryFile.savePing(ping, true);
  1125       break;
  1126 #endif
  1128   },
  1129 };

mercurial