Bug 1457481 - Hook up the new devtools performance panel to nsIProfiler.getSymbolTable. r=gregtatum
☠☠ backed out by 00eb79fc9f8a ☠ ☠
authorMarkus Stange <mstange@themasta.com>
Mon, 01 Oct 2018 20:53:20 +0000
changeset 494795 212450f77860898d1652bf96aef05b9986a94d96
parent 494794 ac3deff9340f230fc2c91e4b05531ba32c827235
child 494796 c209b19271072f5225e86bfe2f7b8b40511233e0
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgregtatum
bugs1457481
milestone64.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 1457481 - Hook up the new devtools performance panel to nsIProfiler.getSymbolTable. r=gregtatum Depends on D7023 Differential Revision: https://phabricator.services.mozilla.com/D7024
devtools/client/performance-new/browser.js
devtools/client/performance-new/frame-script.js
devtools/client/performance-new/store/actions.js
devtools/server/actors/perf.js
devtools/shared/specs/perf.js
--- a/devtools/client/performance-new/browser.js
+++ b/devtools/client/performance-new/browser.js
@@ -1,29 +1,38 @@
 /* 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 Services = require("Services");
 
+const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
+const SYMBOL_TABLE_REQUEST_EVENT = "devtools:perf-html-request-symbol-table";
+const SYMBOL_TABLE_RESPONSE_EVENT = "devtools:perf-html-reply-symbol-table";
+
 /**
  * This file contains all of the privileged browser-specific functionality. This helps
  * keep a clear separation between the privileged and non-privileged client code. It
  * is also helpful in being able to mock out browser behavior for tests, without
  * worrying about polluting the browser environment.
  */
 
 /**
  * Once a profile is received from the actor, it needs to be opened up in perf.html
  * to be analyzed. This function opens up perf.html into a new browser tab, and injects
  * the profile via a frame script.
  *
  * @param {object} profile - The Gecko profile.
+ * @param {function} getSymbolTableCallback - A callback function with the signature
+ *   (debugName, breakpadId) => Promise<SymbolTableAsTuple>, which will be invoked
+ *   when perf-html.io sends SYMBOL_TABLE_REQUEST_EVENT messages to us. This function
+ *   should obtain a symbol table for the requested binary and resolve the returned
+ *   promise with it.
  */
-function receiveProfile(profile) {
+function receiveProfile(profile, getSymbolTableCallback) {
   // Find the most recently used window, as the DevTools client could be in a variety
   // of hosts.
   const win = Services.wm.getMostRecentWindow("navigator:browser");
   if (!win) {
     throw new Error("No browser window");
   }
   const browser = win.gBrowser;
   Services.focus.activeWindow = win;
@@ -34,17 +43,33 @@ function receiveProfile(profile) {
     })
   });
   browser.selectedTab = tab;
   const mm = tab.linkedBrowser.messageManager;
   mm.loadFrameScript(
     "chrome://devtools/content/performance-new/frame-script.js",
     false
   );
-  mm.sendAsyncMessage("devtools:perf-html-transfer-profile", profile);
+  mm.sendAsyncMessage(TRANSFER_EVENT, profile);
+  mm.addMessageListener(SYMBOL_TABLE_REQUEST_EVENT, e => {
+    const { debugName, breakpadId } = e.data;
+    getSymbolTableCallback(debugName, breakpadId).then(result => {
+      const [addr, index, buffer] = result;
+      mm.sendAsyncMessage(SYMBOL_TABLE_RESPONSE_EVENT, {
+        status: "success",
+        debugName, breakpadId, result: [addr, index, buffer]
+      });
+    }, error => {
+      mm.sendAsyncMessage(SYMBOL_TABLE_RESPONSE_EVENT, {
+        status: "error",
+        debugName, breakpadId,
+        error: `${error}`,
+      });
+    });
+  });
 }
 
 /**
  * Don't trust that the user has stored the correct value in preferences, or that it
  * even exists. Gracefully handle malformed data or missing data. Ensure that this
  * function always returns a valid array of strings.
  * @param {PreferenceFront} preferenceFront
  * @param {string} prefName
--- a/devtools/client/performance-new/frame-script.js
+++ b/devtools/client/performance-new/frame-script.js
@@ -1,53 +1,67 @@
 /* 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";
-/* global addMessageListener, addEventListener, content */
+/* global addMessageListener, addEventListener, content, sendAsyncMessage */
 
 /**
  * This frame script injects itself into perf-html.io and injects the profile
  * into the page. It is mostly taken from the Gecko Profiler Addon implementation.
  */
 
 const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
+const SYMBOL_TABLE_REQUEST_EVENT = "devtools:perf-html-request-symbol-table";
+const SYMBOL_TABLE_RESPONSE_EVENT = "devtools:perf-html-reply-symbol-table";
 
 let gProfile = null;
+const symbolReplyPromiseMap = new Map();
 
 addMessageListener(TRANSFER_EVENT, e => {
   gProfile = e.data;
   // Eagerly try and see if the framescript was evaluated after perf loaded its scripts.
   connectToPage();
   // If not try again at DOMContentLoaded which should be called after the script
   // tag was synchronously loaded in.
   addEventListener("DOMContentLoaded", connectToPage);
 });
 
+addMessageListener(SYMBOL_TABLE_RESPONSE_EVENT, e => {
+  const { debugName, breakpadId, status, result, error } = e.data;
+  const promiseKey = [debugName, breakpadId].join(":");
+  const { resolve, reject } = symbolReplyPromiseMap.get(promiseKey);
+  symbolReplyPromiseMap.delete(promiseKey);
+
+  if (status === "success") {
+    const [addresses, index, buffer] = result;
+    resolve([addresses, index, buffer]);
+  } else {
+    reject(error);
+  }
+});
+
 function connectToPage() {
   const unsafeWindow = content.wrappedJSObject;
   if (unsafeWindow.connectToGeckoProfiler) {
     unsafeWindow.connectToGeckoProfiler(makeAccessibleToPage({
       getProfile: () => Promise.resolve(gProfile),
       getSymbolTable: (debugName, breakpadId) => getSymbolTable(debugName, breakpadId),
     }, unsafeWindow));
   }
 }
 
-/**
- * For now, do not try to symbolicate. Reject any attempt.
- */
 function getSymbolTable(debugName, breakpadId) {
-  // Errors will not properly clone into the content page as they bring privileged
-  // stacks information into the page. In this case provide a mock object to maintain
-  // the Error type object shape.
-  const error = {
-    message: `The DevTools' "perf" actor does not support symbolication.`
-  };
-  return Promise.reject(error);
+  return new Promise((resolve, reject) => {
+    sendAsyncMessage(SYMBOL_TABLE_REQUEST_EVENT, { debugName, breakpadId });
+    symbolReplyPromiseMap.set([debugName, breakpadId].join(":"), {
+      resolve,
+      reject,
+    });
+  });
 }
 
 // The following functions handle the security of cloning the object into the page.
 // The code was taken from the original Gecko Profiler Add-on to maintain
 // compatibility with the existing profile importing mechanism:
 // See: https://github.com/devtools-html/Gecko-Profiler-Addon/blob/78138190b42565f54ce4022a5b28583406489ed2/data/tab-framescript.js
 
 /**
--- a/devtools/client/performance-new/store/actions.js
+++ b/devtools/client/performance-new/store/actions.js
@@ -108,24 +108,84 @@ exports.startRecording = () => {
     const recordingSettings = selectors.getRecordingSettings(getState());
     const perfFront = selectors.getPerfFront(getState());
     perfFront.startProfiler(recordingSettings);
     dispatch(changeRecordingState(REQUEST_TO_START_RECORDING));
   };
 };
 
 /**
+ * Returns a function getDebugPathFor(debugName, breakpadId) => string which
+ * resolves a (debugName, breakpadId) pair to the library's debugPath, e.g.
+ * the path on the file system where the binary is stored.
+ *
+ * This is needed for the following reason:
+ *  - In order to obtain a symbol table for a system library, we need to know
+ *    the library's absolute path on the file system.
+ *  - Symbol tables are requested asynchronously, by the profiler UI, after the
+ *    profile itself has been obtained.
+ *  - When the symbol tables are requested, we don't want the profiler UI to
+ *    pass us arbitrary absolute file paths, as an extra defense against
+ *    potential information leaks.
+ *  - Instead, when the UI requests symbol tables, it identifies the library
+ *    with a (debugName, breakpadId) pair. We need to map that pair back to the
+ *    absolute path of that library.
+ *  - We get the "trusted" paths from the "libs" sections of the profile. We
+ *    trust these paths because we just obtained the profile directly from
+ *    Gecko.
+ *  - This function builds the (debugName, breakpadId) => debugPath mapping and
+ *    retains it on the returned closure so that it can be consulted after the
+ *    profile has been passed to the UI.
+ *
+ * @param {object} profile - The profile JSON object
+ */
+function createDebugPathMapForLibsInProfile(profile) {
+  const map = new Map();
+  function fillMapForProcessRecursive(processProfile) {
+    for (const lib of processProfile.libs) {
+      const { debugName, debugPath, breakpadId } = lib;
+      const key = [debugName, breakpadId].join(":");
+      map.set(key, debugPath);
+    }
+    for (const subprocess of processProfile.processes) {
+      fillMapForProcessRecursive(subprocess);
+    }
+  }
+
+  fillMapForProcessRecursive(profile);
+  return function getDebugPathFor(debugName, breakpadId) {
+    const key = [debugName, breakpadId].join(":");
+    return map.get(key);
+  };
+}
+
+/**
  * Stops the profiler, and opens the profile in a new window.
  */
 exports.getProfileAndStopProfiler = () => {
   return async (dispatch, getState) => {
     const perfFront = selectors.getPerfFront(getState());
     dispatch(changeRecordingState(REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER));
     const profile = await perfFront.getProfileAndStopProfiler();
-    selectors.getReceiveProfileFn(getState())(profile);
+
+    const debugPathGetter = createDebugPathMapForLibsInProfile(profile);
+    async function getSymbolTable(debugName, breakpadId) {
+      const debugPath = debugPathGetter(debugName, breakpadId);
+      const [addresses, index, buffer] =
+        await perfFront.getSymbolTable(debugPath, breakpadId);
+      // The protocol transmits these arrays as plain JavaScript arrays of
+      // numbers, but we want to pass them on as typed arrays. Convert them now.
+      return [
+        new Uint32Array(addresses),
+        new Uint32Array(index),
+        new Uint8Array(buffer)
+      ];
+    }
+
+    selectors.getReceiveProfileFn(getState())(profile, getSymbolTable);
     dispatch(changeRecordingState(AVAILABLE_TO_RECORD));
   };
 };
 
 /**
  * Stops the profiler, but does not try to retrieve the profile.
  */
 exports.stopProfilerAndDiscardProfile = () => {
--- a/devtools/server/actors/perf.js
+++ b/devtools/server/actors/perf.js
@@ -80,20 +80,30 @@ exports.PerfActor = ActorClassWithSpec(p
 
   stopProfilerAndDiscardProfile() {
     if (!IS_SUPPORTED_PLATFORM) {
       return;
     }
     Services.profiler.StopProfiler();
   },
 
+  async getSymbolTable(debugPath, breakpadId) {
+    const [addr, index, buffer] =
+      await Services.profiler.getSymbolTable(debugPath, breakpadId);
+    // The protocol does not support the transfer of typed arrays, so we convert
+    // these typed arrays to plain JS arrays of numbers now.
+    // Our return value type is declared as "array:array:number".
+    return [Array.from(addr), Array.from(index), Array.from(buffer)];
+  },
+
   async getProfileAndStopProfiler() {
     if (!IS_SUPPORTED_PLATFORM) {
       return null;
     }
+
     let profile;
     try {
       // Attempt to pull out the data.
       profile = await Services.profiler.getProfileDataAsync();
 
       // Stop and discard the buffers.
       Services.profiler.StopProfiler();
     } catch (e) {
--- a/devtools/shared/specs/perf.js
+++ b/devtools/shared/specs/perf.js
@@ -45,16 +45,24 @@ const perfDescription = {
       response: RetVal("nullable:json")
     },
 
     stopProfilerAndDiscardProfile: {
       request: {},
       response: {}
     },
 
+    getSymbolTable: {
+      request: {
+        debugPath: Arg(0, "string"),
+        breakpadId: Arg(1, "string"),
+      },
+      response: { value: RetVal("array:array:number") }
+    },
+
     isActive: {
       request: {},
       response: { value: RetVal("boolean") }
     },
 
     isSupportedPlatform: {
       request: {},
       response: { value: RetVal("boolean") }