暗物质探测器 (DMD)

DMD(“dark matter detector”的缩写)是 Firefox 中的一个堆分析器。它有四种模式。

  • “暗物质”模式。在此模式下,DMD 会跟踪堆的内容,包括哪些堆块已被内存报告器报告。它有助于我们减少 Firefox 的 about:memory 页面中的“堆未分类”值,并检测是否有任何堆块被报告了两次。最初,这是 DMD 唯一的模式,这也解释了 DMD 的名称。这是默认模式。

  • “实时”模式。在此模式下,DMD 会跟踪堆的当前内容。您可以将这些信息转储到文件中,从而获得当时实时堆块的概要。这对于理解内存如何在某个有趣的时间点使用(例如峰值内存使用)很有帮助。

  • “累积”模式。在此模式下,DMD 会跟踪堆的过去和当前内容。您可以将这些信息转储到文件中,从而获得整个会话的堆使用情况的概要。这对于查找导致高堆抖动的代码部分很有帮助,例如通过分配许多短生命周期的分配。

  • “堆扫描”模式。此模式类似于实时模式,但它还会记录日志中每个实时块的内容。这可以通过找出哪些对象可能持有对其他对象的引用来用于调查泄漏。

构建和运行

Nightly Firefox

使用 DMD 最简单的方法是使用正常的 Nightly Firefox 构建,该构建已启用 DMD。要在运行时激活 DMD,您只需在运行时设置环境变量 DMD=1。例如,在 OSX 上,您可以运行以下命令:

DMD=1 /Applications/Firefox\ Nightly.app/Contents/MacOS/firefox

您可以通过转到 about:memory 并查找“保存 DMD 输出”来确认它是否正在工作。如果 DMD 已正确启用,则“保存”按钮将不会呈灰色显示。查看下面的“触发”部分以查看激活 DMD 报告后获取 DMD 报告的所有方法的完整列表。请注意,您获得的堆栈信息可能不太详细,因为无法进行符号化。您将能够获取函数名称,但不能获取行号。

可以在 Firefox 代码库中找到 dmd.py 脚本。

桌面版 Firefox

构建

使用此选项添加到您的 mozconfig 中构建 Firefox

ac_add_options --enable-dmd

如果通过 try 服务器构建,请在推送之前修改 browser/config/mozconfigs/linux64/common-opt 或类似文件。

启动

使用 mach run --dmd;使用 --mode 选择模式。

在 try 服务器完成的 Windows 构建中,2013 年的 这些说明 可能有用也可能没用。

触发

有几种方法可以触发 DMD 快照。大多数方法还会首先获取内存报告。当 DMD 正在写入其输出时,它会打印如下日志:

DMD[5222] opened /tmp/dmd-1414556492-5222.json.gz for writing
DMD[5222] Dump 1 {
DMD[5222]   Constructing the heap block list...
DMD[5222]   Constructing the stack trace table...
DMD[5222]   Constructing the stack frame table...
DMD[5222] }

您将看到每个进程的单独输出。此步骤可能需要 10 秒或更长时间,并且可能会导致 Firefox 暂时冻结。

如果您看到“已打开”行,它会告诉您文件保存到的位置。它始终位于临时目录中,文件名始终采用 dmd- 的格式。.

触发 DMD 快照的方法有:

  1. 访问 about:memory 并在“保存 DMD 输出”下单击“保存”按钮。在非 DMD 构建中,此按钮不会出现,并且如果在启动时未启用 DMD,则在 DMD 构建中会呈灰色显示。

  2. 如果您希望从 C++ 或 JavaScript 代码中触发 DMD 转储,可以使用 nsIMemoryInfoDumper.dumpMemoryInfoToTempDir。例如,从 JavaScript 代码中,您可以执行以下操作。

    const Cc = Components.classes;
    let mydumper = Cc["@mozilla.org/memory-info-dumper;1"]
                     .getService(Ci.nsIMemoryInfoDumper);
    mydumper.dumpMemoryInfoToTempDir(identifier, anonymize, minimize);
    

    这会将内存报告和 DMD 输出转储到临时目录。 identifier 是一个字符串,将用于文件名的一部分(如果它是空字符串,则会使用时间戳);anonymize 是一个布尔值,指示是否应匿名化内存报告;minimize 是一个布尔值,指示是否应首先最小化内存使用量。

  3. 在 Linux 上,您可以向 firefox 进程发送信号 34,例如使用以下命令。

    $ killall -34 firefox
    
  4. 如果设置了 MOZ_DMD_SHUTDOWN_LOG 环境变量,则会在关闭时触发 DMD 运行;其值必须是日志将放置到的目录。这主要用于调试泄漏。哪些进程被记录由 MOZ_DMD_LOG_PROCESS 环境变量控制。如果未设置此变量,它将记录所有进程。它可以设置为 XRE_GetProcessTypeString() 的任何有效值,并且只会记录这些进程。例如,如果设置为 default,它将只记录父进程。如果设置为 tab,它将只记录内容进程。

    例如,如果您有

    MOZ_DMD_SHUTDOWN_LOG=~/dmdlogs/ MOZ_DMD_LOG_PROCESS=tab
    

    那么 DMD 会在关闭时为内容进程创建日志并将其保存到 ~/dmdlogs/

注意

  • 要从内容进程转储 DMD 数据,您需要使用 MOZ_DISABLE_CONTENT_SANDBOX=1 禁用沙箱。

  • MOZ_DMD_SHUTDOWN_LOG 必须(目前)包含尾随分隔符(“/”)。

Fennec

注意

您会注意到,本节的名称为“Fennec”,说明这些说明非常旧。希望它们会比没有它们更有用。

注意

为了在 Fennec 上使用 DMD,您需要在 Android 设备上拥有 root 权限。有关如何 root 设备的说明不在本文档范围之内。

构建

使用以下选项构建

ac_add_options --enable-dmd

准备

为了准备您的设备以运行启用了 DMD 的 Fennec,您需要执行以下操作。首先,您需要将 libdmd.so 库推送到设备,以便 Fennec 可以动态加载它。您可以通过运行以下命令来执行此操作:

adb push $OBJDIR/dist/bin/libdmd.so /sdcard/

其次,您需要为 Fennec 创建一个可执行的包装器,该包装器在启动 Fennec 之前设置环境变量。(如果您熟悉推荐的“–es env0”方法来设置启动 Fennec 时的环境变量,请注意您不能在此处使用此方法,因为这些方法在启动过程中的处理时间太晚。如果您不熟悉该方法,可以忽略此括号注释。)首先使用您选择的编辑器在主机上创建可执行包装器。将文件命名为 dmd_fennec 并输入以下内容作为内容:

#!/system/bin/sh
export MOZ_REPLACE_MALLOC_LIB=/sdcard/libdmd.so
exec "$@"

如果您想使用其他 DMD 选项,可以在上面输入其他环境变量。您需要将其推送到设备并使其可执行。由于您无法将 /sdcard/ 中的文件标记为可执行文件,因此我们将为此目的使用 /data/local/tmp

adb push dmd_fennec /data/local/tmp
adb shell
cd /data/local/tmp
chmod 755 dmd_fennec

最后一步是让 Android 在启动 Fennec 时使用上述包装器脚本,以便在 Fennec 启动时环境变量存在。假设您已执行本地构建,则应用程序标识符将为 org.mozilla.fennec_$USERNAME$USERNAME 是主机上的用户名),因此我们按如下所示执行此操作。如果您使用的是启用了 DMD 的 try 构建或来自其他来源的构建,请根据需要调整应用程序标识符。

adb shell
su    # You need root access for the setprop command to take effect
setprop wrap.org.mozilla.fennec_$USERNAME "/data/local/tmp/dmd_fennec"

设置完成后,启动 org.mozilla.fennec_$USERNAME 应用程序将使用包装器脚本。

启动

通过像往常一样点击图标或从命令行启动 Fennec(如前所述,请确保将 org.mozilla.fennec_$USERNAME 替换为相应的应用程序标识符)。

adb shell am start -n org.mozilla.fennec_$USERNAME/.App

触发

使用现有的内存报告转储钩子

adb shell am broadcast -a org.mozilla.gecko.MEMORY_DUMP

在 logcat 中,您应该会看到类似于以下内容的输出:

I/DMD     (20731): opened /storage/emulated/0/Download/memory-reports/dmd-default-20731.json.gz for writing
...
I/GeckoConsole(20731): nsIMemoryInfoDumper dumped reports to /storage/emulated/0/Download/memory-reports/unified-memory-report-default-20731.json.gz

该路径是内存报告和 DMD 报告转储到的位置。您可以像这样提取它们:

adb pull /sdcard/Download/memory-reports/dmd-default-20731.json.gz
adb pull /sdcard/Download/memory-reports/unified-memory-report-default-20731.json.gz

处理输出

DMD 为每个进程输出一个 gzip 压缩的 JSON 文件,其中包含该进程堆的描述。您可以使用 dmd.py 分析这些文件(压缩或未压缩)。

某些平台(Linux、Mac、Android)需要堆栈修复,这会添加缺少的文件名、函数名称和行号信息。第一次在输出文件上运行 dmd.py 时,此操作将自动发生。这可能需要 10 秒或更长时间才能完成。(如果您的构建不包含符号,则此操作将失败。但是,如果您有构建的崩溃报告符号(如 tryserver 构建),则可以使用 此脚本:克隆整个存储库,编辑 resymbolicate_dmd.py 顶部的路径并运行它。)最简单的方法是,当您的工作目录为 $OBJDIR/dist/bin 时,只需在 DMD 报告上运行 dmd.py 脚本。这将允许找到并使用本地库。

如果您不带参数调用 dmd.py,您将获得适合 DMD 调用模式的输出。

“暗物质”模式输出

对于“暗物质”模式,dmd.py 的输出描述了实时堆块如何被内存报告覆盖。此输出分为多个部分。

  1. “调用”。这会告诉您 DMD 如何被调用,即使用了哪些选项。

  2. “重复报告的堆栈跟踪记录”。这会告诉您哪些堆块被报告了两次或更多次。任何此类记录的存在都表明一个或多个内存报告器存在错误。

  3. “未报告的堆栈跟踪记录”。这会告诉您哪些堆块未被报告,这表明在哪些地方额外添加内存报告器将最有用。

  4. “已报告一次的堆栈跟踪记录”:类似于“未报告的堆栈跟踪记录”部分,但适用于已报告一次的块。

  5. “摘要”:提供总堆以及未报告/已报告一次/重复报告的部分的度量。

“重复报告的堆栈跟踪记录”和“未报告的堆栈跟踪记录”部分是最重要的,因为它们指出了可以改进内存报告器的方法。

以下是从“未报告的堆栈跟踪记录”部分中的堆栈跟踪记录示例。

Unreported {
  150 blocks in heap block record 283 of 5,495
  21,600 bytes (20,400 requested / 1,200 slop)
  Individual block sizes: 144 x 150
  0.00% of the heap (16.85% cumulative)
  0.02% of unreported (94.68% cumulative)
  Allocated at {
    #01: replace_malloc (/home/njn/moz/mi5/go64dmd/memory/replace/dmd/../../../../memory/replace/dmd/DMD.cpp:1286)
    #02: malloc (/home/njn/moz/mi5/go64dmd/memory/build/../../../memory/build/replace_malloc.c:153)
    #03: moz_xmalloc (/home/njn/moz/mi5/memory/mozalloc/mozalloc.cpp:84)
    #04: nsCycleCollectingAutoRefCnt::incr(void*, nsCycleCollectionParticipant*) (/home/njn/moz/mi5/go64dmd/dom/xul/../../dist/include/nsISupportsImpl.h:250)
    #05: nsXULElement::Create(nsXULPrototypeElement*, nsIDocument*, bool, bool,mozilla::dom::Element**) (/home/njn/moz/mi5/dom/xul/nsXULElement.cpp:287)
    #06: nsXBLContentSink::CreateElement(char16_t const**, unsigned int, mozilla::dom::NodeInfo*, unsigned int, nsIContent**, bool*, mozilla::dom::FromParser) (/home/njn/moz/mi5/dom/xbl/nsXBLContentSink.cpp:874)
    #07: nsCOMPtr<nsIContent>::StartAssignment() (/home/njn/moz/mi5/go64dmd/dom/xml/../../dist/include/nsCOMPtr.h:753)
    #08: nsXMLContentSink::HandleStartElement(char16_t const*, char16_t const**, unsigned int, unsigned int, bool) (/home/njn/moz/mi5/dom/xml/nsXMLContentSink.cpp:1007)
  }
}

它告诉您,有 150 个堆块是从“分配位置”堆栈跟踪指示的程序点分配的,这些块占用了 21,600 字节,所有 150 个块的大小均为 144 字节,其中 1,200 字节为“浪费”(由堆分配器将请求大小四舍五入造成的浪费空间)。它还指示这些块占总堆大小和堆的未报告部分的百分比。

在每个部分中,记录按从大到小的顺序排列。

已报告一次和重复报告的堆栈跟踪记录也具有报告点的堆栈跟踪。例如

Reported at {
  #01: mozilla::dmd::Report(void const*) (/home/njn/moz/mi2/memory/replace/dmd/DMD.cpp:1740) 0x7f68652581ca
  #02: CycleCollectorMallocSizeOf(void const*) (/home/njn/moz/mi2/xpcom/base/nsCycleCollector.cpp:3008) 0x7f6860fdfe02
  #03: nsPurpleBuffer::SizeOfExcludingThis(unsigned long (*)(void const*)) const (/home/njn/moz/mi2/xpcom/base/nsCycleCollector.cpp:933) 0x7f6860fdb7af
  #04: nsCycleCollector::SizeOfIncludingThis(unsigned long (*)(void const*), unsigned long*, unsigned long*, unsigned long*, unsigned long*, unsigned long*) const (/home/njn/moz/mi2/xpcom/base/nsCycleCollector.cpp:3029) 0x7f6860fdb6b1
  #05: CycleCollectorMultiReporter::CollectReports(nsIMemoryMultiReporterCallback*, nsISupports*) (/home/njn/moz/mi2/xpcom/base/nsCycleCollector.cpp:3075) 0x7f6860fde432
  #06: nsMemoryInfoDumper::DumpMemoryReportsToFileImpl(nsAString_internal const&) (/home/njn/moz/mi2/xpcom/base/nsMemoryInfoDumper.cpp:626) 0x7f6860fece79
  #07: nsMemoryInfoDumper::DumpMemoryReportsToFile(nsAString_internal const&, bool, bool) (/home/njn/moz/mi2/xpcom/base/nsMemoryInfoDumper.cpp:344) 0x7f6860febaf9
  #08: mozilla::(anonymous namespace)::DumpMemoryReportsRunnable::Run() (/home/njn/moz/mi2/xpcom/base/nsMemoryInfoDumper.cpp:58) 0x7f6860fefe03
}

您可以通过堆栈跟踪顶部附近的 MallocSizeOf 函数的名称来判断哪个内存报告器进行了报告。在本例中,它是循环收集器的报告器。

默认情况下,DMD 不会为大多数块记录分配堆栈跟踪,以使其运行得更快。是否记录的决定是概率性的,较大的块更有可能记录分配堆栈跟踪。所有缺少分配堆栈跟踪的未报告块都将最终出现在单个记录中。例如

Unreported {
  420,010 blocks in heap block record 2 of 5,495
  29,203,408 bytes (27,777,288 requested / 1,426,120 slop)
  Individual block sizes: 2,048 x 3; 1,024 x 103; 512 x 147; 496 x 7; 480 x 31; 464 x 6; 448 x 50; 432 x 41; 416 x 28; 400 x 53; 384 x 43; 368 x 216; 352 x 141; 336 x 58; 320 x 104; 304 x 5,130; 288 x 150; 272 x 591; 256 x 6,017; 240 x 1,372; 224 x 93; 208 x 488; 192 x 1,919; 176 x 18,903; 160 x 1,754; 144 x 5,041; 128 x 36,709; 112 x 5,571; 96 x 6,280; 80 x 40,738; 64 x 37,925; 48 x 78,392; 32 x 136,199; 16 x 31,001; 8 x 4,706
  3.78% of the heap (10.24% cumulative)
  21.24% of unreported (57.53% cumulative)
  Allocated at {
    #01: (no stack trace recorded due to --stacks=partial)
  }
}

相反,当报告块时,始终会记录堆栈跟踪,这意味着您最终可能会遇到这样的记录,其中分配点未知,但报告点已知的

Once-reported {
  104,491 blocks in heap block record 13 of 4,689
  10,392,000 bytes (10,392,000 requested / 0 slop)
  Individual block sizes: 512 x 124; 256 x 242; 192 x 813; 128 x 54,664; 64 x 48,648
  1.35% of the heap (48.65% cumulative)
  1.64% of once-reported (59.18% cumulative)
  Allocated at {
    #01: (no stack trace recorded due to --stacks=partial)
  }
  Reported at {
    #01: mozilla::dmd::DMDFuncs::Report(void const*) (/home/njn/moz/mi5/go64dmd/memory/replace/dmd/../../../../memory/replace/dmd/DMD.cpp:1646)
    #02: WindowsMallocSizeOf(void const*) (/home/njn/moz/mi5/dom/base/nsWindowMemoryReporter.cpp:189)
    #03: nsAttrAndChildArray::SizeOfExcludingThis(unsigned long (*)(void const*)) const (/home/njn/moz/mi5/dom/base/nsAttrAndChildArray.cpp:880)
    #04: mozilla::dom::FragmentOrElement::SizeOfExcludingThis(unsigned long (*)(void const*)) const (/home/njn/moz/mi5/dom/base/FragmentOrElement.cpp:2337)
    #05: nsINode::SizeOfIncludingThis(unsigned long (*)(void const*)) const (/home/njn/moz/mi5/go64dmd/parser/html/../../../dom/base/nsINode.h:307)
    #06: mozilla::dom::NodeInfo::NodeType() const (/home/njn/moz/mi5/go64dmd/dom/base/../../dist/include/mozilla/dom/NodeInfo.h:127)
    #07: nsHTMLDocument::DocAddSizeOfExcludingThis(nsWindowSizes*) const (/home/njn/moz/mi5/dom/html/nsHTMLDocument.cpp:3710)
    #08: nsIDocument::DocAddSizeOfIncludingThis(nsWindowSizes*) const (/home/njn/moz/mi5/dom/base/nsDocument.cpp:12820)
  }
}

是否为所有块记录分配堆栈跟踪的选择由选项控制(见下文)。

“实时”模式输出

对于“实时”模式,dmd.py 的输出描述了当前存在的哪些实时堆块。此输出被分成多个部分。

  1. “调用”。这会告诉您 DMD 如何被调用,即使用了哪些选项。

  2. “实时堆栈跟踪记录”。这会告诉您哪些堆块存在。

  3. “摘要”:提供对总堆的测量。

各个记录类似于“暗物质”模式中输出的记录。

“累积”模式输出

对于“累积”模式,dmd.py 的输出描述了实时堆块如何被内存报告覆盖。此输出被分成多个部分。

  1. “调用”。这会告诉您 DMD 如何被调用,即使用了哪些选项。

  2. “累积堆栈跟踪记录”。这会告诉您在会话期间分配了哪些堆块。

  3. “摘要”:提供对总(累积)堆的测量。

各个记录类似于“暗物质”模式中输出的记录。

“扫描”模式输出

对于“扫描”模式,dmd.py 的输出与“实时”模式相同。可以使用一个单独的脚本 block_analyzer.py 来查找有关哪些块引用特定块的信息。需要先在日志上运行 dmd.py --clamp-contents。有关如何使用堆扫描模式修复涉及引用计数对象的内存泄漏的概述,请参阅此其他页面

选项

运行时

当您运行 mach run --dmd 时,您可以指定其他选项来控制 DMD 的运行方式。运行 mach help run 以获取有关这些选项的文档。

最有趣的一个是 --mode。可接受的值为 dark-matter(默认值)、livecumulativescan

另一个有趣的是 --stacks。可接受的值为 partial(默认值)和 full。在前一种情况下,大多数块将不会记录分配堆栈跟踪。但是,由于较大的块更有可能记录一个堆栈跟踪,因此即使大多数分配的块没有堆栈跟踪,大多数分配的字节也应该有一个分配堆栈跟踪。如果您想要完整的信息,请使用 --stacks=full,但请注意,在这种情况下,DMD 的运行速度会大大降低。

这些选项也可以放在环境变量 DMD 中,或者将 DMD 设置为 1 以使用默认选项(暗物质和部分堆栈)启用 DMD。

后处理

dmd.py 也接受控制其工作方式的选项。运行 dmd.py -h 以获取文档。以下选项是最有趣的选项。

  • -f / --max-frames。默认情况下,记录显示最多 8 个堆栈帧。您可以选择较小的数字,在这种情况下,更多分配将聚合到每个记录中,但您将拥有较少的上下文。或者,您可以选择较大的数字,在这种情况下,分配将拆分到更多记录中,但您将拥有更多上下文。没有一个最佳值,但 2..10 范围内的值通常很好。最大值为 24。

  • -a / --ignore-alloc-fns。许多分配堆栈跟踪以多个提及分配包装器函数的帧开头,例如 js_calloc() 调用 replace_calloc()。此选项会过滤掉这些帧。当使用较小的 --max-frames 值时,它通常有助于提高输出质量。

  • -s / --sort-by。这控制记录的排序方式。可接受的值为 usable(默认值)、reqslopnum-blocks

  • --clamp-contents。对于堆扫描日志,这会对每个块的内容执行保守的指针分析,将任何指向活动块中间的指针的值更改为指向该块开头的指针。所有其他值都更改为 null。此外,所有尾随的 null 都将从块内容中删除。

例如,如果您将以下命令应用于在“实时”模式下获得的配置文件

dmd.py -r -f 2 -a -s slop

它将使您很好地了解主要浪费来源在哪里。

dmd.py 还可以计算两个 DMD 输出文件之间的差异,只要这些文件是在相同模式下生成的。只需将其传递给两个文件名而不是一个文件名即可获得差异。

报告哪些堆块?

在此阶段,您可能想知道 DMD 在“暗物质”模式下如何知道哪些分配已报告,哪些未报告。DMD 只知道通过使用以下两个宏之一创建的函数测量的堆块

MOZ_DEFINE_MALLOC_SIZE_OF
MOZ_DEFINE_MALLOC_SIZE_OF_ON_ALLOC

幸运的是,大多数现有的内存报告器都执行此操作。有关如何编写内存报告器的更多详细信息,请参阅Performance/Memory_Reporting