Bug 1293656 - Send crash pings for content crashes complete with stack traces r=bsmedberg
☠☠ backed out by 93d785448a69 ☠ ☠
authorGabriele Svelto <gsvelto@mozilla.com>
Wed, 19 Oct 2016 12:51:29 +0200
changeset 325261 c8545ffbd0cb18f7ae3a5810f5b98ec0ba8be0e0
parent 325260 f59861923f8169c003f0f7f8bd50e3fae52e0b51
child 325262 d2ba2e62f871b632535cf8de373f26da1fe649c1
push id31045
push usercbook@mozilla.com
push dateTue, 06 Dec 2016 14:50:04 +0000
treeherdermozilla-central@deacac546efa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbsmedberg
bugs1293656
milestone53.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 1293656 - Send crash pings for content crashes complete with stack traces r=bsmedberg
ipc/glue/CrashReporterHost.cpp
ipc/glue/CrashReporterHost.h
toolkit/components/crashes/CrashManager.jsm
toolkit/components/crashes/CrashService.js
toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
toolkit/crashreporter/docs/index.rst
toolkit/crashreporter/nsExceptionHandler.cpp
toolkit/crashreporter/nsExceptionHandler.h
--- a/ipc/glue/CrashReporterHost.cpp
+++ b/ipc/glue/CrashReporterHost.cpp
@@ -1,20 +1,22 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "CrashReporterHost.h"
 #include "CrashReporterMetadataShmem.h"
+#include "mozilla/LazyIdleThread.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/SyncRunnable.h"
 #include "mozilla/Telemetry.h"
 #ifdef MOZ_CRASHREPORTER
+# include "nsIAsyncShutdown.h"
 # include "nsICrashService.h"
 #endif
 
 namespace mozilla {
 namespace ipc {
 
 CrashReporterHost::CrashReporterHost(GeckoProcessType aProcessType,
                                      const Shmem& aShmem)
@@ -58,38 +60,173 @@ CrashReporterHost::GenerateCrashReport(R
   notes.Put(NS_LITERAL_CSTRING("StartupTime"), nsDependentCString(startTime));
 
   CrashReporterMetadataShmem::ReadAppNotes(mShmem, &notes);
 
   CrashReporter::AppendExtraData(dumpID, notes);
   NotifyCrashService(mProcessType, dumpID, &notes);
 }
 
+/**
+ * Runnable used to execute the minidump analyzer program asynchronously after
+ * a crash. This should run on a background thread not to block the main thread
+ * over the potentially long minidump analysis. Once analysis is over, the
+ * crash information will be relayed to the crash manager via another runnable
+ * sent to the main thread. Shutdown will be blocked for the duration of the
+ * entire process to ensure this information is sent.
+ */
+class AsyncMinidumpAnalyzer final : public nsIRunnable
+                                  , public nsIAsyncShutdownBlocker
+{
+public:
+  /**
+   * Create a new minidump analyzer runnable, this will also block shutdown
+   * until the associated crash has been added to the crash manager.
+   */
+  AsyncMinidumpAnalyzer(int32_t aProcessType,
+                        int32_t aCrashType,
+                        const nsString& aChildDumpID)
+    : mProcessType(aProcessType)
+    , mCrashType(aCrashType)
+    , mChildDumpID(aChildDumpID)
+    , mName(NS_LITERAL_STRING("Crash reporter: blocking on minidump analysis"))
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsresult rv = GetShutdownBarrier()->AddBlocker(
+      this, NS_LITERAL_STRING(__FILE__), __LINE__,
+      NS_LITERAL_STRING("Minidump analysis"));
+    MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+  }
+
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+
+    if (mProcessType == nsICrashService::PROCESS_TYPE_CONTENT) {
+      CrashReporter::RunMinidumpAnalyzer(mChildDumpID);
+    }
+
+    // Make a copy of these so we can copy them into the runnable lambda
+    int32_t processType = mProcessType;
+    int32_t crashType = mCrashType;
+    nsString childDumpID(mChildDumpID);
+
+    NS_DispatchToMainThread(NS_NewRunnableFunction([=] () -> void {
+      nsCOMPtr<nsICrashService> crashService =
+        do_GetService("@mozilla.org/crashservice;1");
+      if (crashService) {
+        crashService->AddCrash(processType, crashType, childDumpID);
+      }
+
+      nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+
+      if (barrier) {
+        barrier->RemoveBlocker(this);
+      }
+    }));
+
+    return NS_OK;
+  }
+
+  NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aBarrierClient) override
+  {
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetName(nsAString& aName) override
+  {
+    aName = mName;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetState(nsIPropertyBag**) override
+  {
+    return NS_OK;
+  }
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+private:
+  ~AsyncMinidumpAnalyzer() {}
+
+  // Returns the profile-before-change shutdown barrier
+  static nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
+    nsCOMPtr<nsIAsyncShutdownClient> barrier;
+    nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier));
+
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return nullptr;
+    }
+
+    return barrier.forget();
+  }
+
+  int32_t mProcessType;
+  int32_t mCrashType;
+  const nsString mChildDumpID;
+  const nsString mName;
+};
+
+NS_IMPL_ISUPPORTS(AsyncMinidumpAnalyzer, nsIRunnable, nsIAsyncShutdownBlocker)
+
+/**
+ * Add information about a crash to the crash manager. This method runs the
+ * various activities required to gather additional information and notify the
+ * crash manager asynchronously, since many of them involve I/O or potentially
+ * long processing.
+ *
+ * @param aProcessType The type of process that crashed
+ * @param aCrashType The type of crash (crash or hang)
+ * @param aChildDumpID A string holding the ID of the dump associated with this
+ *        crash
+ */
+/* static */ void
+CrashReporterHost::AsyncAddCrash(int32_t aProcessType,
+                                 int32_t aCrashType,
+                                 const nsString& aChildDumpID)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  static StaticRefPtr<LazyIdleThread> sBackgroundThread;
+
+  if (!sBackgroundThread) {
+    // Background thread used for running minidump analysis. It will be
+    // destroyed immediately after it's done with the task.
+    sBackgroundThread =
+      new LazyIdleThread(0, NS_LITERAL_CSTRING("CrashReporterHost"));
+    ClearOnShutdown(&sBackgroundThread);
+  }
+
+  RefPtr<AsyncMinidumpAnalyzer> task =
+    new AsyncMinidumpAnalyzer(aProcessType, aCrashType, aChildDumpID);
+
+  Unused << sBackgroundThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
 /* static */ void
 CrashReporterHost::NotifyCrashService(GeckoProcessType aProcessType,
                                       const nsString& aChildDumpID,
                                       const AnnotationTable* aNotes)
 {
   if (!NS_IsMainThread()) {
     RefPtr<Runnable> runnable = NS_NewRunnableFunction([=] () -> void {
       CrashReporterHost::NotifyCrashService(aProcessType, aChildDumpID, aNotes);
     });
     RefPtr<nsIThread> mainThread = do_GetMainThread();
     SyncRunnable::DispatchToThread(mainThread, runnable);
     return;
   }
 
   MOZ_ASSERT(!aChildDumpID.IsEmpty());
 
-  nsCOMPtr<nsICrashService> crashService =
-    do_GetService("@mozilla.org/crashservice;1");
-  if (!crashService) {
-    return;
-  }
-
   int32_t processType;
   int32_t crashType = nsICrashService::CRASH_TYPE_CRASH;
 
   nsCString telemetryKey;
 
   switch (aProcessType) {
     case GeckoProcessType_Content:
       processType = nsICrashService::PROCESS_TYPE_CONTENT;
@@ -114,15 +251,15 @@ CrashReporterHost::NotifyCrashService(Ge
       processType = nsICrashService::PROCESS_TYPE_GPU;
       telemetryKey.AssignLiteral("gpu");
       break;
     default:
       NS_ERROR("unknown process type");
       return;
   }
 
-  crashService->AddCrash(processType, crashType, aChildDumpID);
+  AsyncAddCrash(processType, crashType, aChildDumpID);
   Telemetry::Accumulate(Telemetry::SUBPROCESS_CRASHES_WITH_DUMP, telemetryKey, 1);
 }
 #endif
 
 } // namespace ipc
 } // namespace mozilla
--- a/ipc/glue/CrashReporterHost.h
+++ b/ipc/glue/CrashReporterHost.h
@@ -45,16 +45,18 @@ public:
   static void NotifyCrashService(
     GeckoProcessType aProcessType,
     const nsString& aChildDumpID,
     const AnnotationTable* aNotes);
 #endif
 
 private:
   void GenerateCrashReport(RefPtr<nsIFile> aCrashDump);
+  static void AsyncAddCrash(int32_t aProcessType, int32_t aCrashType,
+                            const nsString& aChildDumpID);
 
 private:
   GeckoProcessType mProcessType;
   Shmem mShmem;
   time_t mStartTime;
 };
 
 } // namespace ipc
--- a/toolkit/components/crashes/CrashManager.jsm
+++ b/toolkit/components/crashes/CrashManager.jsm
@@ -139,16 +139,19 @@ this.CrashManager = function(options) {
 
   // Promise for in-progress aggregation operation. We store it on the
   // object so it can be returned for in-progress operations.
   this._aggregatePromise = null;
 
   // Map of crash ID / promise tuples used to track adding new crashes.
   this._crashPromises = new Map();
 
+  // Promise for the crash ping used only for testing.
+  this._pingPromise = null;
+
   // The CrashStore currently attached to this object.
   this._store = null;
 
   // A Task to retrieve the store. This is needed to avoid races when
   // _getStore() is called multiple times in a short interval.
   this._getStoreTask = null;
 
   // The timer controlling the expiration of the CrashStore instance.
@@ -444,17 +447,22 @@ this.CrashManager.prototype = Object.fre
       }
 
       let deferred = this._crashPromises.get(id);
 
       if (deferred) {
         this._crashPromises.delete(id);
         deferred.resolve();
       }
-    }.bind(this));
+
+      // Send a telemetry ping for each content process crash
+      if (processType === this.PROCESS_TYPE_CONTENT) {
+        this._sendCrashPing(id, processType, date, metadata);
+      }
+   }.bind(this));
 
     return promise;
   },
 
   /**
    * Returns a promise that is resolved only the crash with the specified id
    * has been fully recorded.
    *
@@ -604,42 +612,42 @@ this.CrashManager.prototype = Object.fre
       }
       let date = new Date(time * 1000);
       let payload = data.substring(start);
 
       return this._handleEventFilePayload(store, entry, type, date, payload);
     }.bind(this));
   },
 
-  _filterAnnotations: function (annotations) {
+  _filterAnnotations: function(annotations) {
     let filteredAnnotations = {};
 
     for (let line in annotations) {
       if (this.ANNOTATION_WHITELIST.includes(line)) {
         filteredAnnotations[line] = annotations[line];
       }
     }
 
     return filteredAnnotations;
   },
 
-  _sendCrashPing: function (crashId, type, date, metadata) {
+  _sendCrashPing: function(crashId, type, date, metadata = {}) {
     // If we have a saved environment, use it. Otherwise report
     // the current environment.
     let reportMeta = Cu.cloneInto(metadata, myScope);
     let crashEnvironment = parseAndRemoveField(reportMeta,
                                                "TelemetryEnvironment");
     let sessionId = parseAndRemoveField(reportMeta, "TelemetrySessionId",
                                         /* parseAsJson */ false);
     let stackTraces = parseAndRemoveField(reportMeta, "StackTraces");
 
     // Filter the remaining annotations to remove privacy-sensitive ones
     reportMeta = this._filterAnnotations(reportMeta);
 
-    TelemetryController.submitExternalPing("crash",
+    this._pingPromise = TelemetryController.submitExternalPing("crash",
       {
         version: 1,
         crashDate: date.toISOString().slice(0, 10), // YYYY-MM-DD
         sessionId: sessionId,
         crashId: crashId,
         processType: type,
         stackTraces: stackTraces,
         metadata: reportMeta,
--- a/toolkit/components/crashes/CrashService.js
+++ b/toolkit/components/crashes/CrashService.js
@@ -1,20 +1,55 @@
 /* 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";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
+Cu.import("resource://gre/modules/KeyValueParser.jsm");
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 
 /**
+ * Process the .extra file associated with the crash id and return the
+ * annotations it contains in an object.
+ *
+ * @param crashID {string} Crash ID. Likely a UUID.
+ *
+ * @return {Promise} A promise that resolves to an object holding the crash
+ *         annotations, this object may be empty if no annotations were found.
+ */
+function processExtraFile(id) {
+  let crDir = OS.Path.join(OS.Constants.Path.userApplicationDataDir,
+                           "Crash Reports");
+  let pendingDumpsDir = OS.Path.join(crDir, "pending");
+  let extraPath = OS.Path.join(pendingDumpsDir, id + ".extra");
+
+  return Task.spawn(function* () {
+    try {
+      let decoder = new TextDecoder();
+      let extraFile = yield OS.File.read(extraPath);
+      let extraData = decoder.decode(extraFile);
+
+      return parseKeyValuePairs(extraData);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+
+    return {};
+  });
+}
+
+/**
  * This component makes crash data available throughout the application.
  *
  * It is a service because some background activity will eventually occur.
  */
 this.CrashService = function() {};
 
 CrashService.prototype = Object.freeze({
   classID: Components.ID("{92668367-1b17-4190-86b2-1061b2179744}"),
@@ -50,17 +85,23 @@ CrashService.prototype = Object.freeze({
       break;
     case Ci.nsICrashService.CRASH_TYPE_HANG:
       crashType = Services.crashmanager.CRASH_TYPE_HANG;
       break;
     default:
       throw new Error("Unrecognized CRASH_TYPE: " + crashType);
     }
 
-    Services.crashmanager.addCrash(processType, crashType, id, new Date());
+    AsyncShutdown.profileBeforeChange.addBlocker(
+      "CrashService waiting for content crash ping to be sent",
+      processExtraFile(id).then(metadata => {
+        return Services.crashmanager.addCrash(processType, crashType, id,
+                                              new Date(), metadata)
+      })
+    );
   },
 
   observe: function(subject, topic, data) {
     switch (topic) {
       case "profile-after-change":
         // Side-effect is the singleton is instantiated.
         Services.crashmanager;
         break;
--- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
@@ -207,35 +207,35 @@ add_task(function* test_schedule_mainten
   let crashes = yield m.getCrashes();
   Assert.equal(crashes.length, 1);
   Assert.equal(crashes[0].id, "id1");
 });
 
 const crashId = "3cb67eba-0dc7-6f78-6a569a0e-172287ec";
 const productName = "Firefox";
 const productId = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+const stackTraces = "{\"status\":\"OK\"}";
 
 add_task(function* test_main_crash_event_file() {
   let ac = new TelemetryArchiveTesting.Checker();
   yield ac.promiseInit();
   let theEnvironment = TelemetryEnvironment.currentEnvironment;
   const sessionId = "be66af2f-2ee5-4330-ae95-44462dfbdf0c";
-  let stackTraces = { status: "OK" };
 
   // To test proper escaping, add data to the environment with an embedded
   // double-quote
   theEnvironment.testValue = "MyValue\"";
 
   let m = yield getManager();
   const fileContent = crashId + "\n" +
     "ProductName=" + productName + "\n" +
     "ProductID=" + productId + "\n" +
     "TelemetryEnvironment=" + JSON.stringify(theEnvironment) + "\n" +
     "TelemetrySessionId=" + sessionId + "\n" +
-    "StackTraces=" + JSON.stringify(stackTraces) + "\n" +
+    "StackTraces=" + stackTraces + "\n" +
     "ThisShouldNot=end-up-in-the-ping\n";
 
   yield m.createEventsFile(crashId, "crash.main.2", DUMMY_DATE, fileContent);
   let count = yield m.aggregateEventsFiles();
   Assert.equal(count, 1);
 
   let crashes = yield m.getCrashes();
   Assert.equal(crashes.length, 1);
@@ -453,16 +453,38 @@ add_task(function* test_addCrash() {
 
   crash = map.get("changing-item");
   Assert.ok(!!crash);
   Assert.equal(crash.crashDate, DUMMY_DATE_2);
   Assert.equal(crash.type, m.PROCESS_TYPE_CONTENT + "-" + m.CRASH_TYPE_HANG);
   Assert.ok(crash.isOfType(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_HANG));
 });
 
+add_task(function* test_content_crash_ping() {
+  let ac = new TelemetryArchiveTesting.Checker();
+  yield ac.promiseInit();
+
+  let m = yield getManager();
+  let id = yield m.createDummyDump();
+  yield m.addCrash(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_CRASH, id, DUMMY_DATE, {
+    StackTraces: stackTraces,
+    ThisShouldNot: "end-up-in-the-ping"
+  });
+  yield m._pingPromise;
+
+  let found = yield ac.promiseFindPing("crash", [
+    [["payload", "crashId"], id],
+    [["payload", "processType"], m.PROCESS_TYPE_CONTENT],
+    [["payload", "stackTraces", "status"], "OK"],
+  ]);
+  Assert.ok(found, "Telemetry ping submitted for content crash");
+  Assert.equal(found.payload.metadata.ThisShouldNot, undefined,
+               "Non-whitelisted fields should be filtered out");
+});
+
 add_task(function* test_generateSubmissionID() {
   let m = yield getManager();
 
   const SUBMISSION_ID_REGEX =
     /^(sub-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
   let id = m.generateSubmissionID();
   Assert.ok(SUBMISSION_ID_REGEX.test(id));
 });
--- a/toolkit/crashreporter/docs/index.rst
+++ b/toolkit/crashreporter/docs/index.rst
@@ -125,16 +125,25 @@ actor should check for it by calling Tak
 Method: see ``mozilla::plugins::PluginModuleParent::ActorDestroy`` and
 ``mozilla::plugins::PluginModuleParent::ProcessFirstMinidump``. That method
 is responsible for calling
 ``mozilla::dom::CrashReporterParent::GenerateCrashReportForMinidump`` with
 appropriate crash annotations specific to the crash. All child-process
 crashes are annotated with a ``ProcessType`` annotation, such as "content" or
 "plugin".
 
+Once the minidump file has been generated the
+``mozilla::dom::CrashReporterHost`` is notified of the crash. It will first
+try to extract the stack traces from the minidump file using the
+*minidump analyzer*. Then the stack traces will be stored in the extra file
+together with the rest of the crash annotations and finally the crash will be
+recorded by calling ```CrashService.addCrash()```. This last step adds the
+crash to the ```CrashManager``` database and automatically sends a crash ping
+with information about the crash.
+
 Submission of child process crashes is handled by application code. This
 code prompts the user to submit crashes in context-appropriate UI and then
 submits the crashes using ``CrashSubmit.jsm``.
 
 Memory Reports
 ==============
 
 When a process detects that it is running low on memory, a memory report is
--- a/toolkit/crashreporter/nsExceptionHandler.cpp
+++ b/toolkit/crashreporter/nsExceptionHandler.cpp
@@ -3,16 +3,17 @@
 /* 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/. */
 
 #include "nsExceptionHandler.h"
 
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryService.h"
 #include "nsDataHashtable.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/dom/CrashReporterChild.h"
 #include "mozilla/ipc/CrashReporterClient.h"
 #include "mozilla/Services.h"
 #include "nsIObserverService.h"
 #include "mozilla/Unused.h"
 #include "mozilla/Sprintf.h"
@@ -124,16 +125,17 @@ namespace CrashReporter {
 #ifdef XP_WIN32
 typedef wchar_t XP_CHAR;
 typedef std::wstring xpstring;
 #define XP_TEXT(x) L##x
 #define CONVERT_XP_CHAR_TO_UTF16(x) x
 #define XP_STRLEN(x) wcslen(x)
 #define my_strlen strlen
 #define CRASH_REPORTER_FILENAME "crashreporter.exe"
+#define MINIDUMP_ANALYZER_FILENAME "minidump-analyzer.exe"
 #define PATH_SEPARATOR "\\"
 #define XP_PATH_SEPARATOR L"\\"
 #define XP_PATH_SEPARATOR_CHAR L'\\'
 #define XP_PATH_MAX (MAX_PATH + 1)
 // "<reporter path>" "<minidump path>"
 #define CMDLINE_SIZE ((XP_PATH_MAX * 2) + 6)
 #ifdef _USE_32BIT_TIME_T
 #define XP_TTOA(time, buffer, base) ltoa(time, buffer, base)
@@ -142,16 +144,17 @@ typedef std::wstring xpstring;
 #endif
 #define XP_STOA(size, buffer, base) _ui64toa(size, buffer, base)
 #else
 typedef char XP_CHAR;
 typedef std::string xpstring;
 #define XP_TEXT(x) x
 #define CONVERT_XP_CHAR_TO_UTF16(x) NS_ConvertUTF8toUTF16(x)
 #define CRASH_REPORTER_FILENAME "crashreporter"
+#define MINIDUMP_ANALYZER_FILENAME "minidump-analyzer"
 #define PATH_SEPARATOR "/"
 #define XP_PATH_SEPARATOR "/"
 #define XP_PATH_SEPARATOR_CHAR '/'
 #define XP_PATH_MAX PATH_MAX
 #ifdef XP_LINUX
 #define XP_STRLEN(x) my_strlen(x)
 #define XP_TTOA(time, buffer, base) my_inttostring(time, buffer, sizeof(buffer))
 #define XP_STOA(size, buffer, base) my_inttostring(size, buffer, sizeof(buffer))
@@ -185,16 +188,19 @@ static xpstring *defaultMemoryReportPath
 
 static const char kCrashMainID[] = "crash.main.2\n";
 
 static google_breakpad::ExceptionHandler* gExceptionHandler = nullptr;
 
 static XP_CHAR* pendingDirectory;
 static XP_CHAR* crashReporterPath;
 static XP_CHAR* memoryReportPath;
+#if !defined(MOZ_WIDGET_ANDROID)
+static XP_CHAR* minidumpAnalyzerPath;
+#endif // !defined(MOZ_WIDGET_ANDROID)
 
 // Where crash events should go.
 static XP_CHAR* eventsDirectory;
 static char* eventsEnv = nullptr;
 
 // The current telemetry session ID to write to the event file
 static char* currentSessionId = nullptr;
 
@@ -1531,16 +1537,42 @@ ChildFilter(void* context)
 {
   bool result = Filter(context);
   if (result) {
     PrepareChildExceptionTimeAnnotations();
   }
   return result;
 }
 
+#if !defined(MOZ_WIDGET_ANDROID)
+
+// Locate the specified executable and store its path as a native string in
+// the |aPathPtr| so we can later invoke it from within the exception handler.
+static nsresult
+LocateExecutable(nsIFile* aXREDirectory, const nsACString& aName,
+                 nsAString& aPath)
+{
+  nsCOMPtr<nsIFile> exePath;
+  nsresult rv = aXREDirectory->Clone(getter_AddRefs(exePath));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef XP_MACOSX
+  exePath->SetNativeLeafName(NS_LITERAL_CSTRING("MacOS"));
+  exePath->Append(NS_LITERAL_STRING("crashreporter.app"));
+  exePath->Append(NS_LITERAL_STRING("Contents"));
+  exePath->Append(NS_LITERAL_STRING("MacOS"));
+#endif
+
+  exePath->AppendNative(aName);
+  exePath->GetPath(aPath);
+  return NS_OK;
+}
+
+#endif // !defined(MOZ_WIDGET_ANDROID)
+
 nsresult SetExceptionHandler(nsIFile* aXREDirectory,
                              bool force/*=false*/)
 {
   if (gExceptionHandler)
     return NS_ERROR_ALREADY_INITIALIZED;
 
 #if !defined(DEBUG) || defined(MOZ_WIDGET_GONK)
   // In non-debug builds, enable the crash reporter by default, and allow
@@ -1581,54 +1613,57 @@ nsresult SetExceptionHandler(nsIFile* aX
   crashReporterAPIData_Hash =
     new nsDataHashtable<nsCStringHashKey,nsCString>();
   NS_ENSURE_TRUE(crashReporterAPIData_Hash, NS_ERROR_OUT_OF_MEMORY);
 
   notesField = new nsCString();
   NS_ENSURE_TRUE(notesField, NS_ERROR_OUT_OF_MEMORY);
 
   if (!headlessClient) {
-    // locate crashreporter executable
-    nsCOMPtr<nsIFile> exePath;
-    nsresult rv = aXREDirectory->Clone(getter_AddRefs(exePath));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-#if defined(XP_MACOSX)
-    exePath->SetNativeLeafName(NS_LITERAL_CSTRING("MacOS"));
-    exePath->Append(NS_LITERAL_STRING("crashreporter.app"));
-    exePath->Append(NS_LITERAL_STRING("Contents"));
-    exePath->Append(NS_LITERAL_STRING("MacOS"));
-#endif
-
-    exePath->AppendNative(NS_LITERAL_CSTRING(CRASH_REPORTER_FILENAME));
+#if !defined(MOZ_WIDGET_ANDROID)
+    // Locate the crash reporter executable
+    nsAutoString crashReporterPath_temp;
+    nsresult rv = LocateExecutable(aXREDirectory,
+                                   NS_LITERAL_CSTRING(CRASH_REPORTER_FILENAME),
+                                   crashReporterPath_temp);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsAutoString minidumpAnalyzerPath_temp;
+    rv = LocateExecutable(aXREDirectory,
+                          NS_LITERAL_CSTRING(MINIDUMP_ANALYZER_FILENAME),
+                          minidumpAnalyzerPath_temp);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
 
 #ifdef XP_WIN32
-    nsString crashReporterPath_temp;
-
-    exePath->GetPath(crashReporterPath_temp);
-    crashReporterPath = reinterpret_cast<wchar_t*>(ToNewUnicode(crashReporterPath_temp));
-#elif !defined(__ANDROID__)
-    nsCString crashReporterPath_temp;
-
-    exePath->GetNativePath(crashReporterPath_temp);
-    crashReporterPath = ToNewCString(crashReporterPath_temp);
+  crashReporterPath =
+    reinterpret_cast<wchar_t*>(ToNewUnicode(crashReporterPath_temp));
+  minidumpAnalyzerPath =
+    reinterpret_cast<wchar_t*>(ToNewUnicode(minidumpAnalyzerPath_temp));
+#else
+  crashReporterPath = ToNewCString(crashReporterPath_temp);
+  minidumpAnalyzerPath = ToNewCString(minidumpAnalyzerPath_temp);
+#endif // XP_WIN32
 #else
     // On Android, we launch using the application package name instead of a
     // filename, so use the dynamically set MOZ_ANDROID_PACKAGE_NAME, or fall
     // back to the static ANDROID_PACKAGE_NAME.
     const char* androidPackageName = PR_GetEnv("MOZ_ANDROID_PACKAGE_NAME");
     if (androidPackageName != nullptr) {
       nsCString package(androidPackageName);
       package.Append("/org.mozilla.gecko.CrashReporter");
       crashReporterPath = ToNewCString(package);
     } else {
       nsCString package(ANDROID_PACKAGE_NAME "/org.mozilla.gecko.CrashReporter");
       crashReporterPath = ToNewCString(package);
     }
-#endif
+#endif // !defined(MOZ_WIDGET_ANDROID)
   }
 
   // get temp path to use for minidump path
 #if defined(XP_WIN32)
   nsString tempPath;
 #else
   nsCString tempPath;
 #endif
@@ -3028,16 +3063,40 @@ bool
 AppendExtraData(const nsAString& id, const AnnotationTable& data)
 {
   nsCOMPtr<nsIFile> extraFile;
   if (!GetExtraFileForID(id, getter_AddRefs(extraFile)))
     return false;
   return AppendExtraData(extraFile, data);
 }
 
+/**
+ * Runs the minidump analyzer program on the specified crash dump. The analyzer
+ * will extract the stack traces from the dump and store them in JSON format as
+ * an annotation in the extra file associated with the crash.
+ *
+ * This method waits synchronously for the program to have finished executing,
+ * do not call it from the main thread!
+ */
+void
+RunMinidumpAnalyzer(const nsAString& id)
+{
+#if !defined(MOZ_WIDGET_ANDROID)
+  nsCOMPtr<nsIFile> file;
+
+  if (CrashReporter::GetMinidumpForID(id, getter_AddRefs(file)) && file) {
+    nsAutoString path;
+
+    file->GetPath(path);
+    LaunchProgram(minidumpAnalyzerPath,
+                  (XP_CHAR*)(NS_ConvertUTF16toUTF8(path).get()));
+  }
+#endif // !defined(MOZ_WIDGET_ANDROID)
+}
+
 //-----------------------------------------------------------------------------
 // Helpers for AppendExtraData()
 //
 struct Blacklist {
   Blacklist() : mItems(nullptr), mLen(0) { }
   Blacklist(const char** items, int len) : mItems(items), mLen(len) { }
 
   bool Contains(const nsACString& key) const {
--- a/toolkit/crashreporter/nsExceptionHandler.h
+++ b/toolkit/crashreporter/nsExceptionHandler.h
@@ -98,16 +98,17 @@ typedef nsDataHashtable<nsCStringHashKey
 
 void DeleteMinidumpFilesForID(const nsAString& id);
 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 RunMinidumpAnalyzer(const nsAString& id);
 
 /*
  * Renames the stand alone dump file aDumpFile to:
  *  |aOwnerDumpFile-aDumpFileProcessType.dmp|
  * and moves it into the same directory as aOwnerDumpFile. Does not
  * modify aOwnerDumpFile in any way.
  *
  * @param aDumpFile - the dump file to associate with aOwnerDumpFile.