toolkit/components/telemetry/TelemetryFile.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/telemetry/TelemetryFile.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,273 @@
     1.4 +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
     1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +"use strict";
    1.10 +
    1.11 +this.EXPORTED_SYMBOLS = ["TelemetryFile"];
    1.12 +
    1.13 +const Cc = Components.classes;
    1.14 +const Ci = Components.interfaces;
    1.15 +const Cr = Components.results;
    1.16 +const Cu = Components.utils;
    1.17 +
    1.18 +Cu.import("resource://gre/modules/Services.jsm", this);
    1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    1.20 +Cu.import("resource://gre/modules/osfile.jsm", this);
    1.21 +Cu.import("resource://gre/modules/Task.jsm", this);
    1.22 +Cu.import("resource://gre/modules/Promise.jsm", this);
    1.23 +
    1.24 +XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated',
    1.25 +  'resource://gre/modules/Deprecated.jsm');
    1.26 +
    1.27 +const Telemetry = Services.telemetry;
    1.28 +
    1.29 +// Files that have been lying around for longer than MAX_PING_FILE_AGE are
    1.30 +// deleted without being loaded.
    1.31 +const MAX_PING_FILE_AGE = 14 * 24 * 60 * 60 * 1000; // 2 weeks
    1.32 +
    1.33 +// Files that are older than OVERDUE_PING_FILE_AGE, but younger than
    1.34 +// MAX_PING_FILE_AGE indicate that we need to send all of our pings ASAP.
    1.35 +const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
    1.36 +
    1.37 +// The number of outstanding saved pings that we have issued loading
    1.38 +// requests for.
    1.39 +let pingsLoaded = 0;
    1.40 +
    1.41 +// The number of pings that we have destroyed due to being older
    1.42 +// than MAX_PING_FILE_AGE.
    1.43 +let pingsDiscarded = 0;
    1.44 +
    1.45 +// The number of pings that are older than OVERDUE_PING_FILE_AGE
    1.46 +// but younger than MAX_PING_FILE_AGE.
    1.47 +let pingsOverdue = 0;
    1.48 +
    1.49 +// Data that has neither been saved nor sent by ping
    1.50 +let pendingPings = [];
    1.51 +
    1.52 +let isPingDirectoryCreated = false;
    1.53 +
    1.54 +this.TelemetryFile = {
    1.55 +
    1.56 +  get MAX_PING_FILE_AGE() {
    1.57 +    return MAX_PING_FILE_AGE;
    1.58 +  },
    1.59 +
    1.60 +  get OVERDUE_PING_FILE_AGE() {
    1.61 +    return OVERDUE_PING_FILE_AGE;
    1.62 +  },
    1.63 +
    1.64 +  get pingDirectoryPath() {
    1.65 +    return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings");
    1.66 +  },
    1.67 +
    1.68 +  /**
    1.69 +   * Save a single ping to a file.
    1.70 +   *
    1.71 +   * @param {object} ping The content of the ping to save.
    1.72 +   * @param {string} file The destination file.
    1.73 +   * @param {bool} overwrite If |true|, the file will be overwritten if it exists,
    1.74 +   * if |false| the file will not be overwritten and no error will be reported if
    1.75 +   * the file exists.
    1.76 +   * @returns {promise}
    1.77 +   */
    1.78 +  savePingToFile: function(ping, file, overwrite) {
    1.79 +    return Task.spawn(function*() {
    1.80 +      try {
    1.81 +        let pingString = JSON.stringify(ping);
    1.82 +        yield OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp",
    1.83 +                                  noOverwrite: !overwrite});
    1.84 +      } catch(e if e.becauseExists) {
    1.85 +      }
    1.86 +    })
    1.87 +  },
    1.88 +
    1.89 +  /**
    1.90 +   * Save a ping to its file.
    1.91 +   *
    1.92 +   * @param {object} ping The content of the ping to save.
    1.93 +   * @param {bool} overwrite If |true|, the file will be overwritten
    1.94 +   * if it exists.
    1.95 +   * @returns {promise}
    1.96 +   */
    1.97 +  savePing: function(ping, overwrite) {
    1.98 +    return Task.spawn(function*() {
    1.99 +      yield getPingDirectory();
   1.100 +      let file = pingFilePath(ping);
   1.101 +      yield this.savePingToFile(ping, file, overwrite);
   1.102 +    }.bind(this));
   1.103 +  },
   1.104 +
   1.105 +  /**
   1.106 +   * Save all pending pings.
   1.107 +   *
   1.108 +   * @param {object} sessionPing The additional session ping.
   1.109 +   * @returns {promise}
   1.110 +   */
   1.111 +  savePendingPings: function(sessionPing) {
   1.112 +    let p = pendingPings.reduce((p, ping) => {
   1.113 +      // Restore the files with the previous pings if for some reason they have
   1.114 +      // been deleted, don't overwrite them otherwise.
   1.115 +      p.push(this.savePing(ping, false));
   1.116 +      return p;}, [this.savePing(sessionPing, true)]);
   1.117 +
   1.118 +    pendingPings = [];
   1.119 +    return Promise.all(p);
   1.120 +  },
   1.121 +
   1.122 +  /**
   1.123 +   * Remove the file for a ping
   1.124 +   *
   1.125 +   * @param {object} ping The ping.
   1.126 +   * @returns {promise}
   1.127 +   */
   1.128 +  cleanupPingFile: function(ping) {
   1.129 +    return OS.File.remove(pingFilePath(ping));
   1.130 +  },
   1.131 +
   1.132 +  /**
   1.133 +   * Load all saved pings.
   1.134 +   *
   1.135 +   * Once loaded, the saved pings can be accessed (destructively only)
   1.136 +   * through |popPendingPings|.
   1.137 +   *
   1.138 +   * @returns {promise}
   1.139 +   */
   1.140 +  loadSavedPings: function() {
   1.141 +    return Task.spawn(function*() {
   1.142 +      let directory = TelemetryFile.pingDirectoryPath;
   1.143 +      let iter = new OS.File.DirectoryIterator(directory);
   1.144 +      let exists = yield iter.exists();
   1.145 +
   1.146 +      if (exists) {
   1.147 +        let entries = yield iter.nextBatch();
   1.148 +        yield iter.close();
   1.149 +
   1.150 +        let p = [e for (e of entries) if (!e.isDir)].
   1.151 +            map((e) => this.loadHistograms(e.path));
   1.152 +
   1.153 +        yield Promise.all(p);
   1.154 +      }
   1.155 +
   1.156 +      yield iter.close();
   1.157 +    }.bind(this));
   1.158 +  },
   1.159 +
   1.160 +  /**
   1.161 +   * Load the histograms from a file.
   1.162 +   *
   1.163 +   * Once loaded, the saved pings can be accessed (destructively only)
   1.164 +   * through |popPendingPings|.
   1.165 +   *
   1.166 +   * @param {string} file The file to load.
   1.167 +   * @returns {promise}
   1.168 +   */
   1.169 +  loadHistograms: function loadHistograms(file) {
   1.170 +    return OS.File.stat(file).then(function(info){
   1.171 +      let now = Date.now();
   1.172 +      if (now - info.lastModificationDate > MAX_PING_FILE_AGE) {
   1.173 +        // We haven't had much luck in sending this file; delete it.
   1.174 +        pingsDiscarded++;
   1.175 +        return OS.File.remove(file);
   1.176 +      }
   1.177 +
   1.178 +      // This file is a bit stale, and overdue for sending.
   1.179 +      if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) {
   1.180 +        pingsOverdue++;
   1.181 +      }
   1.182 +
   1.183 +      pingsLoaded++;
   1.184 +      return addToPendingPings(file);
   1.185 +    });
   1.186 +  },
   1.187 +
   1.188 +  /**
   1.189 +   * The number of pings loaded since the beginning of time.
   1.190 +   */
   1.191 +  get pingsLoaded() {
   1.192 +    return pingsLoaded;
   1.193 +  },
   1.194 +
   1.195 +  /**
   1.196 +   * The number of pings loaded that are older than OVERDUE_PING_FILE_AGE
   1.197 +   * but younger than MAX_PING_FILE_AGE.
   1.198 +   */
   1.199 +  get pingsOverdue() {
   1.200 +    return pingsOverdue;
   1.201 +  },
   1.202 +
   1.203 +  /**
   1.204 +   * The number of pings that we just tossed out for being older than
   1.205 +   * MAX_PING_FILE_AGE.
   1.206 +   */
   1.207 +  get pingsDiscarded() {
   1.208 +    return pingsDiscarded;
   1.209 +  },
   1.210 +
   1.211 +  /**
   1.212 +   * Iterate destructively through the pending pings.
   1.213 +   *
   1.214 +   * @return {iterator}
   1.215 +   */
   1.216 +  popPendingPings: function*(reason) {
   1.217 +    while (pendingPings.length > 0) {
   1.218 +      let data = pendingPings.pop();
   1.219 +      // Send persisted pings to the test URL too.
   1.220 +      if (reason == "test-ping") {
   1.221 +        data.reason = reason;
   1.222 +      }
   1.223 +      yield data;
   1.224 +    }
   1.225 +  },
   1.226 +
   1.227 +  testLoadHistograms: function(file) {
   1.228 +    pingsLoaded = 0;
   1.229 +    return this.loadHistograms(file.path);
   1.230 +  }
   1.231 +};
   1.232 +
   1.233 +///// Utility functions
   1.234 +function pingFilePath(ping) {
   1.235 +  return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
   1.236 +}
   1.237 +
   1.238 +function getPingDirectory() {
   1.239 +  return Task.spawn(function*() {
   1.240 +    let directory = TelemetryFile.pingDirectoryPath;
   1.241 +
   1.242 +    if (!isPingDirectoryCreated) {
   1.243 +      yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
   1.244 +      isPingDirectoryCreated = true;
   1.245 +    }
   1.246 +
   1.247 +    return directory;
   1.248 +  });
   1.249 +}
   1.250 +
   1.251 +function addToPendingPings(file) {
   1.252 +  function onLoad(success) {
   1.253 +    let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
   1.254 +    success_histogram.add(success);
   1.255 +  }
   1.256 +
   1.257 +  return Task.spawn(function*() {
   1.258 +    try {
   1.259 +      let array = yield OS.File.read(file);
   1.260 +      let decoder = new TextDecoder();
   1.261 +      let string = decoder.decode(array);
   1.262 +
   1.263 +      let ping = JSON.parse(string);
   1.264 +      // The ping's payload used to be stringified JSON.  Deal with that.
   1.265 +      if (typeof(ping.payload) == "string") {
   1.266 +        ping.payload = JSON.parse(ping.payload);
   1.267 +      }
   1.268 +
   1.269 +      pendingPings.push(ping);
   1.270 +      onLoad(true);
   1.271 +    } catch (e) {
   1.272 +      onLoad(false);
   1.273 +      yield OS.File.remove(file);
   1.274 +    }
   1.275 +  });
   1.276 +}

mercurial