Bug 784145 - When submitting hang reports, submit the browser report as a field of the plugin report instead of as a completely separate report. r=ted
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Sat, 08 Sep 2012 19:20:59 +0200
changeset 104747 d5bf48b5cd6f5c5865ea59574d78218e95654a28
parent 104746 78f957de5c9d9f47fecd00cc18c68fd1ca45acee
child 104748 07f04ff0b782423e4195a1e2b0cc3cb975b63dee
push idunknown
push userunknown
push dateunknown
reviewersted
bugs784145
milestone18.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 784145 - When submitting hang reports, submit the browser report as a field of the plugin report instead of as a completely separate report. r=ted
dom/ipc/CrashReporterParent.cpp
dom/ipc/CrashReporterParent.h
dom/plugins/ipc/PluginModuleParent.cpp
dom/plugins/test/mochitest/hang_test.js
dom/plugins/test/mochitest/test_crashing.html
testing/mochitest/specialpowers/content/specialpowers.js
testing/mochitest/tests/SimpleTest/ChromePowers.js
testing/mochitest/tests/SimpleTest/SpecialPowersObserverAPI.js
toolkit/crashreporter/CrashSubmit.jsm
toolkit/crashreporter/KeyValueParser.jsm
toolkit/crashreporter/Makefile.in
toolkit/crashreporter/nsExceptionHandler.cpp
toolkit/crashreporter/nsExceptionHandler.h
toolkit/crashreporter/test/unit/head_crashreporter.js
--- a/dom/ipc/CrashReporterParent.cpp
+++ b/dom/ipc/CrashReporterParent.cpp
@@ -34,24 +34,23 @@ CrashReporterParent::RecvAddLibraryMappi
                                              m.start_address(),
                                              m.mapping_length(),
                                              m.file_offset());
   }
 #endif
   return true;
 }
 
-bool
-CrashReporterParent::RecvAnnotateCrashReport(const nsCString& key,
-                                             const nsCString& data)
+void
+CrashReporterParent::AnnotateCrashReport(const nsCString& key,
+                                         const nsCString& data)
 {
 #ifdef MOZ_CRASHREPORTER
     mNotes.Put(key, data);
 #endif
-    return true;
 }
 
 bool
 CrashReporterParent::RecvAppendAppNotes(const nsCString& data)
 {
     mAppNotes.Append(data);
     return true;
 }
@@ -78,32 +77,16 @@ CrashReporterParent::SetChildData(const 
 {
     mInitialized = true;
     mMainThread = tid;
     mProcessType = processType;
 }
 
 #ifdef MOZ_CRASHREPORTER
 bool
-CrashReporterParent::GenerateHangCrashReport(const AnnotationTable* processNotes)
-{
-    if (mChildDumpID.IsEmpty())
-        return false;
-
-    GenerateChildData(processNotes);
-
-    CrashReporter::AnnotationTable notes;
-    notes.Init(4);
-    notes.Put(nsDependentCString("HangID"), NS_ConvertUTF16toUTF8(mHangID));
-    if (!CrashReporter::AppendExtraData(mParentDumpID, notes))
-        NS_WARNING("problem appending parent data to .extra");
-    return true;
-}
-
-bool
 CrashReporterParent::GenerateCrashReportForMinidump(nsIFile* minidump,
     const AnnotationTable* processNotes)
 {
     if (!CrashReporter::GetIDFromMinidump(minidump, mChildDumpID))
         return false;
     return GenerateChildData(processNotes);
 }
 
--- a/dom/ipc/CrashReporterParent.h
+++ b/dom/ipc/CrashReporterParent.h
@@ -29,84 +29,72 @@ public:
   /* Attempt to generate a parent/child pair of minidumps from the given
      toplevel actor in the event of a hang. Returns true if successful,
      false otherwise.
   */
   template<class Toplevel>
   bool
   GeneratePairedMinidump(Toplevel* t);
 
-  /* Attempt to create a bare-bones crash report for a hang, along with extra
-     process-specific annotations present in the given AnnotationTable. Returns
-     true if successful, false otherwise.
-  */
-  bool
-  GenerateHangCrashReport(const AnnotationTable* processNotes);
-
   /* Attempt to create a bare-bones crash report, along with extra process-
      specific annotations present in the given AnnotationTable. Returns true if
      successful, false otherwise.
   */
   template<class Toplevel>
   bool
   GenerateCrashReport(Toplevel* t, const AnnotationTable* processNotes);
 
+  /**
+   * Add the .extra data for an existing crash report.
+   */
+  bool
+  GenerateChildData(const AnnotationTable* processNotes);
+
   bool
   GenerateCrashReportForMinidump(nsIFile* minidump,
                                  const AnnotationTable* processNotes);
 
   /* Instantiate a new crash reporter actor from a given parent that manages
      the protocol.
   */
   template<class Toplevel>
   static bool CreateCrashReporter(Toplevel* actor);
 #endif
   /* Initialize this reporter with data from the child process */
   void
     SetChildData(const NativeThreadId& id, const uint32_t& processType);
 
-  /* Returns the shared hang ID of a parent/child paired minidump.
-     GeneratePairedMinidump must be called first.
-  */
-  const nsString& HangID() {
-    return mHangID;
-  }
-  /* Returns the ID of the parent minidump.
-     GeneratePairedMinidump must be called first.
-  */
-  const nsString& ParentDumpID() {
-    return mParentDumpID;
-  }
   /* Returns the ID of the child minidump.
      GeneratePairedMinidump or GenerateCrashReport must be called first.
   */
   const nsString& ChildDumpID() {
     return mChildDumpID;
   }
 
+  void
+  AnnotateCrashReport(const nsCString& key, const nsCString& data);
+
  protected:
   virtual void ActorDestroy(ActorDestroyReason why);
 
   virtual bool
     RecvAddLibraryMappings(const InfallibleTArray<Mapping>& m);
   virtual bool
-    RecvAnnotateCrashReport(const nsCString& key, const nsCString& data);
+    RecvAnnotateCrashReport(const nsCString& key, const nsCString& data) {
+    AnnotateCrashReport(key, data);
+    return true;
+  }
   virtual bool
     RecvAppendAppNotes(const nsCString& data);
 
 #ifdef MOZ_CRASHREPORTER
-  bool
-  GenerateChildData(const AnnotationTable* processNotes);
-
   AnnotationTable mNotes;
 #endif
   nsCString mAppNotes;
-  nsString mHangID;
   nsString mChildDumpID;
-  nsString mParentDumpID;
   NativeThreadId mMainThread;
   time_t mStartTime;
   uint32_t mProcessType;
   bool mInitialized;
 };
 
 #ifdef MOZ_CRASHREPORTER
 template<class Toplevel>
@@ -115,24 +103,20 @@ CrashReporterParent::GeneratePairedMinid
 {
   CrashReporter::ProcessHandle child;
 #ifdef XP_MACOSX
   child = t->Process()->GetChildTask();
 #else
   child = t->OtherProcess();
 #endif
   nsCOMPtr<nsIFile> childDump;
-  nsCOMPtr<nsIFile> parentDump;
   if (CrashReporter::CreatePairedMinidumps(child,
                                            mMainThread,
-                                           &mHangID,
-                                           getter_AddRefs(childDump),
-                                           getter_AddRefs(parentDump)) &&
-      CrashReporter::GetIDFromMinidump(childDump, mChildDumpID) &&
-      CrashReporter::GetIDFromMinidump(parentDump, mParentDumpID)) {
+                                           getter_AddRefs(childDump)) &&
+      CrashReporter::GetIDFromMinidump(childDump, mChildDumpID)) {
     return true;
   }
   return false;
 }
 
 template<class Toplevel>
 inline bool
 CrashReporterParent::GenerateCrashReport(Toplevel* t,
--- a/dom/plugins/ipc/PluginModuleParent.cpp
+++ b/dom/plugins/ipc/PluginModuleParent.cpp
@@ -169,38 +169,34 @@ PluginModuleParent::WriteExtraDataForMin
 
     //TODO: add plugin name and version: bug 539841
     // (as PluginName, PluginVersion)
     notes.Put(CS("PluginName"), CS(""));
     notes.Put(CS("PluginVersion"), CS(""));
 
     CrashReporterParent* crashReporter = CrashReporter();
     if (crashReporter) {
-        const nsString& hangID = crashReporter->HangID();
-        if (!hangID.IsEmpty()) {
-            notes.Put(CS("HangID"), NS_ConvertUTF16toUTF8(hangID));
 #ifdef XP_WIN
-            if (mPluginCpuUsageOnHang.Length() > 0) {
-              notes.Put(CS("NumberOfProcessors"),
-                        nsPrintfCString("%d", PR_GetNumberOfProcessors()));
+        if (mPluginCpuUsageOnHang.Length() > 0) {
+            notes.Put(CS("NumberOfProcessors"),
+                      nsPrintfCString("%d", PR_GetNumberOfProcessors()));
 
-              nsCString cpuUsageStr;
-              cpuUsageStr.AppendFloat(std::ceil(mPluginCpuUsageOnHang[0] * 100) / 100);
-              notes.Put(CS("PluginCpuUsage"), cpuUsageStr);
+            nsCString cpuUsageStr;
+            cpuUsageStr.AppendFloat(std::ceil(mPluginCpuUsageOnHang[0] * 100) / 100);
+            notes.Put(CS("PluginCpuUsage"), cpuUsageStr);
 
 #ifdef MOZ_CRASHREPORTER_INJECTOR
-              for (uint32_t i=1; i<mPluginCpuUsageOnHang.Length(); ++i) {
+            for (uint32_t i=1; i<mPluginCpuUsageOnHang.Length(); ++i) {
                 nsCString tempStr;
                 tempStr.AppendFloat(std::ceil(mPluginCpuUsageOnHang[i] * 100) / 100);
                 notes.Put(nsPrintfCString("CpuUsageFlashProcess%d", i), tempStr);
-              }
-#endif
             }
 #endif
         }
+#endif
     }
 }
 #endif  // MOZ_CRASHREPORTER
 
 int
 PluginModuleParent::TimeoutChanged(const char* aPref, void* aModule)
 {
     NS_ASSERTION(NS_IsMainThread(), "Wrong thead!");
@@ -292,24 +288,29 @@ GetProcessCpuUsage(const InfallibleTArra
 } // anonymous namespace
 #endif // #ifdef XP_WIN
 
 bool
 PluginModuleParent::ShouldContinueFromReplyTimeout()
 {
 #ifdef MOZ_CRASHREPORTER
     CrashReporterParent* crashReporter = CrashReporter();
+    crashReporter->AnnotateCrashReport(NS_LITERAL_CSTRING("PluginHang"),
+                                       NS_LITERAL_CSTRING("1"));
     if (crashReporter->GeneratePairedMinidump(this)) {
-        mBrowserDumpID = crashReporter->ParentDumpID();
         mPluginDumpID = crashReporter->ChildDumpID();
         PLUGIN_LOG_DEBUG(
-                ("generated paired browser/plugin minidumps: %s/%s (ID=%s)",
-                 NS_ConvertUTF16toUTF8(mBrowserDumpID).get(),
-                 NS_ConvertUTF16toUTF8(mPluginDumpID).get(),
-                 NS_ConvertUTF16toUTF8(crashReporter->HangID()).get()));
+                ("generated paired browser/plugin minidumps: %s)",
+                 NS_ConvertUTF16toUTF8(mPluginDumpID).get()));
+
+        crashReporter->AnnotateCrashReport(
+            NS_LITERAL_CSTRING("additional_minidumps"),
+            NS_LITERAL_CSTRING("browser"));
+
+        // TODO: collect Flash minidumps here
     } else {
         NS_WARNING("failed to capture paired minidumps from hang");
     }
 #endif
 
 #ifdef XP_WIN
     // collect cpu usage for plugin processes
 
@@ -372,19 +373,19 @@ PluginModuleParent::ProcessFirstMinidump
 {
     CrashReporterParent* crashReporter = CrashReporter();
     if (!crashReporter)
         return;
 
     AnnotationTable notes;
     notes.Init(4);
     WriteExtraDataForMinidump(notes);
-        
-    if (!mPluginDumpID.IsEmpty() && !mBrowserDumpID.IsEmpty()) {
-        crashReporter->GenerateHangCrashReport(&notes);
+
+    if (!mPluginDumpID.IsEmpty()) {
+        crashReporter->GenerateChildData(&notes);
         return;
     }
 
     uint32_t sequence = PR_UINT32_MAX;
     nsCOMPtr<nsIFile> dumpFile;
     nsAutoCString flashProcessType;
     TakeMinidump(getter_AddRefs(dumpFile), &sequence);
 
--- a/dom/plugins/test/mochitest/hang_test.js
+++ b/dom/plugins/test/mochitest/hang_test.js
@@ -1,105 +1,82 @@
+
+Components.utils.import("resource://gre/modules/KeyValueParser.jsm");
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 var success = false;
 var observerFired = false;
 
-function parseKeyValuePairs(text) {
-  var lines = text.split('\n');
-  var data = {};
-  for (let i = 0; i < lines.length; i++) {
-    if (lines[i] == '')
-      continue;
-
-    // can't just .split() because the value might contain = characters
-    let eq = lines[i].indexOf('=');
-    if (eq != -1) {
-      let [key, value] = [lines[i].substring(0, eq),
-                          lines[i].substring(eq + 1)];
-      if (key && value)
-        data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
-    }
-  }
-  return data;
-}
-
-function parseKeyValuePairsFromFile(file) {
-  var fstream = Cc["@mozilla.org/network/file-input-stream;1"].
-                createInstance(Ci.nsIFileInputStream);
-  fstream.init(file, -1, 0, 0);
-  var is = Cc["@mozilla.org/intl/converter-input-stream;1"].
-           createInstance(Ci.nsIConverterInputStream);
-  is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
-  var str = {};
-  var contents = '';
-  while (is.readString(4096, str) != 0) {
-    contents += str.value;
-  }
-  is.close();
-  fstream.close();
-  return parseKeyValuePairs(contents);
-}
-
-
 var testObserver = {
   idleHang: true,
 
   observe: function(subject, topic, data) {
     observerFired = true;
     ok(true, "Observer fired");
     is(topic, "plugin-crashed", "Checking correct topic");
     is(data,  null, "Checking null data");
     ok((subject instanceof Ci.nsIPropertyBag2), "got Propbag");
     ok((subject instanceof Ci.nsIWritablePropertyBag2), "got writable Propbag");
 
     var pluginId = subject.getPropertyAsAString("pluginDumpID");
     isnot(pluginId, "", "got a non-empty plugin crash id");
-    var browserId = subject.getPropertyAsAString("browserDumpID");
-    isnot(browserId, "", "got a non-empty browser crash id");
-    
-    // check dump and extra files
+
+    // check plugin dump and extra files
     let directoryService =
       Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
     let profD = directoryService.get("ProfD", Ci.nsIFile);
     profD.append("minidumps");
     let pluginDumpFile = profD.clone();
     pluginDumpFile.append(pluginId + ".dmp");
     ok(pluginDumpFile.exists(), "plugin minidump exists");
-    let browserDumpFile = profD.clone();
-    browserDumpFile.append(browserId + ".dmp");
-    ok(browserDumpFile.exists(), "browser minidump exists");
+
     let pluginExtraFile = profD.clone();
     pluginExtraFile.append(pluginId + ".extra");
     ok(pluginExtraFile.exists(), "plugin extra file exists");
-    let browserExtraFile = profD.clone();
-    browserExtraFile.append(browserId + ".extra");
-    ok(pluginExtraFile.exists(), "browser extra file exists");
-     
+
+    let extraData = parseKeyValuePairsFromFile(pluginExtraFile);
+
+    // check additional dumps
+
+    ok("additional_minidumps" in extraData, "got field for additional minidumps");
+    let additionalDumps = extraData.additional_minidumps.split(',');
+    ok(additionalDumps.indexOf('browser') >= 0, "browser in additional_minidumps");
+
+    let additionalDumpFiles = [];
+    for (let name of additionalDumps) {
+      let file = profD.clone();
+      file.append(pluginId + "-" + name + ".dmp");
+      ok(file.exists(), "additional dump '"+name+"' exists");
+      if (file.exists()) {
+        additionalDumpFiles.push(file);
+      }
+    }
+
     // check cpu usage field
-    let extraData = parseKeyValuePairsFromFile(pluginExtraFile);
+
     ok("PluginCpuUsage" in extraData, "got extra field for plugin cpu usage");
     let cpuUsage = parseFloat(extraData["PluginCpuUsage"]);
     if (this.idleHang) {
       ok(cpuUsage == 0, "plugin cpu usage is 0%");
     } else {
       ok(cpuUsage > 0, "plugin cpu usage is >0%");
     }
-    
+
     // check processor count field
     ok("NumberOfProcessors" in extraData, "got extra field for processor count");
     ok(parseInt(extraData["NumberOfProcessors"]) > 0, "number of processors is >0");
 
     // cleanup, to be nice
     pluginDumpFile.remove(false);
-    browserDumpFile.remove(false);
     pluginExtraFile.remove(false);
-    browserExtraFile.remove(false);
+    for (let file of additionalDumpFiles) {
+      file.remove(false);
+    }
   },
 
   QueryInterface: function(iid) {
     if (iid.equals(Ci.nsIObserver) ||
         iid.equals(Ci.nsISupportsWeakReference) ||
         iid.equals(Ci.nsISupports))
       return this;
     throw Components.results.NS_NOINTERFACE;
--- a/dom/plugins/test/mochitest/test_crashing.html
+++ b/dom/plugins/test/mochitest/test_crashing.html
@@ -1,24 +1,25 @@
 <head>
   <title>Plugin crashing</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
 
 <body>
   <script class="testbody" type="application/javascript">
   SimpleTest.waitForExplicitFinish();
 
-  const isOSXLion = navigator.userAgent.indexOf("Mac OS X 10.7") != -1;
-const isOSXMtnLion = navigator.userAgent.indexOf("Mac OS X 10.8") != -1;
-  if (isOSXLion || isOSXMtnLion) {
-    todo(false, "Can't test plugin crash notification on OS X 10.7 or 10.8, see bug 705047");
-    SimpleTest.finish();
-  }
+  window.frameLoaded = function frameLoaded_toCrash() {
+    const isOSXLion = navigator.userAgent.indexOf("Mac OS X 10.7") != -1;
+    const isOSXMtnLion = navigator.userAgent.indexOf("Mac OS X 10.8") != -1;
+    if (isOSXLion || isOSXMtnLion) {
+      todo(false, "Can't test plugin crash notification on OS X 10.7 or 10.8, see bug 705047");
+      SimpleTest.finish();
+      return;
+    }
 
-  window.frameLoaded = function frameLoaded_toCrash() {
     if (!SimpleTest.testPluginIsOOP()) {
       ok(true, "Skipping this test when test plugin is not OOP.");
       SimpleTest.finish();
       return;
     }
 
     SimpleTest.expectChildProcessCrash();
 
--- a/testing/mochitest/specialpowers/content/specialpowers.js
+++ b/testing/mochitest/specialpowers/content/specialpowers.js
@@ -42,21 +42,19 @@ SpecialPowers.prototype.unregisterProces
   addMessageListener("SPProcessCrashService", this._messageListener);
   sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
 };
 
 SpecialPowers.prototype._messageReceived = function(aMessage) {
   switch (aMessage.name) {
     case "SPProcessCrashService":
       if (aMessage.json.type == "crash-observed") {
-        var self = this;
-        aMessage.json.dumpIDs.forEach(function(id) {
-          self._encounteredCrashDumpFiles.push(id + ".dmp");
-          self._encounteredCrashDumpFiles.push(id + ".extra");
-        });
+        for (let e of aMessage.json.dumpIDs) {
+          this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
+        }
       }
       break;
 
     case "SPPingService":
       if (aMessage.json.op == "pong") {
         var handler = this._pongHandlers.shift();
         if (handler) {
           handler();
--- a/testing/mochitest/tests/SimpleTest/ChromePowers.js
+++ b/testing/mochitest/tests/SimpleTest/ChromePowers.js
@@ -49,21 +49,19 @@ ChromePowers.prototype._receiveMessage =
       let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
       appStartup.quit(Ci.nsIAppStartup.eForceQuit);
       break;
     case "SPProcessCrashService":
       if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") {
         // Hack out register/unregister specifically for browser-chrome leaks
         break;
       } else if (aMessage.type == "crash-observed") {
-        var self = this;
-        msg.dumpIDs.forEach(function(id) {
-          self._encounteredCrashDumpFiles.push(id + ".dmp");
-          self._encounteredCrashDumpFiles.push(id + ".extra");
-        });
+        for (let e of msg.dumpIDs) {
+          this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
+        }
       }
     default:
       // All calls go here, because we need to handle SPProcessCrashService calls as well
       return this.spObserver._receiveMessageAPI(aMessage);
       break;
   }
 };
 
--- a/testing/mochitest/tests/SimpleTest/SpecialPowersObserverAPI.js
+++ b/testing/mochitest/tests/SimpleTest/SpecialPowersObserverAPI.js
@@ -1,12 +1,14 @@
 /* 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/. */
 
+Components.utils.import("resource://gre/modules/Services.jsm");
+
 /**
  * Special Powers Exception - used to throw exceptions nicely
  **/
 function SpecialPowersException(aMsg) {
   this.message = aMsg;
   this.name = "SpecialPowersException";
 }
 
@@ -14,52 +16,105 @@ SpecialPowersException.prototype.toStrin
   return this.name + ': "' + this.message + '"';
 };
 
 function SpecialPowersObserverAPI() {
   this._crashDumpDir = null;
   this._processCrashObserversRegistered = false;
 }
 
+function parseKeyValuePairs(text) {
+  var lines = text.split('\n');
+  var data = {};
+  for (let i = 0; i < lines.length; i++) {
+    if (lines[i] == '')
+      continue;
+
+    // can't just .split() because the value might contain = characters
+    let eq = lines[i].indexOf('=');
+    if (eq != -1) {
+      let [key, value] = [lines[i].substring(0, eq),
+                          lines[i].substring(eq + 1)];
+      if (key && value)
+        data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
+    }
+  }
+  return data;
+}
+
+function parseKeyValuePairsFromFile(file) {
+  var fstream = Cc["@mozilla.org/network/file-input-stream;1"].
+                createInstance(Ci.nsIFileInputStream);
+  fstream.init(file, -1, 0, 0);
+  var is = Cc["@mozilla.org/intl/converter-input-stream;1"].
+           createInstance(Ci.nsIConverterInputStream);
+  is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+  var str = {};
+  var contents = '';
+  while (is.readString(4096, str) != 0) {
+    contents += str.value;
+  }
+  is.close();
+  fstream.close();
+  return parseKeyValuePairs(contents);
+}
+
 SpecialPowersObserverAPI.prototype = {
 
   _observe: function(aSubject, aTopic, aData) {
     switch(aTopic) {
       case "plugin-crashed":
       case "ipc:content-shutdown":
         function addDumpIDToMessage(propertyName) {
           var id = aSubject.getPropertyAsAString(propertyName);
           if (id) {
-            message.dumpIDs.push(id);
+            message.dumpIDs.push({id: id, extension: "dmp"});
+            message.dumpIDs.push({id: id, extension: "extra"});
           }
         }
 
         var message = { type: "crash-observed", dumpIDs: [] };
-        aSubject = aSubject.QueryInterface(Components.interfaces.nsIPropertyBag2);
+        aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
         if (aTopic == "plugin-crashed") {
           addDumpIDToMessage("pluginDumpID");
           addDumpIDToMessage("browserDumpID");
+
+          let pluginID = aSubject.getPropertyAsAString("pluginDumpID");
+          let extra = this._getExtraData(pluginID);
+          if (extra && ("additional_minidumps" in extra)) {
+            let dumpNames = extra.additional_minidumps.split(',');
+            for (let name of dumpNames) {
+              message.dumpIDs.push({id: pluginID + "-" + name, extension: "dmp"});
+            }
+          }
         } else { // ipc:content-shutdown
           addDumpIDToMessage("dumpID");
         }
         this._sendAsyncMessage("SPProcessCrashService", message);
         break;
     }
   },
 
   _getCrashDumpDir: function() {
     if (!this._crashDumpDir) {
-      var directoryService = Components.classes["@mozilla.org/file/directory_service;1"]
-                             .getService(Components.interfaces.nsIProperties);
-      this._crashDumpDir = directoryService.get("ProfD", Components.interfaces.nsIFile);
+      this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
       this._crashDumpDir.append("minidumps");
     }
     return this._crashDumpDir;
   },
 
+  _getExtraData: function(dumpId) {
+    let extraFile = this._getCrashDumpDir().clone();
+    extraFile.append(dumpId + ".extra");
+    if (!extraFile.exists()) {
+      return null;
+    }
+    return parseKeyValuePairsFromFile(extraFile);
+  },
+
   _deleteCrashDumpFiles: function(aFilenames) {
     var crashDumpDir = this._getCrashDumpDir();
     if (!crashDumpDir.exists()) {
       return false;
     }
 
     var success = aFilenames.length != 0;
     aFilenames.forEach(function(crashFilename) {
@@ -78,40 +133,37 @@ SpecialPowersObserverAPI.prototype = {
     var crashDumpDir = this._getCrashDumpDir();
     var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
     if (!entries) {
       return [];
     }
 
     var crashDumpFiles = [];
     while (entries.hasMoreElements()) {
-      var file = entries.getNext().QueryInterface(Components.interfaces.nsIFile);
+      var file = entries.getNext().QueryInterface(Ci.nsIFile);
       var path = String(file.path);
       if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
         crashDumpFiles.push(path);
       }
     }
     return crashDumpFiles.concat();
   },
 
   _getURI: function (url) {
-    return Components.classes["@mozilla.org/network/io-service;1"]
-                     .getService(Components.interfaces.nsIIOService)
-                     .newURI(url, null, null);
+    return Services.io.newURI(url, null, null);
   },
 
   /**
    * messageManager callback function
    * This will get requests from our API in the window and process them in chrome for it
    **/
   _receiveMessageAPI: function(aMessage) {
     switch(aMessage.name) {
       case "SPPrefService":
-        var prefs = Components.classes["@mozilla.org/preferences-service;1"].
-                    getService(Components.interfaces.nsIPrefBranch);
+        var prefs = Services.prefs;
         var prefType = aMessage.json.prefType.toUpperCase();
         var prefName = aMessage.json.prefName;
         var prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null;
 
         if (aMessage.json.op == "get") {
           if (!prefName || !prefType)
             throw new SpecialPowersException("Invalid parameters for get in SPPrefService");
         } else if (aMessage.json.op == "set") {
@@ -167,30 +219,27 @@ SpecialPowersObserverAPI.prototype = {
           case "find-crash-dump-files":
             return this._findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore);
           default:
             throw new SpecialPowersException("Invalid operation for SPProcessCrashService");
         }
         break;
 
       case "SPPermissionManager":
-        let perms =
-          Components.classes["@mozilla.org/permissionmanager;1"]
-                    .getService(Components.interfaces.nsIPermissionManager);
         let msg = aMessage.json;
 
-        let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
+        let secMan = Services.scriptSecurityManager;
         let principal = secMan.getAppCodebasePrincipal(this._getURI(msg.url), msg.appId, msg.isInBrowserElement);
 
         switch (msg.op) {
           case "add":
-            perms.addFromPrincipal(principal, msg.type, msg.permission);
+            Services.perms.addFromPrincipal(principal, msg.type, msg.permission);
             break;
           case "remove":
-            perms.removeFromPrincipal(principal, msg.type);
+            Services.perms.removeFromPrincipal(principal, msg.type);
             break;
           default:
             throw new SpecialPowersException("Invalid operation for " +
                                              "SPPermissionManager");
         }
         break;
 
       default:
--- a/toolkit/crashreporter/CrashSubmit.jsm
+++ b/toolkit/crashreporter/CrashSubmit.jsm
@@ -1,13 +1,14 @@
 /* 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/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/KeyValueParser.jsm");
 
 let EXPORTED_SYMBOLS = [
   "CrashSubmit"
 ];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const STATE_START = Ci.nsIWebProgressListener.STATE_START;
@@ -16,52 +17,16 @@ const STATE_STOP = Ci.nsIWebProgressList
 const SUCCESS = "success";
 const FAILED  = "failed";
 const SUBMITTING = "submitting";
 
 let reportURL = null;
 let strings = null;
 let myListener = null;
 
-function parseKeyValuePairs(text) {
-  var lines = text.split('\n');
-  var data = {};
-  for (let i = 0; i < lines.length; i++) {
-    if (lines[i] == '')
-      continue;
-
-    // can't just .split() because the value might contain = characters
-    let eq = lines[i].indexOf('=');
-    if (eq != -1) {
-      let [key, value] = [lines[i].substring(0, eq),
-                          lines[i].substring(eq + 1)];
-      if (key && value)
-        data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
-    }
-  }
-  return data;
-}
-
-function parseKeyValuePairsFromFile(file) {
-  var fstream = Cc["@mozilla.org/network/file-input-stream;1"].
-                createInstance(Ci.nsIFileInputStream);
-  fstream.init(file, -1, 0, 0);
-  var is = Cc["@mozilla.org/intl/converter-input-stream;1"].
-           createInstance(Ci.nsIConverterInputStream);
-  is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
-  var str = {};
-  var contents = '';
-  while (is.readString(4096, str) != 0) {
-    contents += str.value;
-  }
-  is.close();
-  fstream.close();
-  return parseKeyValuePairs(contents);
-}
-
 function parseINIStrings(file) {
   var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
                 getService(Ci.nsIINIParserFactory);
   var parser = factory.createINIParser(file);
   var obj = {};
   var en = parser.getKeys("Strings");
   while (en.hasMore()) {
     var key = en.getNext();
@@ -154,16 +119,17 @@ function writeSubmittedReport(crashID, v
 }
 
 // the Submitter class represents an individual submission.
 function Submitter(id, submitSuccess, submitError, noThrottle) {
   this.id = id;
   this.successCallback = submitSuccess;
   this.errorCallback = submitError;
   this.noThrottle = noThrottle;
+  this.additionalDumps = [];
 }
 
 Submitter.prototype = {
   submitSuccess: function Submitter_submitSuccess(ret)
   {
     if (!ret.CrashID) {
       this.notifyStatus(FAILED);
       this.cleanup();
@@ -172,32 +138,36 @@ Submitter.prototype = {
 
     // Write out the details file to submitted/
     writeSubmittedReport(ret.CrashID, ret.ViewURL);
 
     // Delete from pending dir
     try {
       this.dump.remove(false);
       this.extra.remove(false);
+      for (let i of this.additionalDumps) {
+        i.dump.remove(false);
+      }
     }
     catch (ex) {
       // report an error? not much the user can do here.
     }
 
     this.notifyStatus(SUCCESS, ret);
     this.cleanup();
   },
 
   cleanup: function Submitter_cleanup() {
     // drop some references just to be nice
     this.successCallback = null;
     this.errorCallback = null;
     this.iframe = null;
     this.dump = null;
     this.extra = null;
+    this.additionalDumps = null;
     // remove this object from the list of active submissions
     let idx = CrashSubmit._activeSubmissions.indexOf(this);
     if (idx != -1)
       CrashSubmit._activeSubmissions.splice(idx, 1);
   },
 
   submitForm: function Submitter_submitForm()
   {
@@ -216,18 +186,29 @@ Submitter.prototype = {
     // add the other data
     for (let [name, value] in Iterator(reportData)) {
       formData.append(name, value);
     }
     if (this.noThrottle) {
       // tell the server not to throttle this, since it was manually submitted
       formData.append("Throttleable", "0");
     }
-    // add the minidump
+    // add the minidumps
     formData.append("upload_file_minidump", File(this.dump.path));
+    if (this.additionalDumps.length > 0) {
+      let names = [];
+      for (let i of this.additionalDumps) {
+        names.push(i.name);
+        formData.append("upload_file_minidump_"+i.name,
+                        File(i.dump.path));
+      }
+
+      formData.append("additional_minidumps", names.join(","));
+    }
+
     let self = this;
     xhr.addEventListener("readystatechange", function (aEvt) {
       if (xhr.readyState == 4) {
         if (xhr.status != 200) {
           self.notifyStatus(FAILED);
           self.cleanup();
         } else {
           let ret = parseKeyValuePairs(xhr.responseText);
@@ -269,20 +250,36 @@ Submitter.prototype = {
   {
     let [dump, extra] = getPendingMinidump(this.id);
     if (!dump.exists() || !extra.exists()) {
       this.notifyStatus(FAILED);
       this.cleanup();
       return false;
     }
 
+    let reportData = parseKeyValuePairsFromFile(extra);
+    let additionalDumps = [];
+    if ("additional_minidumps" in reportData) {
+      let names = extraData.additional_minidumps.split(',');
+      for (let name in names) {
+        let [dump, extra] = getPendingMiniDump(this.id + "-" + name);
+        if (!dump.exists()) {
+          this.notifyStatus(FAILED);
+          this.cleanup();
+          return false;
+        }
+        additionalDumps.push({'name': name, 'dump': dump});
+      }
+    }
+
     this.notifyStatus(SUBMITTING);
 
     this.dump = dump;
     this.extra = extra;
+    this.additionalDumps = additionalDumps;
 
     if (!this.submitForm()) {
        this.notifyStatus(FAILED);
        this.cleanup();
        return false;
     }
     return true;
   }
new file mode 100644
--- /dev/null
+++ b/toolkit/crashreporter/KeyValueParser.jsm
@@ -0,0 +1,49 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+let EXPORTED_SYMBOLS = [
+  "parseKeyValuePairs",
+  "parseKeyValuePairsFromFile"
+];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+function parseKeyValuePairs(text) {
+  let lines = text.split('\n');
+  let data = {};
+  for (let i = 0; i < lines.length; i++) {
+    if (lines[i] == '')
+      continue;
+
+    // can't just .split() because the value might contain = characters
+    let eq = lines[i].indexOf('=');
+    if (eq != -1) {
+      let [key, value] = [lines[i].substring(0, eq),
+                          lines[i].substring(eq + 1)];
+      if (key && value)
+        data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
+    }
+  }
+  return data;
+}
+
+function parseKeyValuePairsFromFile(file) {
+  let fstream = Cc["@mozilla.org/network/file-input-stream;1"].
+                createInstance(Ci.nsIFileInputStream);
+  fstream.init(file, -1, 0, 0);
+  let is = Cc["@mozilla.org/intl/converter-input-stream;1"].
+           createInstance(Ci.nsIConverterInputStream);
+  is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+  let str = {};
+  let contents = '';
+  while (is.readString(4096, str) != 0) {
+    contents += str.value;
+  }
+  is.close();
+  fstream.close();
+  return parseKeyValuePairs(contents);
+}
--- a/toolkit/crashreporter/Makefile.in
+++ b/toolkit/crashreporter/Makefile.in
@@ -93,16 +93,17 @@ CPPSRCS += \
   InjectCrashReporter.cpp \
   $(NULL)
 endif
 
 FORCE_STATIC_LIB = 1
 
 EXTRA_JS_MODULES = \
   CrashSubmit.jsm \
+  KeyValueParser.jsm \
   $(NULL)
 
 ifdef ENABLE_TESTS
 TOOL_DIRS = test
 endif
 
 include $(topsrcdir)/config/config.mk
 include $(topsrcdir)/ipc/chromium/chromium-config.mk
--- a/toolkit/crashreporter/nsExceptionHandler.cpp
+++ b/toolkit/crashreporter/nsExceptionHandler.cpp
@@ -2041,18 +2041,25 @@ WriteExtraForMinidump(nsIFile* minidump,
 // ShouldReport() is true.
 static bool
 MoveToPending(nsIFile* dumpFile, nsIFile* extraFile)
 {
   nsCOMPtr<nsIFile> pendingDir;
   if (!GetPendingDir(getter_AddRefs(pendingDir)))
     return false;
 
-  return NS_SUCCEEDED(dumpFile->MoveTo(pendingDir, EmptyString())) &&
-    NS_SUCCEEDED(extraFile->MoveTo(pendingDir, EmptyString()));
+  if (NS_FAILED(dumpFile->MoveTo(pendingDir, EmptyString()))) {
+    return false;
+  }
+
+  if (extraFile && NS_FAILED(extraFile->MoveTo(pendingDir, EmptyString()))) {
+    return false;
+  }
+
+  return true;
 }
 
 static void
 OnChildProcessDumpRequested(void* aContext,
 #ifdef XP_MACOSX
                             const ClientInfo& aClientInfo,
                             const xpstring& aFilePath
 #else
@@ -2508,44 +2515,76 @@ TakeMinidumpForChild(uint32_t childPid, 
   pidToMinidump->RemoveEntry(childPid);
 
   return !!*dump;
 }
 
 //-----------------------------------------------------------------------------
 // CreatePairedMinidumps() and helpers
 //
-struct PairedDumpContext {
-  nsCOMPtr<nsIFile>* minidump;
-  nsCOMPtr<nsIFile>* extra;
-  const Blacklist& blacklist;
-};
+
+void
+RenameAdditionalHangMinidump(nsIFile* minidump, nsIFile* childMinidump,
+                           const nsACString& name)
+{
+  nsCOMPtr<nsIFile> directory;
+  childMinidump->GetParent(getter_AddRefs(directory));
+  if (!directory)
+    return;
+
+  nsAutoCString leafName;
+  childMinidump->GetNativeLeafName(leafName);
+
+  // turn "<id>.dmp" into "<id>-<name>.dmp
+  leafName.Insert(NS_LITERAL_CSTRING("-") + name, leafName.Length() - 4);
+
+  minidump->MoveToNative(directory, leafName);
+}
 
 static bool
 PairedDumpCallback(const XP_CHAR* dump_path,
                    const XP_CHAR* minidump_id,
                    void* context,
 #ifdef XP_WIN32
                    EXCEPTION_POINTERS* /*unused*/,
                    MDRawAssertionInfo* /*unused*/,
 #endif
                    bool succeeded)
 {
-  PairedDumpContext* ctx = static_cast<PairedDumpContext*>(context);
-  nsCOMPtr<nsIFile>& minidump = *ctx->minidump;
-  nsCOMPtr<nsIFile>& extra = *ctx->extra;
-  const Blacklist& blacklist = ctx->blacklist;
+  nsCOMPtr<nsIFile>& minidump = *static_cast< nsCOMPtr<nsIFile>* >(context);
 
   xpstring dump(dump_path);
   dump += XP_PATH_SEPARATOR;
   dump += minidump_id;
   dump += dumpFileExtension;
 
   CreateFileFromPath(dump, getter_AddRefs(minidump));
-  return WriteExtraForMinidump(minidump, blacklist, getter_AddRefs(extra));
+  return true;
+}
+
+static bool
+PairedDumpCallbackExtra(const XP_CHAR* dump_path,
+                        const XP_CHAR* minidump_id,
+                        void* context,
+#ifdef XP_WIN32
+                        EXCEPTION_POINTERS* /*unused*/,
+                        MDRawAssertionInfo* /*unused*/,
+#endif
+                        bool succeeded)
+{
+  PairedDumpCallback(dump_path, minidump_id, context,
+#ifdef XP_WIN32
+                     nullptr, nullptr,
+#endif
+                     succeeded);
+
+  nsCOMPtr<nsIFile>& minidump = *static_cast< nsCOMPtr<nsIFile>* >(context);
+
+  nsCOMPtr<nsIFile> extra;
+  return WriteExtraForMinidump(minidump, Blacklist(), getter_AddRefs(extra));
 }
 
 ThreadId
 CurrentThreadId()
 {
 #if defined(XP_WIN)
   return ::GetCurrentThreadId();
 #elif defined(XP_LINUX)
@@ -2566,93 +2605,71 @@ CurrentThreadId()
 #else
 #  error "Unsupported platform"
 #endif
 }
 
 bool
 CreatePairedMinidumps(ProcessHandle childPid,
                       ThreadId childBlamedThread,
-                      nsAString* pairGUID,
-                      nsIFile** childDump,
-                      nsIFile** parentDump)
+                      nsIFile** childDump)
 {
   if (!GetEnabled())
     return false;
 
-  // create the UUID for the hang dump as a pair
-  nsresult rv;
-  nsCOMPtr<nsIUUIDGenerator> uuidgen =
-    do_GetService("@mozilla.org/uuid-generator;1", &rv);
-  NS_ENSURE_SUCCESS(rv, false);  
-
-  nsID id;
-  rv = uuidgen->GenerateUUIDInPlace(&id);
-  NS_ENSURE_SUCCESS(rv, false);
-  
-  char chars[NSID_LENGTH];
-  id.ToProvidedString(chars);
-  CopyASCIItoUTF16(chars, *pairGUID);
-
-  // trim off braces
-  pairGUID->Cut(0, 1);
-  pairGUID->Cut(pairGUID->Length()-1, 1);
-
 #ifdef XP_MACOSX
   mach_port_t childThread = MACH_PORT_NULL;
   thread_act_port_array_t   threads_for_task;
   mach_msg_type_number_t    thread_count;
 
   if (task_threads(childPid, &threads_for_task, &thread_count)
       == KERN_SUCCESS && childBlamedThread < thread_count) {
     childThread = threads_for_task[childBlamedThread];
   }
 #else
   ThreadId childThread = childBlamedThread;
 #endif
 
   // dump the child
   nsCOMPtr<nsIFile> childMinidump;
-  nsCOMPtr<nsIFile> childExtra;
-  Blacklist childBlacklist(kSubprocessBlacklist,
-                           ArrayLength(kSubprocessBlacklist));
-  PairedDumpContext childCtx =
-    { &childMinidump, &childExtra, childBlacklist };
   if (!google_breakpad::ExceptionHandler::WriteMinidumpForChild(
          childPid,
          childThread,
          gExceptionHandler->dump_path(),
-         PairedDumpCallback,
-         &childCtx))
+         PairedDumpCallbackExtra,
+         static_cast<void*>(&childMinidump)))
     return false;
 
+  nsCOMPtr<nsIFile> childExtra;
+  GetExtraFileForMinidump(childMinidump, getter_AddRefs(childExtra));
+
   // dump the parent
   nsCOMPtr<nsIFile> parentMinidump;
-  nsCOMPtr<nsIFile> parentExtra;
-  // nothing's blacklisted for this process
-  Blacklist parentBlacklist;
-  PairedDumpContext parentCtx =
-    { &parentMinidump, &parentExtra, parentBlacklist };
   if (!google_breakpad::ExceptionHandler::WriteMinidump(
          gExceptionHandler->dump_path(),
          true,                  // write exception stream
          PairedDumpCallback,
-         &parentCtx))
+         static_cast<void*>(&parentMinidump))) {
+
+    childMinidump->Remove(false);
+    childExtra->Remove(false);
+
     return false;
+  }
 
   // success
+  RenameAdditionalHangMinidump(parentMinidump, childMinidump,
+                               NS_LITERAL_CSTRING("browser"));
+
   if (ShouldReport()) {
     MoveToPending(childMinidump, childExtra);
-    MoveToPending(parentMinidump, parentExtra);
+    MoveToPending(parentMinidump, nullptr);
   }
 
-  *childDump = NULL;
-  *parentDump = NULL;
-  childMinidump.swap(*childDump);
-  parentMinidump.swap(*parentDump);
+  childMinidump.forget(childDump);
 
   return true;
 }
 
 bool
 UnsetRemoteExceptionHandler()
 {
   delete gExceptionHandler;
--- a/toolkit/crashreporter/nsExceptionHandler.h
+++ b/toolkit/crashreporter/nsExceptionHandler.h
@@ -55,16 +55,18 @@ nsresult UnregisterAppMemory(void* ptr);
 typedef nsDataHashtable<nsCStringHashKey, nsCString> AnnotationTable;
 
 bool GetMinidumpForID(const nsAString& id, nsIFile** minidump);
 bool GetIDFromMinidump(nsIFile* minidump, nsAString& id);
 bool GetExtraFileForID(const nsAString& id, nsIFile** extraFile);
 bool GetExtraFileForMinidump(nsIFile* minidump, nsIFile** extraFile);
 bool AppendExtraData(const nsAString& id, const AnnotationTable& data);
 bool AppendExtraData(nsIFile* extraFile, const AnnotationTable& data);
+void RenameAdditionalHangMinidump(nsIFile* minidump, nsIFile* childMinidump,
+                                  const nsACString& name);
 
 #ifdef XP_WIN32
   nsresult WriteMinidumpForException(EXCEPTION_POINTERS* aExceptionInfo);
 #endif
 #ifdef XP_MACOSX
   nsresult AppendObjCExceptionInfoToAppNotes(void *inException);
 #endif
 nsresult GetSubmitReports(bool* aSubmitReport);
@@ -98,28 +100,29 @@ typedef int ThreadId;
 // Return the current thread's ID.
 //
 // XXX: this is a somewhat out-of-place interface to expose through
 // crashreporter, but it takes significant work to call sys_gettid()
 // correctly on Linux and breakpad has already jumped through those
 // hoops for us.
 ThreadId CurrentThreadId();
 
-// Create new minidumps that are snapshots of the state of this parent
-// process and |childPid|.  Return true on success along with the
-// minidumps and a new UUID that can be used to correlate the dumps.
+// Create a hang report with two minidumps that are snapshots of the state
+// of this parent process and |childPid|. The "main" minidump will be the
+// child process, and this parent process will have the _browser extension.
 //
-// If this function fails, it's the caller's responsibility to clean
-// up |childDump| and |parentDump|.  Either or both can be created and
-// returned non-null on failure.
+// Returns true on success. If this function fails, it will attempt to delete
+// any files that were created.
+//
+// The .extra information created will not include an additional_minidumps
+// annotation: the caller should annotate additional_minidumps with
+// at least "browser" and perhaps other minidumps attached to this report.
 bool CreatePairedMinidumps(ProcessHandle childPid,
                            ThreadId childBlamedThread,
-                           nsAString* pairGUID,
-                           nsIFile** childDump,
-                           nsIFile** parentDump);
+                           nsIFile** childDump);
 
 #  if defined(XP_WIN32) || defined(XP_MACOSX)
 // Parent-side API for children
 const char* GetChildNotificationPipe();
 
 #ifdef MOZ_CRASHREPORTER_INJECTOR
 // Inject a crash report client into an arbitrary process, and inform the
 // callback object when it crashes. Parent process only.
--- a/toolkit/crashreporter/test/unit/head_crashreporter.js
+++ b/toolkit/crashreporter/test/unit/head_crashreporter.js
@@ -139,47 +139,11 @@ function do_content_crash(setup, callbac
     sendCommand(setup, function()
       sendCommand("load(\"" + tailfile.path.replace(/\\/g, "/") + "\");",
         function() do_execute_soon(handleCrash)
       )
     )
   );
 }
 
-// Utility functions for parsing .extra files
-function parseKeyValuePairs(text) {
-  var lines = text.split('\n');
-  var data = {};
-  for (let i = 0; i < lines.length; i++) {
-    if (lines[i] == '')
-      continue;
-
-    // can't just .split() because the value might contain = characters
-    let eq = lines[i].indexOf('=');
-    if (eq != -1) {
-      let [key, value] = [lines[i].substring(0, eq),
-                          lines[i].substring(eq + 1)];
-      if (key && value)
-        data[key] = value.replace("\\n", "\n", "g").replace("\\\\", "\\", "g");
-    }
-  }
-  return data;
-}
-
-function parseKeyValuePairsFromFile(file) {
-  var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
-                createInstance(Components.interfaces.nsIFileInputStream);
-  fstream.init(file, -1, 0, 0);
-  var is = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
-           createInstance(Components.interfaces.nsIConverterInputStream);
-  is.init(fstream, "UTF-8", 1024, Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
-  var str = {};
-  var contents = '';
-  while (is.readString(4096, str) != 0) {
-    contents += str.value;
-  }
-  is.close();
-  fstream.close();
-  return parseKeyValuePairs(contents);
-}
-
 // Import binary APIs via js-ctypes.
 Components.utils.import("resource://test/CrashTestUtils.jsm");
+Components.utils.import("resource://gre/modules/KeyValueParser.jsm");