Bug 839794 - Use OS.File in Telemetry. r=Yoric
☠☠ backed out by 681f9c28ed07 ☠ ☠
authorRoberto A. Vitillo <rvitillo@mozilla.com>
Thu, 23 Jan 2014 17:47:53 +0000
changeset 181401 bafe9571f3e8e9c0f17248554fc19eeaea70e171
parent 181400 77540823c0577dacb35a23d2c0abc564a781f62b
child 181402 6dc4806fa8a2669d8d80d10d6bda30fb293097e7
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersYoric
bugs839794
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 839794 - Use OS.File in Telemetry. r=Yoric
toolkit/components/telemetry/TelemetryFile.jsm
toolkit/components/telemetry/TelemetryPing.jsm
toolkit/components/telemetry/tests/unit/test_TelemetryPing.js
toolkit/components/telemetry/tests/unit/test_TelemetryPingBuildID.js
toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
--- a/toolkit/components/telemetry/TelemetryFile.jsm
+++ b/toolkit/components/telemetry/TelemetryFile.jsm
@@ -1,231 +1,179 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["TelemetryFile"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-let imports = {};
-Cu.import("resource://gre/modules/Services.jsm", imports);
-Cu.import("resource://gre/modules/Deprecated.jsm", imports);
-Cu.import("resource://gre/modules/NetUtil.jsm", imports);
-
-let {Services, Deprecated, NetUtil} = imports;
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Deprecated.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
 
-// Constants from prio.h for nsIFileOutputStream.init
-const PR_WRONLY = 0x2;
-const PR_CREATE_FILE = 0x8;
-const PR_TRUNCATE = 0x20;
-const PR_EXCL = 0x80;
-const RW_OWNER = parseInt("0600", 8);
-const RWX_OWNER = parseInt("0700", 8);
+const Telemetry = Services.telemetry;
 
 // Files that have been lying around for longer than MAX_PING_FILE_AGE are
 // deleted without being loaded.
 const MAX_PING_FILE_AGE = 14 * 24 * 60 * 60 * 1000; // 2 weeks
 
 // Files that are older than OVERDUE_PING_FILE_AGE, but younger than
 // MAX_PING_FILE_AGE indicate that we need to send all of our pings ASAP.
 const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
 
 // The number of outstanding saved pings that we have issued loading
 // requests for.
 let pingsLoaded = 0;
 
-// The number of those requests that have actually completed.
-let pingLoadsCompleted = 0;
-
 // The number of pings that we have destroyed due to being older
 // than MAX_PING_FILE_AGE.
 let pingsDiscarded = 0;
 
 // The number of pings that are older than OVERDUE_PING_FILE_AGE
 // but younger than MAX_PING_FILE_AGE.
 let pingsOverdue = 0;
 
-// If |true|, send notifications "telemetry-test-save-complete"
-// and "telemetry-test-load-complete" once save/load is complete.
-let shouldNotifyUponSave = false;
-
 // Data that has neither been saved nor sent by ping
 let pendingPings = [];
 
+let isPingDirectoryCreated = false;
+
 this.TelemetryFile = {
 
   get MAX_PING_FILE_AGE() {
     return MAX_PING_FILE_AGE;
   },
 
   get OVERDUE_PING_FILE_AGE() {
     return OVERDUE_PING_FILE_AGE;
   },
 
+  get pingDirectoryPath() {
+    return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings");
+  },
+
   /**
    * Save a single ping to a file.
    *
    * @param {object} ping The content of the ping to save.
-   * @param {nsIFile} file The destination file.
-   * @param {bool} sync If |true|, write synchronously. Deprecated.
-   * This argument should be |false|.
+   * @param {string} file The destination file.
    * @param {bool} overwrite If |true|, the file will be overwritten
    * if it exists.
+   * @returns {promise}
    */
-  savePingToFile: function(ping, file, sync, overwrite) {
+  savePingToFile: function(ping, file, overwrite) {
     let pingString = JSON.stringify(ping);
-
-    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
-                    .createInstance(Ci.nsIScriptableUnicodeConverter);
-    converter.charset = "UTF-8";
-
-    let ostream = Cc["@mozilla.org/network/file-output-stream;1"]
-                  .createInstance(Ci.nsIFileOutputStream);
-    let initFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE;
-    if (!overwrite) {
-      initFlags |= PR_EXCL;
-    }
-    try {
-      ostream.init(file, initFlags, RW_OWNER, 0);
-    } catch (e) {
-      // Probably due to PR_EXCL.
-      return;
-    }
-
-    if (sync) {
-      let utf8String = converter.ConvertFromUnicode(pingString);
-      utf8String += converter.Finish();
-      let success = false;
-      try {
-        let amount = ostream.write(utf8String, utf8String.length);
-        success = amount == utf8String.length;
-      } catch (e) {
-      }
-      finishTelemetrySave(success, ostream);
-    } else {
-      let istream = converter.convertToInputStream(pingString);
-      let self = this;
-      NetUtil.asyncCopy(istream, ostream,
-                        function(result) {
-                          finishTelemetrySave(Components.isSuccessCode(result),
-                              ostream);
-                        });
-    }
+    return OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp",
+                                noOverwrite: !overwrite});
   },
 
   /**
-   * Save a ping to its file, synchronously.
+   * Save a ping to its file.
    *
    * @param {object} ping The content of the ping to save.
    * @param {bool} overwrite If |true|, the file will be overwritten
    * if it exists.
+   * @returns {promise}
    */
   savePing: function(ping, overwrite) {
-    this.savePingToFile(ping,
-      getSaveFileForPing(ping), true, overwrite);
+    return Task.spawn(function*() {
+      yield getPingDirectory();
+      let file = pingFilePath(ping);
+      return this.savePingToFile(ping, file, overwrite);
+    }.bind(this));
   },
 
   /**
-   * Save all pending pings, synchronously.
+   * Save all pending pings.
    *
    * @param {object} sessionPing The additional session ping.
+   * @returns {promise}
    */
   savePendingPings: function(sessionPing) {
-    this.savePing(sessionPing, true);
-    pendingPings.forEach(function sppcb(e, i, a) {
-      this.savePing(e, false);
-    }, this);
+    let p = pendingPings.reduce((p, ping) => {
+      p.push(this.savePing(ping, false));
+      return p;}, [this.savePing(sessionPing, true)]);
+
     pendingPings = [];
+    return Promise.all(p);
   },
 
   /**
    * Remove the file for a ping
    *
    * @param {object} ping The ping.
+   * @returns {promise}
    */
   cleanupPingFile: function(ping) {
-    // FIXME: We shouldn't create the directory just to remove the file.
-    let file = getSaveFileForPing(ping);
-    try {
-      file.remove(true); // FIXME: Should be |false|, isn't it?
-    } catch(e) {
-    }
+    return OS.File.remove(pingFilePath(ping));
   },
 
   /**
    * Load all saved pings.
    *
    * Once loaded, the saved pings can be accessed (destructively only)
    * through |popPendingPings|.
    *
-   * @param {bool} sync If |true|, loading takes place synchronously.
-   * @param {function*} onLoad A function called upon loading of each
-   * ping. It is passed |true| in case of success, |false| in case of
-   * format error.
+   * @returns {promise}
    */
-  loadSavedPings: function(sync, onLoad = null, onDone = null) {
-    let directory = ensurePingDirectory();
-    let entries = directory.directoryEntries
-                           .QueryInterface(Ci.nsIDirectoryEnumerator);
-    pingsLoaded = 0;
-    pingLoadsCompleted = 0;
-    try {
-      while (entries.hasMoreElements()) {
-        this.loadHistograms(entries.nextFile, sync, onLoad, onDone);
+  loadSavedPings: function() {
+    return Task.spawn(function*() {
+      let directory = TelemetryFile.pingDirectoryPath;
+      let iter = new OS.File.DirectoryIterator(directory);
+      let exists = yield iter.exists();
+
+      if (exists) {
+        let entries = yield iter.nextBatch();
+        yield iter.close();
+
+        let p = [e for (e of entries) if (!e.isDir)].
+            map((e) => this.loadHistograms(e.path));
+
+        yield Promise.all(p);
       }
-    } finally {
-      entries.close();
-    }
+
+      yield iter.close();
+    }.bind(this));
   },
 
   /**
    * Load the histograms from a file.
    *
    * Once loaded, the saved pings can be accessed (destructively only)
    * through |popPendingPings|.
    *
-   * @param {nsIFile} file The file to load.
-   * @param {bool} sync If |true|, loading takes place synchronously.
-   * @param {function*} onLoad A function called upon loading of the
-   * ping. It is passed |true| in case of success, |false| in case of
-   * format error.
+   * @param {string} file The file to load.
+   * @returns {promise}
    */
-  loadHistograms: function loadHistograms(file, sync, onLoad = null, onDone = null) {
-    let now = Date.now();
-    if (now - file.lastModifiedTime > MAX_PING_FILE_AGE) {
-      // We haven't had much luck in sending this file; delete it.
-      file.remove(true);
-      pingsDiscarded++;
-      return;
-    }
+  loadHistograms: function loadHistograms(file) {
+    return OS.File.stat(file).then(function(info){
+      let now = Date.now();
+      if (now - info.lastModificationDate > MAX_PING_FILE_AGE) {
+        // We haven't had much luck in sending this file; delete it.
+        pingsDiscarded++;
+        return OS.File.remove(file);
+      }
 
-    // This file is a bit stale, and overdue for sending.
-    if (now - file.lastModifiedTime > OVERDUE_PING_FILE_AGE) {
-      pingsOverdue++;
-    }
+      // This file is a bit stale, and overdue for sending.
+      if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) {
+        pingsOverdue++;
+      }
 
-    pingsLoaded++;
-    if (sync) {
-      let stream = Cc["@mozilla.org/network/file-input-stream;1"]
-                   .createInstance(Ci.nsIFileInputStream);
-      stream.init(file, -1, -1, 0);
-      addToPendingPings(file, stream, onLoad, onDone);
-    } else {
-      let channel = NetUtil.newChannel(file);
-      channel.contentType = "application/json";
-
-      NetUtil.asyncFetch(channel, (function(stream, result) {
-        if (!Components.isSuccessCode(result)) {
-          return;
-        }
-        addToPendingPings(file, stream, onLoad, onDone);
-      }).bind(this));
-    }
+      pingsLoaded++;
+      return addToPendingPings(file);
+    });
   },
 
   /**
    * The number of pings loaded since the beginning of time.
    */
   get pingsLoaded() {
     return pingsLoaded;
   },
@@ -246,90 +194,69 @@ this.TelemetryFile = {
     return pingsDiscarded;
   },
 
   /**
    * Iterate destructively through the pending pings.
    *
    * @return {iterator}
    */
-  popPendingPings: function(reason) {
+  popPendingPings: function*(reason) {
     while (pendingPings.length > 0) {
       let data = pendingPings.pop();
       // Send persisted pings to the test URL too.
       if (reason == "test-ping") {
         data.reason = reason;
       }
       yield data;
     }
   },
 
-  set shouldNotifyUponSave(value) {
-    shouldNotifyUponSave = value;
-  },
-
-  testLoadHistograms: function(file, sync, onLoad) {
+  testLoadHistograms: function(file) {
     pingsLoaded = 0;
-    pingLoadsCompleted = 0;
-    this.loadHistograms(file, sync, onLoad);
+    return this.loadHistograms(file.path);
   }
 };
 
 ///// Utility functions
-
-function getSaveFileForPing(ping) {
-  let file = ensurePingDirectory();
-  file.append(ping.slug);
-  return file;
-};
+function pingFilePath(ping) {
+  return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
+}
 
-function ensurePingDirectory() {
-  let directory = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone();
-  directory.append("saved-telemetry-pings");
-  try {
-    directory.create(Ci.nsIFile.DIRECTORY_TYPE, RWX_OWNER);
-  } catch (e) {
-    // Already exists, just ignore this.
-  }
-  return directory;
-};
+function getPingDirectory() {
+  return Task.spawn(function*() {
+    let directory = TelemetryFile.pingDirectoryPath;
 
-function addToPendingPings(file, stream, onLoad, onDone) {
-  let success = false;
+    if (!isPingDirectoryCreated) {
+      yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
+      isPingDirectoryCreated = true;
+    }
 
-  try {
-    let string = NetUtil.readInputStreamToString(stream, stream.available(),
-      { charset: "UTF-8" });
-    stream.close();
-    let ping = JSON.parse(string);
-    // The ping's payload used to be stringified JSON.  Deal with that.
-    if (typeof(ping.payload) == "string") {
-      ping.payload = JSON.parse(ping.payload);
-    }
-    pingLoadsCompleted++;
-    pendingPings.push(ping);
-    success = true;
-  } catch (e) {
-    // An error reading the file, or an error parsing the contents.
-    stream.close();           // close is idempotent.
-    file.remove(true); // FIXME: Should be false, isn't it?
+    return directory;
+  });
+}
+
+function addToPendingPings(file) {
+  function onLoad(success) {
+    let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
+    success_histogram.add(success);
   }
 
-  if (onLoad) {
-    onLoad(success);
-  }
+  return Task.spawn(function*() {
+    try {
+      let array = yield OS.File.read(file);
+      let decoder = new TextDecoder();
+      let string = decoder.decode(array);
 
-  if (pingLoadsCompleted == pingsLoaded) {
-    if (onDone) {
-      onDone();
+      let ping = JSON.parse(string);
+      // The ping's payload used to be stringified JSON.  Deal with that.
+      if (typeof(ping.payload) == "string") {
+        ping.payload = JSON.parse(ping.payload);
+      }
+
+      pendingPings.push(ping);
+      onLoad(true);
+    } catch (e) {
+      onLoad(false);
+      yield OS.File.remove(file);
     }
-    if (shouldNotifyUponSave) {
-      Services.obs.notifyObservers(null, "telemetry-test-load-complete", null);
-    }
-  }
-};
-
-function finishTelemetrySave(ok, stream) {
-  stream.close();
-  if (shouldNotifyUponSave && ok) {
-    Services.obs.notifyObservers(null, "telemetry-test-save-complete", null);
-  }
-};
+  });
+}
--- a/toolkit/components/telemetry/TelemetryPing.jsm
+++ b/toolkit/components/telemetry/TelemetryPing.jsm
@@ -5,23 +5,25 @@
 
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/debug.js");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/debug.js", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 #ifndef MOZ_WIDGET_GONK
-Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
+Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
 #endif
-Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm");
+Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
 
 // When modifying the payload in incompatible ways, please bump this version number
 const PAYLOAD_VERSION = 1;
 
 // This is the HG changeset of the Histogram.json file, used to associate
 // submitted ping data with its histogram definition (bug 832007)
 #expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__";
 
@@ -122,70 +124,95 @@ let processInfo = {
       return null;
     return [parseInt(io.readBytes), parseInt(io.writeBytes)];
   }
 };
 
 this.EXPORTED_SYMBOLS = ["TelemetryPing"];
 
 this.TelemetryPing = Object.freeze({
+  /**
+   * Returns the current telemetry payload.
+   * @returns Object
+   */
   getPayload: function() {
     return Impl.getPayload();
   },
-  saveHistograms: function(aFile, aSync) {
-    return Impl.saveHistograms(aFile, aSync);
+  /**
+   * Save histograms to a file.
+   * Used only for testing purposes.
+   *
+   * @param {nsIFile} aFile The file to load from.
+   */
+  testSaveHistograms: function(aFile) {
+    return Impl.testSaveHistograms(aFile);
   },
+  /**
+   * Collect and store information about startup.
+   */
   gatherStartup: function() {
     return Impl.gatherStartup();
   },
-  enableLoadSaveNotifications: function() {
-    return Impl.enableLoadSaveNotifications();
-  },
-  cacheProfileDirectory: function() {
-    return Impl.cacheProfileDirectory();
-  },
+  /**
+   * Inform the ping which AddOns are installed.
+   *
+   * @param aAddOns - The AddOns.
+   */
   setAddOns: function(aAddOns) {
     return Impl.setAddOns(aAddOns);
   },
+  /**
+   * Send a ping to a test server. Used only for testing.
+   *
+   * @param aServer - The server.
+   */
   testPing: function(aServer) {
     return Impl.testPing(aServer);
   },
-  testLoadHistograms: function(aFile, aSync) {
-    return Impl.testLoadHistograms(aFile, aSync);
+  /**
+   * Load histograms from a file.
+   * Used only for testing purposes.
+   *
+   * @param aFile - File to load from.
+   */
+  testLoadHistograms: function(aFile) {
+    return Impl.testLoadHistograms(aFile);
   },
+  /**
+   * Returns the path component of the current submission URL.
+   * @returns String
+   */
   submissionPath: function() {
     return Impl.submissionPath();
   },
   Constants: Object.freeze({
     PREF_ENABLED: PREF_ENABLED,
     PREF_SERVER: PREF_SERVER,
     PREF_PREVIOUS_BUILDID: PREF_PREVIOUS_BUILDID,
   }),
   /**
    * Used only for testing purposes.
    */
   reset: function() {
-    this.uninstall();
-    this.setup();
+    return Task.spawn(function*(){
+      yield this.uninstall();
+      yield this.setup();
+    }.bind(this));
   },
   /**
    * Used only for testing purposes.
    */
   setup: function() {
-    Impl.setup(true);
+    return Impl.setup(true);
   },
   /**
    * Used only for testing purposes.
    */
   uninstall: function() {
-    try {
-      Impl.uninstall();
-    } catch (ex) {
-      // Ignore errors
-    }
+    return Impl.uninstall();
   },
   /**
    * Descriptive metadata
    *
    * @param  reason
    *         The reason for the telemetry ping, this will be included in the
    *         returned metadata,
    * @return The metadata as a JS object
@@ -694,67 +721,38 @@ let Impl = {
   },
 
   /**
    * Send data to the server. Record success/send-time in histograms
    */
   send: function send(reason, server) {
     // populate histograms one last time
     this.gatherMemory();
-    this.sendPingsFromIterator(server, reason,
+    return this.sendPingsFromIterator(server, reason,
                                Iterator(this.popPayloads(reason)));
   },
 
-  /**
-   * What we want to do is the following:
-   *
-   * for data in getPayloads(reason):
-   *   if sending ping data to server failed:
-   *     break;
-   *
-   * but we can't do that, since XMLHttpRequest is async.  What we do
-   * instead is let this function control the essential looping logic
-   * and provide callbacks for XMLHttpRequest when a request has
-   * finished.
-   */
   sendPingsFromIterator: function sendPingsFromIterator(server, reason, i) {
-    function finishPings(reason) {
-      if (reason == "test-ping") {
-        Services.obs.notifyObservers(null, "telemetry-test-xhr-complete", null);
-      }
-    }
+    let p = [data for (data in i)].map((data) =>
+      this.doPing(server, data).then(null, () => TelemetryFile.savePing(data, true)));
 
-    let data = null;
-    try {
-      data = i.next();
-    } catch (e if e instanceof StopIteration) {
-      finishPings(reason);
-      return;
-    }
-    function onSuccess() {
-      this.sendPingsFromIterator(server, reason, i);
-    }
-    function onError() {
-      TelemetryFile.savePing(data, true);
-      // Notify that testing is complete, even if we didn't send everything.
-      finishPings(reason);
-    }
-    this.doPing(server, data,
-                onSuccess.bind(this), onError.bind(this));
+    return Promise.all(p);
   },
 
   finishPingRequest: function finishPingRequest(success, startTime, ping) {
     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
 
     hsuccess.add(success);
     hping.add(new Date() - startTime);
 
     if (success) {
-      TelemetryFile.cleanupPingFile(ping);
+      return TelemetryFile.cleanupPingFile(ping);
+    } else {
+      return Promise.resolve();
     }
   },
 
   submissionPath: function submissionPath(ping) {
     let slug;
     if (!ping) {
       slug = this._uuid;
     } else {
@@ -762,46 +760,53 @@ let Impl = {
       let pathComponents = [ping.slug, info.reason, info.appName,
                             info.appVersion, info.appUpdateChannel,
                             info.appBuildID];
       slug = pathComponents.join("/");
     }
     return "/submit/telemetry/" + slug;
   },
 
-  doPing: function doPing(server, ping, onSuccess, onError) {
+  doPing: function doPing(server, ping) {
+    let deferred = Promise.defer();
     let url = server + this.submissionPath(ping);
     let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                   .createInstance(Ci.nsIXMLHttpRequest);
     request.mozBackgroundRequest = true;
     request.open("POST", url, true);
     request.overrideMimeType("text/plain");
     request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
 
     let startTime = new Date();
 
-    function handler(success, callback) {
+    function handler(success) {
       return function(event) {
         this.finishPingRequest(success, startTime, ping);
-        callback();
+
+        if (success) {
+          deferred.resolve();
+        } else {
+          deferred.reject(event);
+        }
       };
     }
-    request.addEventListener("error", handler(false, onError).bind(this), false);
-    request.addEventListener("load", handler(true, onSuccess).bind(this), false);
+    request.addEventListener("error", handler(false).bind(this), false);
+    request.addEventListener("load", handler(true).bind(this), false);
 
     request.setRequestHeader("Content-Encoding", "gzip");
     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                     .createInstance(Ci.nsIScriptableUnicodeConverter);
     converter.charset = "UTF-8";
     let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(ping.payload));
     utf8Payload += converter.Finish();
     let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     payloadStream.data = this.gzipCompressString(utf8Payload);
     request.send(payloadStream);
+    return deferred.promise;
   },
 
   gzipCompressString: function gzipCompressString(string) {
     let observer = {
       buffer: "",
       onStreamComplete: function(loader, context, status, length, result) {
         this.buffer = String.fromCharCode.apply(this, result);
       }
@@ -896,74 +901,71 @@ let Impl = {
     Services.obs.addObserver(this, "xul-window-visible", false);
     this._hasWindowRestoredObserver = true;
     this._hasXulWindowVisibleObserver = true;
 
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
     // footprint and other numbers would be too optimistic.
     this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    let deferred = Promise.defer();
+
     function timerCallback() {
-      this._initialized = true;
-      TelemetryFile.loadSavedPings(false, (success =>
-        {
-          let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
-          success_histogram.add(success);
-        }), () =>
-        {
-          // If we have any TelemetryPings lying around, we'll be aggressive
-          // and try to send them all off ASAP.
-          if (TelemetryFile.pingsOverdue > 0) {
-            // It doesn't really matter what we pass to this.send as a reason,
-            // since it's never sent to the server. All that this.send does with
-            // the reason is check to make sure it's not a test-ping.
-            this.send("overdue-flush", this._server);
-          }
-        });
-      this.attachObservers();
-      this.gatherMemory();
+      Task.spawn(function*(){
+        this._initialized = true;
 
-      Telemetry.asyncFetchTelemetryData(function () {
-      });
-      delete this._timer;
+        yield TelemetryFile.loadSavedPings();
+        // If we have any TelemetryPings lying around, we'll be aggressive
+        // and try to send them all off ASAP.
+        if (TelemetryFile.pingsOverdue > 0) {
+          // It doesn't really matter what we pass to this.send as a reason,
+          // since it's never sent to the server. All that this.send does with
+          // the reason is check to make sure it's not a test-ping.
+          yield this.send("overdue-flush", this._server);
+        }
+
+        this.attachObservers();
+        this.gatherMemory();
+
+        Telemetry.asyncFetchTelemetryData(function () {});
+        delete this._timer;
+        deferred.resolve();
+      }.bind(this));
     }
+
     this._timer.initWithCallback(timerCallback.bind(this),
                                  aTesting ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY,
                                  Ci.nsITimer.TYPE_ONE_SHOT);
+    return deferred.promise;
   },
 
-  testLoadHistograms: function testLoadHistograms(file, sync) {
-    TelemetryFile.testLoadHistograms(file, sync, (success =>
-        {
-          let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
-          success_histogram.add(success);
-        }));
+  testLoadHistograms: function testLoadHistograms(file) {
+    return TelemetryFile.testLoadHistograms(file);
   },
 
   getFlashVersion: function getFlashVersion() {
     let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
     let tags = host.getPluginTags();
 
     for (let i = 0; i < tags.length; i++) {
       if (tags[i].name == "Shockwave Flash")
         return tags[i].version;
     }
 
     return null;
   },
 
   savePendingPings: function savePendingPings() {
     let sessionPing = this.getSessionPayloadAndSlug("saved-session");
-    TelemetryFile.savePendingPings(sessionPing);
+    return TelemetryFile.savePendingPings(sessionPing);
   },
 
-  saveHistograms: function saveHistograms(file, sync) {
-    TelemetryFile.savePingToFile(
-      this.getSessionPayloadAndSlug("saved-session"),
-      file, sync, true);
+  testSaveHistograms: function testSaveHistograms(file) {
+    return TelemetryFile.savePingToFile(this.getSessionPayloadAndSlug("saved-session"),
+      file.path, true);
   },
 
   /**
    * Remove observers to avoid leaks
    */
   uninstall: function uninstall() {
     this.detachObservers();
     if (this._hasWindowRestoredObserver) {
@@ -974,16 +976,21 @@ let Impl = {
       Services.obs.removeObserver(this, "xul-window-visible");
       this._hasXulWindowVisibleObserver = false;
     }
     Services.obs.removeObserver(this, "profile-before-change2");
     Services.obs.removeObserver(this, "quit-application-granted");
 #ifdef MOZ_WIDGET_ANDROID
     Services.obs.removeObserver(this, "application-background", false);
 #endif
+    if (Telemetry.canSend) {
+      return this.savePendingPings();
+    } else {
+      Promise.resolve();
+    }
   },
 
   getPayload: function getPayload() {
     // This function returns the current Telemetry payload to the caller.
     // We only gather startup info once.
     if (Object.keys(this._slowSQLStartup).length == 0) {
       this.gatherStartupHistograms();
       this._slowSQLStartup = Telemetry.slowSQL;
@@ -997,43 +1004,34 @@ let Impl = {
     if (counters) {
       [this._startupIO.startupSessionRestoreReadBytes,
         this._startupIO.startupSessionRestoreWriteBytes] = counters;
     }
     this.gatherStartupHistograms();
     this._slowSQLStartup = Telemetry.slowSQL;
   },
 
-  enableLoadSaveNotifications: function enableLoadSaveNotifications() {
-    TelemetryFile.shouldNotifyUponSave = true;
-  },
-
   setAddOns: function setAddOns(aAddOns) {
     this._addons = aAddOns;
   },
 
   sendIdlePing: function sendIdlePing(aTest, aServer) {
     if (this._isIdleObserver) {
       idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
       this._isIdleObserver = false;
     }
     if (aTest) {
-      this.send("test-ping", aServer);
+      return this.send("test-ping", aServer);
     } else if (Telemetry.canSend) {
-      this.send("idle-daily", aServer);
+      return this.send("idle-daily", aServer);
     }
   },
 
   testPing: function testPing(server) {
-    this.sendIdlePing(true, server);
-  },
-
-  cacheProfileDirectory: function cacheProfileDirectory() {
-    // This method doesn't do anything anymore
-    return;
+    return this.sendIdlePing(true, server);
   },
 
   /**
    * This observer drives telemetry.
    */
   observe: function (aSubject, aTopic, aData) {
     switch (aTopic) {
     case "profile-after-change":
@@ -1075,19 +1073,16 @@ let Impl = {
         this._isIdleObserver = true;
       }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
       break;
     case "idle":
       this.sendIdlePing(false, this._server);
       break;
     case "profile-before-change2":
       this.uninstall();
-      if (Telemetry.canSend) {
-        this.savePendingPings();
-      }
       break;
 
 #ifdef MOZ_WIDGET_ANDROID
     // On Android, we can get killed without warning once we are in the background,
     // but we may also submit data and/or come back into the foreground without getting
     // killed. To deal with this, we save the current session data to file when we are
     // put into the background. This handles the following post-backgrounding scenarios:
     // 1) We are killed immediately. In this case the current session data (which we
@@ -1104,10 +1099,10 @@ let Impl = {
     case "application-background":
       if (Telemetry.canSend) {
         let ping = this.getSessionPayloadAndSlug("saved-session");
         TelemetryFile.savePing(ping, true);
       }
       break;
 #endif
     }
-  }
+  },
 };
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryPing.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryPing.js
@@ -8,21 +8,23 @@
  * checked in the second request.
  */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
-Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/httpd.js", this);
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/TelemetryPing.jsm");
+Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
 
 const IGNORE_HISTOGRAM = "test::ignore_me";
 const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED";
 const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also";
 const ADDON_NAME = "Telemetry test addon";
 const ADDON_HISTOGRAM = "addon-histogram";
 // Add some unicode characters here to ensure that sending them works correctly.
 const FLASH_VERSION = "\u201c1.1.1.1\u201d";
@@ -31,87 +33,51 @@ const FAILED_PROFILE_LOCK_ATTEMPTS = 2;
 
 // Constants from prio.h for nsIFileOutputStream.init
 const PR_WRONLY = 0x2;
 const PR_CREATE_FILE = 0x8;
 const PR_TRUNCATE = 0x20;
 const RW_OWNER = 0600;
 
 const NUMBER_OF_THREADS_TO_LAUNCH = 30;
-var gNumberOfThreadsLaunched = 0;
+let gNumberOfThreadsLaunched = 0;
 
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
 
-var httpserver = new HttpServer();
-var serverStarted = false;
-var gFinished = false;
-
-function test_expired_histogram() {
-  var histogram_id = "FOOBAR";
-  var dummy = Telemetry.newHistogram(histogram_id, "30", 1, 2, 3, Telemetry.HISTOGRAM_EXPONENTIAL);
-
-  dummy.add(1);
+let gHttpServer = new HttpServer();
+let gServerStarted = false;
+let gRequestIterator = null;
 
-  do_check_eq(TelemetryPing.getPayload()["histograms"][histogram_id], undefined);
-  do_check_eq(TelemetryPing.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
-}
-
-function telemetry_ping () {
+function sendPing () {
   TelemetryPing.gatherStartup();
-  TelemetryPing.enableLoadSaveNotifications();
-  TelemetryPing.cacheProfileDirectory();
-  if (serverStarted) {
-    TelemetryPing.testPing("http://localhost:" + httpserver.identity.primaryPort);
+  if (gServerStarted) {
+    return TelemetryPing.testPing("http://localhost:" + gHttpServer.identity.primaryPort);
   } else {
-    TelemetryPing.testPing("http://doesnotexist");
+    return TelemetryPing.testPing("http://doesnotexist");
   }
 }
 
-// Mostly useful so that you can dump payloads from decodeRequestPayload.
-function dummyHandler(request, response) {
-  let p = decodeRequestPayload(request);
-  return p;
-}
-
 function wrapWithExceptionHandler(f) {
-  function wrapper() {
+  function wrapper(...args) {
     try {
-      f.apply(null, arguments);
+      f(...args);
     } catch (ex if typeof(ex) == 'object') {
       dump("Caught exception: " + ex.message + "\n");
       dump(ex.stack);
       do_test_finished();
     }
   }
   return wrapper;
 }
 
-function addWrappedObserver(f, topic) {
-  let wrappedObserver = wrapWithExceptionHandler(f);
-  Services.obs.addObserver(function g(aSubject, aTopic, aData) {
-    Services.obs.removeObserver(g, aTopic);
-    wrappedObserver(aSubject, aTopic, aData);
-  }, topic, false);
-}
-
 function registerPingHandler(handler) {
-  httpserver.registerPrefixHandler("/submit/telemetry/",
+  gHttpServer.registerPrefixHandler("/submit/telemetry/",
 				   wrapWithExceptionHandler(handler));
 }
 
-function nonexistentServerObserver(aSubject, aTopic, aData) {
-  httpserver.start(-1);
-  serverStarted = true;
-
-  // Provide a dummy function so it returns 200 instead of 404 to telemetry.
-  registerPingHandler(dummyHandler);
-  addWrappedObserver(telemetryObserver, "telemetry-test-xhr-complete");
-  telemetry_ping();
-}
-
 function setupTestData() {
   Telemetry.newHistogram(IGNORE_HISTOGRAM, "never", 1, 2, 3, Telemetry.HISTOGRAM_BOOLEAN);
   Telemetry.histogramFrom(IGNORE_CLONED_HISTOGRAM, IGNORE_HISTOGRAM_TO_CLONE);
   Services.startup.interrupted = true;
   Telemetry.registerAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM, 1, 5, 6,
                                    Telemetry.HISTOGRAM_LINEAR);
   h1 = Telemetry.getAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM);
   h1.add(1);
@@ -128,26 +94,16 @@ function getSavedHistogramsFile(basename
     try {
       histogramsFile.remove(true);
     } catch (e) {
     }
   });
   return histogramsFile;
 }
 
-function telemetryObserver(aSubject, aTopic, aData) {
-  registerPingHandler(checkHistogramsSync);
-  let histogramsFile = getSavedHistogramsFile("saved-histograms.dat");
-  setupTestData();
-
-  TelemetryPing.saveHistograms(histogramsFile, true);
-  TelemetryPing.testLoadHistograms(histogramsFile, true);
-  telemetry_ping();
-}
-
 function decodeRequestPayload(request) {
   let s = request.bodyInputStream;
   let payload = null;
   let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON)
 
   if (request.getHeader("content-encoding") == "gzip") {
     let observer = {
       buffer: "",
@@ -199,19 +155,19 @@ function checkPayloadInfo(payload, reaso
   do_check_true("appUpdateChannel" in payload.info);
   do_check_true("locale" in payload.info);
   do_check_true("revision" in payload.info);
   do_check_true(payload.info.revision.startsWith("http"));
 
   try {
     // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
     // this test.
-    var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
-    var isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
-    var isOSX = ("nsILocalFileMac" in Components.interfaces);
+    let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
+    let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
+    let isOSX = ("nsILocalFileMac" in Components.interfaces);
 
     if (isWindows || isOSX) {
       do_check_true("adapterVendorID" in payload.info);
       do_check_true("adapterDeviceID" in payload.info);
     }
   }
   catch (x) {
   }
@@ -235,17 +191,17 @@ function checkPayload(request, reason, s
   do_check_eq(payload.simpleMeasurements.failedProfileLockCount,
               FAILED_PROFILE_LOCK_ATTEMPTS);
   let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
   let failedProfileLocksFile = profileDirectory.clone();
   failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
   do_check_true(!failedProfileLocksFile.exists());
 
 
-  var isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
+  let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
   if (isWindows) {
     do_check_true(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
     do_check_true(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
   }
 
   const TELEMETRY_PING = "TELEMETRY_PING";
   const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS";
   const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG";
@@ -307,123 +263,57 @@ function checkPayload(request, reason, s
   do_check_true("addonHistograms" in payload);
   do_check_true(ADDON_NAME in payload.addonHistograms);
   do_check_true(ADDON_HISTOGRAM in payload.addonHistograms[ADDON_NAME]);
 
   do_check_true(("mainThread" in payload.slowSQL) &&
                 ("otherThreads" in payload.slowSQL));
 }
 
-function checkPersistedHistogramsSync(request, response) {
-  // Even though we have had two successful pings when this handler is
-  // run, we only had one successful ping when the histograms were
-  // saved.
-  checkPayload(request, "saved-session", 1);
-
-  addWrappedObserver(runAsyncTestObserver, "telemetry-test-xhr-complete");
-}
-
-function checkHistogramsSync(request, response) {
-  registerPingHandler(checkPersistedHistogramsSync);
-  checkPayload(request, "test-ping", 1);
-}
-
-function runAsyncTestObserver(aSubject, aTopic, aData) {
-  registerPingHandler(checkHistogramsAsync);
-  let histogramsFile = getSavedHistogramsFile("saved-histograms2.dat");
-
-  addWrappedObserver(function(aSubject, aTopic, aData) {
-    addWrappedObserver(function(aSubject, aTopic, aData) {
-      telemetry_ping();
-    }, "telemetry-test-load-complete");
-
-    TelemetryPing.testLoadHistograms(histogramsFile, false);
-  }, "telemetry-test-save-complete");
-  TelemetryPing.saveHistograms(histogramsFile, false);
-}
-
-function checkPersistedHistogramsAsync(request, response) {
-  // do not need the http server anymore
-  httpserver.stop(do_test_finished);
-  // Even though we have had four successful pings when this handler is
-  // run, we only had three successful pings when the histograms were
-  // saved.
-  checkPayload(request, "saved-session", 3);
-
-  runOldPingFileTest();
-
-  gFinished = true;
-}
-
-function checkHistogramsAsync(request, response) {
-  registerPingHandler(checkPersistedHistogramsAsync);
-  checkPayload(request, "test-ping", 3);
-}
-
-function runInvalidJSONTest() {
-  let histogramsFile = getSavedHistogramsFile("invalid-histograms.dat");
-  writeStringToFile(histogramsFile, "this.is.invalid.JSON");
-  do_check_true(histogramsFile.exists());
-  
-  TelemetryPing.testLoadHistograms(histogramsFile, true);
-  do_check_false(histogramsFile.exists());
-}
-
-function runOldPingFileTest() {
-  let histogramsFile = getSavedHistogramsFile("old-histograms.dat");
-  TelemetryPing.saveHistograms(histogramsFile, true);
-  do_check_true(histogramsFile.exists());
-
-  let mtime = histogramsFile.lastModifiedTime;
-  histogramsFile.lastModifiedTime = mtime - (14 * 24 * 60 * 60 * 1000 + 60000); // 14 days, 1m
-  TelemetryPing.testLoadHistograms(histogramsFile, true);
-  do_check_false(histogramsFile.exists());
-}
-
 function dummyTheme(id) {
   return {
     id: id,
     name: Math.random().toString(),
     headerURL: "http://lwttest.invalid/a.png",
     footerURL: "http://lwttest.invalid/b.png",
     textcolor: Math.random().toString(),
     accentcolor: Math.random().toString()
   };
 }
 
 // A fake plugin host for testing flash version telemetry
-var PluginHost = {
+let PluginHost = {
   getPluginTags: function(countRef) {
     let plugins = [{name: "Shockwave Flash", version: FLASH_VERSION}];
     countRef.value = plugins.length;
     return plugins;
   },
 
   QueryInterface: function(iid) {
     if (iid.equals(Ci.nsIPluginHost)
      || iid.equals(Ci.nsISupports))
       return this;
   
     throw Components.results.NS_ERROR_NO_INTERFACE;
   }
 }
 
-var PluginHostFactory = {
+let PluginHostFactory = {
   createInstance: function (outer, iid) {
     if (outer != null)
       throw Components.results.NS_ERROR_NO_AGGREGATION;
     return PluginHost.QueryInterface(iid);
   }
 };
 
 const PLUGINHOST_CONTRACTID = "@mozilla.org/plugin/host;1";
 const PLUGINHOST_CID = Components.ID("{2329e6ea-1f15-4cbe-9ded-6e98e842de0e}");
 
 function registerFakePluginHost() {
-  var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
   registrar.registerFactory(PLUGINHOST_CID, "Fake Plugin Host",
                             PLUGINHOST_CONTRACTID, PluginHostFactory);
 }
 
 function writeStringToFile(file, contents) {
   let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"]
                 .createInstance(Ci.nsIFileOutputStream);
   ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
@@ -447,17 +337,17 @@ function write_fake_failedprofilelocks_f
   file.append("Telemetry.FailedProfileLocks.txt");
   let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
   writeStringToFile(file, contents);
 }
 
 function run_test() {
   do_test_pending();
   try {
-    var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
+    let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
     gfxInfo.spoofVendorID("0xabcd");
     gfxInfo.spoofDeviceID("0x1234");
   } catch (x) {
     // If we can't test gfxInfo, that's fine, we'll note it later.
   }
 
   // Addon manager needs a profile directory
   do_get_profile();
@@ -490,30 +380,115 @@ function run_test() {
       thread.shutdown();
     });
   });
 
   Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(actualTest));
 }
 
 function actualTest() {
-  // ensure that test runs to completion
-  do_register_cleanup(function () do_check_true(gFinished));
   // try to make LightweightThemeManager do stuff
   let gInternalManager = Cc["@mozilla.org/addons/integration;1"]
                          .getService(Ci.nsIObserver)
                          .QueryInterface(Ci.nsITimerCallback);
 
   gInternalManager.observe(null, "addons-startup", null);
   LightweightThemeManager.currentTheme = dummyTheme("1234");
 
   // fake plugin host for consistent flash version data
   registerFakePluginHost();
 
-  runInvalidJSONTest();
-  test_expired_histogram();
+  run_next_test();
+}
+
+// Ensures that expired histograms are not part of the payload.
+add_task(function* test_expiredHistogram() {
+  let histogram_id = "FOOBAR";
+  let dummy = Telemetry.newHistogram(histogram_id, "30", 1, 2, 3, Telemetry.HISTOGRAM_EXPONENTIAL);
+
+  dummy.add(1);
+
+  do_check_eq(TelemetryPing.getPayload()["histograms"][histogram_id], undefined);
+  do_check_eq(TelemetryPing.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
+});
+
+// Checks that an invalid histogram file is deleted if TelemetryFile fails to parse it.
+add_task(function* test_runInvalidJSON() {
+  let histogramsFile = getSavedHistogramsFile("invalid-histograms.dat");
+
+  writeStringToFile(histogramsFile, "this.is.invalid.JSON");
+  do_check_true(histogramsFile.exists());
+
+  yield TelemetryPing.testLoadHistograms(histogramsFile);
+  do_check_false(histogramsFile.exists());
+});
+
+// Sends a ping to a non existing server.
+add_task(function* test_noServerPing() {
+  yield sendPing();
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(function* test_simplePing() {
+  gHttpServer.start(-1);
+  gServerStarted = true;
+  gRequestIterator = Iterator(new Request());
+
+  yield sendPing();
+  decodeRequestPayload(yield gRequestIterator.next());
+});
+
+// Saves the current session histograms, reloads them, perfoms a ping
+// and checks that the dummy http server received both the previously
+// saved histograms and the new ones.
+add_task(function* test_saveLoadPing() {
+  let histogramsFile = getSavedHistogramsFile("saved-histograms.dat");
 
-  addWrappedObserver(nonexistentServerObserver, "telemetry-test-xhr-complete");
-  telemetry_ping();
-  // spin the event loop
-  do_test_pending();
-  do_test_finished();
+  setupTestData();
+  yield TelemetryPing.testSaveHistograms(histogramsFile);
+  yield TelemetryPing.testLoadHistograms(histogramsFile);
+  yield sendPing();
+  checkPayload((yield gRequestIterator.next()), "test-ping", 1);
+  checkPayload((yield gRequestIterator.next()), "saved-session", 1);
+});
+
+// Checks that an expired histogram file is deleted when loaded.
+add_task(function* test_runOldPingFile() {
+  let histogramsFile = getSavedHistogramsFile("old-histograms.dat");
+
+  yield TelemetryPing.testSaveHistograms(histogramsFile);
+  do_check_true(histogramsFile.exists());
+  let mtime = histogramsFile.lastModifiedTime;
+  histogramsFile.lastModifiedTime = mtime - (14 * 24 * 60 * 60 * 1000 + 60000); // 14 days, 1m
+
+  yield TelemetryPing.testLoadHistograms(histogramsFile);
+  do_check_false(histogramsFile.exists());
+});
+
+add_task(function* stopServer(){
+  gHttpServer.stop(do_test_finished);
+});
+
+// An iterable sequence of http requests
+function Request() {
+  let defers = [];
+  let current = 0;
+
+  function RequestIterator() {}
+
+  // Returns a promise that resolves to the next http request
+  RequestIterator.prototype.next = function() {
+    let deferred = defers[current++];
+    return deferred.promise;
+  }
+
+  this.__iterator__ = function(){
+    return new RequestIterator();
+  }
+
+  registerPingHandler((request, response) => {
+    let deferred = defers[defers.length - 1];
+    defers.push(Promise.defer());
+    deferred.resolve(request);
+  });
+
+  defers.push(Promise.defer());
 }
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryPingBuildID.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryPingBuildID.js
@@ -8,62 +8,63 @@
  * 1) Run with no "previousBuildID" stored in prefs:
  *     -> no previousBuildID in telemetry system info, new value set in prefs.
  * 2) previousBuildID in prefs, equal to current build ID:
  *     -> no previousBuildID in telemetry, prefs not updated.
  * 3) previousBuildID in prefs, not equal to current build ID:
  *     -> previousBuildID in telemetry, new value set in prefs.
  */
 
+"use strict"
+
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/TelemetryPing.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
 
 // Force the Telemetry enabled preference so that TelemetryPing.reset() doesn't exit early.
 Services.prefs.setBoolPref(TelemetryPing.Constants.PREF_ENABLED, true);
 
 // Set up our dummy AppInfo object so we can control the appBuildID.
-Cu.import("resource://testing-common/AppInfo.jsm");
+Cu.import("resource://testing-common/AppInfo.jsm", this);
 updateAppInfo();
 
 // Check that when run with no previous build ID stored, we update the pref but do not
 // put anything into the metadata.
-function testFirstRun() {
-  TelemetryPing.reset();
+add_task(function* test_firstRun() {
+  yield TelemetryPing.setup()
+  yield TelemetryPing.reset();
   let metadata = TelemetryPing.getMetadata();
   do_check_false("previousBuildID" in metadata);
   let appBuildID = getAppInfo().appBuildID;
   let buildIDPref = Services.prefs.getCharPref(TelemetryPing.Constants.PREF_PREVIOUS_BUILDID);
   do_check_eq(appBuildID, buildIDPref);
-}
+});
 
 // Check that a subsequent run with the same build ID does not put prev build ID in
 // metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref.
-function testSecondRun() {
-  TelemetryPing.reset();
+add_task(function* test_secondRun() {
+  yield TelemetryPing.reset();
   let metadata = TelemetryPing.getMetadata();
   do_check_false("previousBuildID" in metadata);
-}
+});
 
 // Set up telemetry with a different app build ID and check that the old build ID
 // is returned in the metadata and the pref is updated to the new build ID.
 // Assumes testFirstRun() has been called to set the previousBuildID pref.
 const NEW_BUILD_ID = "20130314";
-function testNewBuild() {
+add_task(function* test_newBuild() {
   let info = getAppInfo();
   let oldBuildID = info.appBuildID;
   info.appBuildID = NEW_BUILD_ID;
-  TelemetryPing.reset();
+  yield TelemetryPing.reset();
   let metadata = TelemetryPing.getMetadata();
   do_check_eq(metadata.previousBuildID, oldBuildID);
   let buildIDPref = Services.prefs.getCharPref(TelemetryPing.Constants.PREF_PREVIOUS_BUILDID);
   do_check_eq(NEW_BUILD_ID, buildIDPref);
-}
+});
 
 
 function run_test() {
   // Make sure we have a profile directory.
   do_get_profile();
-  testFirstRun();
-  testSecondRun();
-  testNewBuild();
+  run_next_test();
 }
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -5,26 +5,30 @@
  * This test case populates the profile with some fake stored
  * pings, and checks that:
  *
  * 1) Pings that are considered "expired" are deleted and never sent.
  * 2) Pings that are considered "overdue" trigger a send of all
  *    overdue and recent pings.
  */
 
+"use strict"
+
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://testing-common/httpd.js");
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/TelemetryFile.jsm");
-Cu.import("resource://gre/modules/TelemetryPing.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://testing-common/httpd.js", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/TelemetryFile.jsm", this);
+Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
 
 // We increment TelemetryFile's MAX_PING_FILE_AGE and
 // OVERDUE_PING_FILE_AGE by 1ms so that our test pings exceed
 // those points in time.
 const EXPIRED_PING_FILE_AGE = TelemetryFile.MAX_PING_FILE_AGE + 1;
 const OVERDUE_PING_FILE_AGE = TelemetryFile.OVERDUE_PING_FILE_AGE + 1;
 
 const PING_SAVE_FOLDER = "saved-telemetry-pings";
@@ -46,35 +50,40 @@ let gSeenPings = 0;
  *
  * @param aNum the number of pings to create.
  * @param aAge the age in milliseconds to offset from now. A value
  *             of 10 would make the ping 10ms older than now, for
  *             example.
  * @returns an Array with the created pings.
  */
 function createSavedPings(aNum, aAge) {
-  // Create a TelemetryPing service that we can generate payloads from.
-  // Luckily, the TelemetryPing constructor does nothing that we need to
-  // clean up.
-  let pings = [];
-  let age = Date.now() - aAge;
-  for (let i = 0; i < aNum; ++i) {
-    let payload = TelemetryPing.getPayload();
-    let ping = { slug: "test-ping-" + gCreatedPings, reason: "test", payload: payload };
-    TelemetryFile.savePing(ping);
-    if (aAge) {
-      // savePing writes to the file synchronously, so we're good to
-      // modify the lastModifedTime now.
-      let file = getSaveFileForPing(ping);
-      file.lastModifiedTime = age;
+  return Task.spawn(function*(){
+    // Create a TelemetryPing service that we can generate payloads from.
+    // Luckily, the TelemetryPing constructor does nothing that we need to
+    // clean up.
+    let pings = [];
+    let age = Date.now() - aAge;
+
+    for (let i = 0; i < aNum; ++i) {
+      let payload = TelemetryPing.getPayload();
+      let ping = { slug: "test-ping-" + gCreatedPings, reason: "test", payload: payload };
+
+      yield TelemetryFile.savePing(ping);
+
+      if (aAge) {
+        // savePing writes to the file synchronously, so we're good to
+        // modify the lastModifedTime now.
+        let file = getSaveFileForPing(ping);
+        file.lastModifiedTime = age;
+      }
+      gCreatedPings++;
+      pings.push(ping);
     }
-    gCreatedPings++;
-    pings.push(ping);
-  }
-  return pings;
+    return pings;
+  });
 }
 
 /**
  * Deletes locally saved pings in aPings if they
  * exist.
  *
  * @param aPings an Array of pings to delete.
  */
@@ -96,55 +105,23 @@ function clearPings(aPings) {
 function getSaveFileForPing(aPing) {
   let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone();
   file.append(PING_SAVE_FOLDER);
   file.append(aPing.slug);
   return file;
 }
 
 /**
- * Wait for PING_TIMEOUT_LENGTH ms, and make sure we didn't receive
- * TelemetryPings in that time.
- *
- * @returns Promise
- */
-function assertReceivedNoPings() {
-  let deferred = Promise.defer();
-
-  do_timeout(PING_TIMEOUT_LENGTH, function() {
-    if (gSeenPings > 0) {
-      deferred.reject();
-    } else {
-      deferred.resolve();
-    }
-  });
-
-  return deferred.promise;
-}
-
-/**
- * Returns a Promise that rejects if the number of TelemetryPings
- * received by the HttpServer is not equal to aExpectedNum.
+ * Check if the number of TelemetryPings received by the 
+ * HttpServer is not equal to aExpectedNum.
  *
  * @param aExpectedNum the number of pings we expect to receive.
- * @returns Promise
  */
 function assertReceivedPings(aExpectedNum) {
-  let deferred = Promise.defer();
-
-  do_timeout(PING_TIMEOUT_LENGTH, function() {
-    if (gSeenPings == aExpectedNum) {
-      deferred.resolve();
-    } else {
-      deferred.reject("Saw " + gSeenPings + " TelemetryPings, " +
-                      "but expected " + aExpectedNum);
-    }
-  })
-
-  return deferred.promise;
+  do_check_eq(gSeenPings, aExpectedNum);
 }
 
 /**
  * Throws if any pings in aPings is saved locally.
  *
  * @param aPings an Array of pings to check.
  */
 function assertNotSaved(aPings) {
@@ -185,29 +162,31 @@ function stopHttpServer() {
   return deferred.promise;
 }
 
 /**
  * Teardown a TelemetryPing instance and clear out any pending
  * pings to put as back in the starting state.
  */
 function resetTelemetry() {
-  TelemetryPing.uninstall();
-  // Quick and dirty way to clear TelemetryFile's pendingPings
-  // collection, and put it back in its initial state.
-  let gen = TelemetryFile.popPendingPings();
-  for (let item of gen) {};
+  return Task.spawn(function*(){
+    yield TelemetryPing.uninstall();
+    // Quick and dirty way to clear TelemetryFile's pendingPings
+    // collection, and put it back in its initial state.
+    let gen = TelemetryFile.popPendingPings();
+    for (let item of gen) {};
+  });
 }
 
 /**
  * Creates and returns a TelemetryPing instance in "testing"
  * mode.
  */
 function startTelemetry() {
-  TelemetryPing.setup();
+  return TelemetryPing.setup();
 }
 
 function run_test() {
   gHttpServer.registerPrefixHandler("/submit/telemetry/", pingHandler);
   gHttpServer.start(-1);
   do_get_profile();
   Services.prefs.setBoolPref(TelemetryPing.Constants.PREF_ENABLED, true);
   Services.prefs.setCharPref(TelemetryPing.Constants.PREF_SERVER,
@@ -215,48 +194,48 @@ function run_test() {
   run_next_test();
 }
 
 /**
  * Test that pings that are considered too old are just chucked out
  * immediately and never sent.
  */
 add_task(function test_expired_pings_are_deleted() {
-  let expiredPings = createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
-  startTelemetry();
-  yield assertReceivedNoPings();
+  let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
+  yield startTelemetry();
+  assertReceivedPings(0);
   assertNotSaved(expiredPings);
-  resetTelemetry();
+  yield resetTelemetry();
 });
 
 /**
  * Test that really recent pings are not sent on Telemetry initialization.
  */
 add_task(function test_recent_pings_not_sent() {
-  let recentPings = createSavedPings(RECENT_PINGS);
-  startTelemetry();
-  yield assertReceivedNoPings();
-  resetTelemetry();
+  let recentPings = yield createSavedPings(RECENT_PINGS);
+  yield startTelemetry();
+  assertReceivedPings(0);
+  yield resetTelemetry();
   clearPings(recentPings);
 });
 
 /**
  * Create some recent, expired and overdue pings. The overdue pings should
  * trigger a send of all recent and overdue pings, but the expired pings
  * should just be deleted.
  */
 add_task(function test_overdue_pings_trigger_send() {
-  let recentPings = createSavedPings(RECENT_PINGS);
-  let expiredPings = createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
-  let overduePings = createSavedPings(OVERDUE_PINGS, OVERDUE_PING_FILE_AGE);
+  let recentPings = yield createSavedPings(RECENT_PINGS);
+  let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
+  let overduePings = yield createSavedPings(OVERDUE_PINGS, OVERDUE_PING_FILE_AGE);
 
-  startTelemetry();
-  yield assertReceivedPings(TOTAL_EXPECTED_PINGS);
+  yield startTelemetry();
+  assertReceivedPings(TOTAL_EXPECTED_PINGS);
 
   assertNotSaved(recentPings);
   assertNotSaved(expiredPings);
   assertNotSaved(overduePings);
-  resetTelemetry();
+  yield resetTelemetry();
 });
 
 add_task(function teardown() {
   yield stopHttpServer();
 });