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

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

mercurial