toolkit/components/telemetry/TelemetryFile.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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 this.EXPORTED_SYMBOLS = ["TelemetryFile"];
    10 const Cc = Components.classes;
    11 const Ci = Components.interfaces;
    12 const Cr = Components.results;
    13 const Cu = Components.utils;
    15 Cu.import("resource://gre/modules/Services.jsm", this);
    16 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    17 Cu.import("resource://gre/modules/osfile.jsm", this);
    18 Cu.import("resource://gre/modules/Task.jsm", this);
    19 Cu.import("resource://gre/modules/Promise.jsm", this);
    21 XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated',
    22   'resource://gre/modules/Deprecated.jsm');
    24 const Telemetry = Services.telemetry;
    26 // Files that have been lying around for longer than MAX_PING_FILE_AGE are
    27 // deleted without being loaded.
    28 const MAX_PING_FILE_AGE = 14 * 24 * 60 * 60 * 1000; // 2 weeks
    30 // Files that are older than OVERDUE_PING_FILE_AGE, but younger than
    31 // MAX_PING_FILE_AGE indicate that we need to send all of our pings ASAP.
    32 const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
    34 // The number of outstanding saved pings that we have issued loading
    35 // requests for.
    36 let pingsLoaded = 0;
    38 // The number of pings that we have destroyed due to being older
    39 // than MAX_PING_FILE_AGE.
    40 let pingsDiscarded = 0;
    42 // The number of pings that are older than OVERDUE_PING_FILE_AGE
    43 // but younger than MAX_PING_FILE_AGE.
    44 let pingsOverdue = 0;
    46 // Data that has neither been saved nor sent by ping
    47 let pendingPings = [];
    49 let isPingDirectoryCreated = false;
    51 this.TelemetryFile = {
    53   get MAX_PING_FILE_AGE() {
    54     return MAX_PING_FILE_AGE;
    55   },
    57   get OVERDUE_PING_FILE_AGE() {
    58     return OVERDUE_PING_FILE_AGE;
    59   },
    61   get pingDirectoryPath() {
    62     return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings");
    63   },
    65   /**
    66    * Save a single ping to a file.
    67    *
    68    * @param {object} ping The content of the ping to save.
    69    * @param {string} file The destination file.
    70    * @param {bool} overwrite If |true|, the file will be overwritten if it exists,
    71    * if |false| the file will not be overwritten and no error will be reported if
    72    * the file exists.
    73    * @returns {promise}
    74    */
    75   savePingToFile: function(ping, file, overwrite) {
    76     return Task.spawn(function*() {
    77       try {
    78         let pingString = JSON.stringify(ping);
    79         yield OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp",
    80                                   noOverwrite: !overwrite});
    81       } catch(e if e.becauseExists) {
    82       }
    83     })
    84   },
    86   /**
    87    * Save a ping to its file.
    88    *
    89    * @param {object} ping The content of the ping to save.
    90    * @param {bool} overwrite If |true|, the file will be overwritten
    91    * if it exists.
    92    * @returns {promise}
    93    */
    94   savePing: function(ping, overwrite) {
    95     return Task.spawn(function*() {
    96       yield getPingDirectory();
    97       let file = pingFilePath(ping);
    98       yield this.savePingToFile(ping, file, overwrite);
    99     }.bind(this));
   100   },
   102   /**
   103    * Save all pending pings.
   104    *
   105    * @param {object} sessionPing The additional session ping.
   106    * @returns {promise}
   107    */
   108   savePendingPings: function(sessionPing) {
   109     let p = pendingPings.reduce((p, ping) => {
   110       // Restore the files with the previous pings if for some reason they have
   111       // been deleted, don't overwrite them otherwise.
   112       p.push(this.savePing(ping, false));
   113       return p;}, [this.savePing(sessionPing, true)]);
   115     pendingPings = [];
   116     return Promise.all(p);
   117   },
   119   /**
   120    * Remove the file for a ping
   121    *
   122    * @param {object} ping The ping.
   123    * @returns {promise}
   124    */
   125   cleanupPingFile: function(ping) {
   126     return OS.File.remove(pingFilePath(ping));
   127   },
   129   /**
   130    * Load all saved pings.
   131    *
   132    * Once loaded, the saved pings can be accessed (destructively only)
   133    * through |popPendingPings|.
   134    *
   135    * @returns {promise}
   136    */
   137   loadSavedPings: function() {
   138     return Task.spawn(function*() {
   139       let directory = TelemetryFile.pingDirectoryPath;
   140       let iter = new OS.File.DirectoryIterator(directory);
   141       let exists = yield iter.exists();
   143       if (exists) {
   144         let entries = yield iter.nextBatch();
   145         yield iter.close();
   147         let p = [e for (e of entries) if (!e.isDir)].
   148             map((e) => this.loadHistograms(e.path));
   150         yield Promise.all(p);
   151       }
   153       yield iter.close();
   154     }.bind(this));
   155   },
   157   /**
   158    * Load the histograms from a file.
   159    *
   160    * Once loaded, the saved pings can be accessed (destructively only)
   161    * through |popPendingPings|.
   162    *
   163    * @param {string} file The file to load.
   164    * @returns {promise}
   165    */
   166   loadHistograms: function loadHistograms(file) {
   167     return OS.File.stat(file).then(function(info){
   168       let now = Date.now();
   169       if (now - info.lastModificationDate > MAX_PING_FILE_AGE) {
   170         // We haven't had much luck in sending this file; delete it.
   171         pingsDiscarded++;
   172         return OS.File.remove(file);
   173       }
   175       // This file is a bit stale, and overdue for sending.
   176       if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) {
   177         pingsOverdue++;
   178       }
   180       pingsLoaded++;
   181       return addToPendingPings(file);
   182     });
   183   },
   185   /**
   186    * The number of pings loaded since the beginning of time.
   187    */
   188   get pingsLoaded() {
   189     return pingsLoaded;
   190   },
   192   /**
   193    * The number of pings loaded that are older than OVERDUE_PING_FILE_AGE
   194    * but younger than MAX_PING_FILE_AGE.
   195    */
   196   get pingsOverdue() {
   197     return pingsOverdue;
   198   },
   200   /**
   201    * The number of pings that we just tossed out for being older than
   202    * MAX_PING_FILE_AGE.
   203    */
   204   get pingsDiscarded() {
   205     return pingsDiscarded;
   206   },
   208   /**
   209    * Iterate destructively through the pending pings.
   210    *
   211    * @return {iterator}
   212    */
   213   popPendingPings: function*(reason) {
   214     while (pendingPings.length > 0) {
   215       let data = pendingPings.pop();
   216       // Send persisted pings to the test URL too.
   217       if (reason == "test-ping") {
   218         data.reason = reason;
   219       }
   220       yield data;
   221     }
   222   },
   224   testLoadHistograms: function(file) {
   225     pingsLoaded = 0;
   226     return this.loadHistograms(file.path);
   227   }
   228 };
   230 ///// Utility functions
   231 function pingFilePath(ping) {
   232   return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
   233 }
   235 function getPingDirectory() {
   236   return Task.spawn(function*() {
   237     let directory = TelemetryFile.pingDirectoryPath;
   239     if (!isPingDirectoryCreated) {
   240       yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
   241       isPingDirectoryCreated = true;
   242     }
   244     return directory;
   245   });
   246 }
   248 function addToPendingPings(file) {
   249   function onLoad(success) {
   250     let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
   251     success_histogram.add(success);
   252   }
   254   return Task.spawn(function*() {
   255     try {
   256       let array = yield OS.File.read(file);
   257       let decoder = new TextDecoder();
   258       let string = decoder.decode(array);
   260       let ping = JSON.parse(string);
   261       // The ping's payload used to be stringified JSON.  Deal with that.
   262       if (typeof(ping.payload) == "string") {
   263         ping.payload = JSON.parse(ping.payload);
   264       }
   266       pendingPings.push(ping);
   267       onLoad(true);
   268     } catch (e) {
   269       onLoad(false);
   270       yield OS.File.remove(file);
   271     }
   272   });
   273 }

mercurial