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 +}