内存分配测试

allocations 文件夹包含特殊的 mochitests,旨在记录有关 DevTools 内存使用情况的数据。这使用了 Spidermonkey 的内存 API,该 API 在调试器 API 旁边实现。有关更多信息,请参阅以下文档: https://searchfox.org/mozilla-central/source/js/src/doc/Debugger/Debugger.Memory.md

测试示例

add_task(async function() {
  // Execute preliminary setup in order to be able to run your scenario
  // You would typicaly load modules, open a tab, a toolbox, ...
  ...

  // Run the test scenario first before recording in order to load all the
  // modules. Otherwise they get reported as "still allocated" objects,
  // whereas we do expect them to be kept in memory as they are loaded via
  // the main DevTools loader, which keeps the module loaded until the
  // shutdown of Firefox
  await testScript();

  // Pass alsoRecordContentProcess if you want to record the content process
  // of the current tab. Otherwise it will only record parent process objects.
  await startRecordingAllocations({ alsoRecordContentProcess: true });

  // Now, run the test script. This time, we record this run.
  await testScript(toolbox);

  // This will stop the record and also publish the results to Talos database
  // Second argument will be the name of the test displayed in Talos.
  // Many tests will be recorded, but all of them will be prefixed with this string.
  await stopRecordingAllocations("reload", { alsoRecordContentProcess: true });

  // Then, here you can execute cleanup.
  // You would typically close the tab, toolbox, ...
});

如何在本地运行测试

$ ./mach mochitest --headless devtools/client/framework/test/allocations/

以及仅查看结果

$ ./mach mochitest --headless devtools/client/framework/test/allocations/ | grep " test leaked "

调试内存泄漏

如果您看到回归或改进,仅仅查看泄漏的对象数量并没有多大帮助。测试包含一些特殊的调试模式,这些模式会打印大量数据以找出泄漏的内容以及原因。

您可以使用以下环境变量来启用调试模式

DEBUG_DEVTOOLS_ALLOCATIONS=leak|allocations $ ./mach mochitest --headless devtools/client/framework/test/allocations/the-fault-test.js

**DEBUG_DEVTOOLS_ALLOCATIONS** 可以启用两种不同的调试输出。(一次只能启用一个)

**DEBUG_DEVTOOLS_ALLOCATIONS=allocations** 将报告在运行测试期间进行的所有分配站点。这将包括已释放的分配。如果您希望减少分配以减少 GC 负载,则此视图特别有用。

**DEBUG_DEVTOOLS_ALLOCATIONS=leak** 将仅报告在测试结束时仍分配的对象。有时它只会报告缺少堆栈跟踪的分配。从而使预览视图变得有用。

示例

假设我们有以下代码

 1:  exports.MyModule = {
 2:    globalArray: [],
 3:    test() {
 3:      // The following object will be allocated but not leaked,
 5:      // as we keep no reference to it anywhere
 6:      const transientObject = {};
 7:
 8:      // The following object will be allocated on this line,
 9:      // but leaked on the following one. By storing a reference
10:      // to it in the global array which is never cleared.
11:      const leakedObject = {};
12:      this.globalArray.push(leakedObject);
13:    },
14:  };

并且,我们有一个执行此操作的内存测试

  const { MyModule } = require("devtools/my-module");

  await startRecordingAllocations();

  MyModule.test();

  await stopRecordingAllocations("target");

我们可以首先通过运行以下命令来查看所有分配

DEBUG_DEVTOOLS_ALLOCATIONS=allocations $ ./mach mochitest --headless devtools/client/framework/test/allocations/browser_allocation_myTest.js

这将在最后打印

DEVTOOLS ALLOCATION: all allocations (which may be freed or are still allocated):
[
   {
     "src": "UNKNOWN",
     "count": 80,
     "lines": [
       "?: 80"
     ]
   },
   {
     "src": "resource://devtools/my-module.js",
     "count": 2,
     "lines": [
       "11: 1"
       "6: 1"
     ]
   }
]

第一部分,带有 UNKNOWN 可以忽略。这是关于缺少分配站点的对象。此日志的第二部分告诉我们,在运行测试时从 my-module.js 分配了 2 个对象。一个是在第 6 行分配的,它是 transcientObject。另一个是在第 11 行分配的,它是 leakedObject

现在,我们可以使用第二个视图来专注于保留分配的对象

DEBUG_DEVTOOLS_ALLOCATIONS=leaks $ ./mach mochitest --headless devtools/client/framework/test/allocations/browser_allocation_myTest.js

这将在最后打印

DEVTOOLS ALLOCATION: allocations which leaked:
[
   {
     "src": "UNKNOWN",
     "count": 80,
     "lines": [
       "?: 80"
     ]
   },
   {
     "src": "resource://devtools/shared/commands/commands-factory.js",
     "count": 1,
     "lines": [
       "11: 1"
     ]
   }
]

类似地,我们可以只关注第二部分,它告诉我们只有一个对象正在泄漏,并且此对象最初是从第 11 行创建的,它是 leakedObject。这并没有告诉我们为什么对象被保留分配,但至少我们知道哪个对象被保留在内存中。

通过支配者调试内存泄漏

此最后一个功能可能是最强大的,并且不受 DEBUG_DEVTOOLS_ALLOCATIONS 的限制。它始终启用。此外,它需要知道哪个特定对象正在泄漏,还需要修改代码库以将可疑对象的引用传递给测试助手。

您可以通过执行以下操作来指示测试助手跟踪给定对象

 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.sys.mjs");
 5: // From anything else, JSM, XPCOM module,...:
 6: const { track } = ChromeUtils.importESModule("resource://devtools/shared/test-helpers/tracked-objects.sys.mjs");
 7:
 8: const g = [];
 9: function someFunctionInDevToolsCalledBySomething() {
10:   const myLeakedObject = {};
11:   track(myLeakedObject);
12:
13:   // Simulate a leak by holding a reference to the object in a global `g` array
14:   g.push({ seeMyCustomAttributeHere: myLeakedObject });
15: }

然后,在运行测试时,您将获得如下输出

 0:41.26 GECKO(644653)  # Tracing: Object@my-module:10
 0:40.65 GECKO(644653) ### Path(s) from root:
 0:41.26 GECKO(644653) - other@no-stack:undefined.WeakMap entry value
 0:41.26 GECKO(644653)  \--> [email protected]:160.**UNKNOWN SLOT 1**
 0:41.26 GECKO(644653)  \--> [email protected]:155.g
 0:41.26 GECKO(644653)  \--> [email protected]:8.objectElements[0]
 0:41.26 GECKO(644653)  \--> [email protected]:14.seeMyCustomAttributeHere
 0:41.26 GECKO(644653)  \--> [email protected]:10

此输出表示 myLeakedObject 最初是从 my-module.js 的第 10 行分配的。并且被保留分配是因为它被保存在从 my-module.js 的第 14 行分配的对象中。这是我们存储在 g 全局数组中的自定义对象。此自定义对象由第 8 行 my-module.js 分配的数组持有。并且此数组由一个对象持有,该对象本身由 base-loader.sys.mjs 的第 155 行分配。这是 my-module.js 模块的全局变量,由 DevTools 加载器创建。然后我们看到一些更低级别的对象一直到另一个全局对象,该对象缺少其分配站点。

如何轻松获取 try 运行的数据

$ ./mach try fuzzy devtools/client/framework/test/allocations/ --query "'linux 'chrome-e10s 'opt '64-qr/opt"

如果测试结果存在一些噪声并且您希望进行更多测试运行,您也可以传递 --rebuild 3