Bug 1731530 - [devtools] Implement TrackedObjects.track in the content process. r=jdescottes
authorAlexandre Poirot <poirot.alex@gmail.com>
Thu, 23 Sep 2021 08:45:49 +0000
changeset 593021 739ccd54d7c8ea04235c06cc335e93d769cd60f7
parent 593020 c4fec6d398447b815ca9d77843f19ba154899878
child 593022 9ba235034c43eb01175931d005acb806ea4b8ef0
push id38820
push usersmolnar@mozilla.com
push dateThu, 23 Sep 2021 21:45:25 +0000
treeherdermozilla-central@4eda9eb8926b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1731530
milestone94.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 1731530 - [devtools] Implement TrackedObjects.track in the content process. r=jdescottes Differential Revision: https://phabricator.services.mozilla.com/D126082
devtools/client/framework/test/allocations/README.md
devtools/client/framework/test/allocations/head.js
devtools/shared/test-helpers/allocation-tracker.js
devtools/shared/test-helpers/tracked-objects.jsm
--- a/devtools/client/framework/test/allocations/README.md
+++ b/devtools/client/framework/test/allocations/README.md
@@ -160,18 +160,16 @@ This doesn't tell us why the object is b
 
 ## Debug leaks via dominators
 
 This last feature might be the most powerful and isn't bound to DEBUG_DEVTOOLS_ALLOCATIONS.
 This is always enabled.
 Also, it requires to know which particular object is being leaked and also require to hack
 the codebase in order to pass a reference of the suspicious object to the test helper.
 
-/!\ For now, this API only works from the parent process.
-
 You can instruct the test helper to track a given object by doing this:
 ```
  1: // Let's say it is some code running from "my-module.js"
  2:
  3: // From a DevTools CommonJS module:
  4: const { track } = require("devtools/shared/test-helpers/tracked-objects.jsm");
  5: // From anything else, JSM, XPCOM module,...:
  6: const { track } = ChromeUtils.import("resource://devtools/shared/test-helpers/tracked-objects.jsm");
--- a/devtools/client/framework/test/allocations/head.js
+++ b/devtools/client/framework/test/allocations/head.js
@@ -106,19 +106,16 @@ async function stopRecordingAllocations(
   // Ensure that Memory API didn't ran out of buffers
   ok(!tracker.overflowed, "Allocation were all recorded in the parent process");
 
   // And finally, retrieve the record *after* having ran the test
   const parentProcessData = await tracker.stopRecordingAllocations(
     DEBUG_ALLOCATIONS
   );
 
-  // Only do that from the parent process for now,
-  // as traceObjects doesn't work from the content process.
-  // It throws because the content process isn't allowed to read the snapshot files.
   const objectNodeIds = TrackedObjects.getAllNodeIds();
   if (objectNodeIds.length > 0) {
     tracker.traceObjects(objectNodeIds);
   }
 
   let contentProcessData = null;
   if (alsoRecordContentProcess) {
     contentProcessData = await SpecialPowers.spawn(
@@ -133,16 +130,46 @@ async function stopRecordingAllocations(
           !tracker.overflowed,
           "Allocation were all recorded in the content process"
         );
         return tracker.stopRecordingAllocations(debug_allocations);
       }
     );
   }
 
+  const trackedObjectsInContent = await SpecialPowers.spawn(
+    gBrowser.selectedBrowser,
+    [],
+    () => {
+      const TrackedObjects = ChromeUtils.import(
+        "resource://devtools/shared/test-helpers/tracked-objects.jsm"
+      );
+      const objectNodeIds = TrackedObjects.getAllNodeIds();
+      if (objectNodeIds.length > 0) {
+        const { DevToolsLoader } = ChromeUtils.import(
+          "resource://devtools/shared/Loader.jsm"
+        );
+        const { tracker } = DevToolsLoader;
+        // Record the heap snapshot from the content process,
+        // and pass the record's filepath to the parent process
+        // As only the parent process can read the file because
+        // of sandbox restrictions made to content processes regarding file I/O.
+        const snapshotFile = tracker.getSnapshotFile();
+        return { snapshotFile, objectNodeIds };
+      }
+      return null;
+    }
+  );
+  if (trackedObjectsInContent) {
+    tracker.traceObjects(
+      trackedObjectsInContent.objectNodeIds,
+      trackedObjectsInContent.snapshotFile
+    );
+  }
+
   // Craft the JSON object required to save data in talos database
   info(
     `The ${recordName} test leaked ${parentProcessData.objectsWithStack} objects (${parentProcessData.objectsWithoutStack} with missing allocation site) in the parent process`
   );
   const PERFHERDER_DATA = {
     framework: {
       name: "devtools",
     },
--- a/devtools/shared/test-helpers/allocation-tracker.js
+++ b/devtools/shared/test-helpers/allocation-tracker.js
@@ -433,21 +433,43 @@ exports.allocationTracker = function({
         Cu.forceCC();
         await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve));
 
         // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
         await new Promise(resolve => setTimeout(resolve, 1000));
       }
     },
 
-    traceObjects(objects) {
+    /**
+     * Return the absolute file path to a memory snapshot.
+     * This is used to compute dominator trees in `traceObjects`.
+     */
+    getSnapshotFile() {
+      return ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+    },
+
+    /**
+     * Print information about why a list of objects are being held in memory.
+     *
+     * @param Array<NodeId> objects
+     *        List of NodeId's of objects to debug. NodeIds can be retrieved
+     *        via ChromeUtils.getObjectNodeId.
+     * @param String snapshotFile
+     *        Absolute path to a Heap snapshot file retrieved via this.getSnapshotFile.
+     *        This is used to trace content process objects. We have to record the snapshot
+     *        from the content process, but can only read it from the parent process because
+     *        of I/O restrictions in content processes.
+     */
+    traceObjects(objects, snapshotFile) {
       // There is no API to get the heap snapshot at runtime,
       // the only way is to save it to disk and then load it from disk
-      const filePath = ChromeUtils.saveHeapSnapshot({ debugger: dbg });
-      const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+      if (!snapshotFile) {
+        snapshotFile = this.getSnapshotFile();
+      }
+      const snapshot = ChromeUtils.readHeapSnapshot(snapshotFile);
 
       function getObjectClass(id) {
         if (!id) {
           return "<null>";
         }
         try {
           let stack = [...snapshot.describeNode({ by: "allocationStack" }, id)];
           let line;
@@ -474,16 +496,19 @@ exports.allocationTracker = function({
             stack = "no-desc";
           }
           return (
             Object.entries(
               snapshot.describeNode({ by: "objectClass" }, id)
             )[0][0] + (stack ? "@" + stack + ":" + line : "")
           );
         } catch (e) {
+          if (e.name == "NS_ERROR_ILLEGAL_VALUE") {
+            return "<not-in-memory-snapshot:is-from-untracked-global?>";
+          }
           return "<invalid:" + id + ":" + e + ">";
         }
       }
       function printPath(src, dst) {
         let paths;
         try {
           paths = snapshot.computeShortestPaths(src, [dst], 10);
         } catch (e) {}
--- a/devtools/shared/test-helpers/tracked-objects.jsm
+++ b/devtools/shared/test-helpers/tracked-objects.jsm
@@ -9,28 +9,23 @@
 //
 // We are going to store a weak reference to the passed objects,
 // in order to prevent holding them in memory.
 // Allocation tracker will then print detailed information
 // about why these objects are still allocated.
 
 var EXPORTED_SYMBOLS = ["track", "getAllNodeIds", "clear"];
 
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
 const objects = [];
 
 /**
  * Request to track why the given object is kept in memory,
  * later on, when retrieving all the watched object via getAllNodeIds.
  */
 function track(obj) {
-  if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
-    throw new Error("For now, this API only works from the parent process");
-  }
   // We store a weak reference, so that we do force keeping the object in memory!!
   objects.push(Cu.getWeakReference(obj));
 }
 
 /**
  * Return the NodeId's of all the objects passed via `track()` method.
  *
  * NodeId's are used by spidermonkey memory API to designates JS objects in head snapshots.