引用计数跟踪和平衡

引用计数跟踪和平衡是用于追踪使用 BloatView 发现的引用计数对象的泄漏的高级技术。第一步是启用引用计数跟踪运行 Firefox,这将生成一个或多个日志文件。引用计数跟踪记录对 AddrefRelease 的调用,最好针对特定的一组类,包括以符号形式表示的调用栈(在支持此功能的平台上)。引用计数平衡是一个后续步骤,它分析生成的日志以帮助开发人员找出引用计数出错的地方。

如何构建以启用引用计数跟踪

使用 --enable-debug--enable-logrefcnt 构建。

如何在启用引用计数跟踪的情况下运行

可以使用几个环境变量。

首先,选择三个环境变量之一来选择所需的日志记录类型。您几乎肯定需要 XPCOM_MEM_REFCNT_LOG

注意:由于 Windows 上沙箱存在问题(bug 1345568),引用计数日志记录当前需要设置 MOZ_DISABLE_CONTENT_SANDBOX 环境变量。

XPCOM_MEM_REFCNT_LOG

设置此环境变量可启用引用计数跟踪。如果将此环境变量设置为文件名,则日志将输出到该文件。您也可以将其设置为 1 以记录到标准输出或 2 以记录到标准错误,但这些日志很大且捕获成本高,因此您可能不希望这样做。**警告**:如果没有 XPCOM_MEM_LOG_CLASSES 和/或 XPCOM_MEM_LOG_OBJECTS,则不应使用此选项,因为如果没有一些过滤,日志记录将完全无用,因为浏览器运行速度会非常慢,并且生成的日志文件会非常大。

XPCOM_MEM_COMPTR_LOG

此环境变量启用对将对象添加到 nsCOMPtr 和从 nsCOMPtr 中释放对象的日志记录。这需要 C++ 动态转换,因此并非所有平台都支持。但是,拥有 nsCOMPtr 日志并在创建平衡树时使用它,可以消除我们知道已匹配的 AddRef 和 Release 调用,从而使调试引用计数流量很大的对象的引用计数泄漏变得更容易。

XPCOM_MEM_ALLOC_LOG

对于没有堆栈跟踪支持的平台,XPCOM 支持使用通常的 cpp __FILE__ 和 **LINE** 编号宏扩展技巧在 AddRef/Release 的调用站点进行日志记录。这会导致代码运行速度变慢,但至少您可以获得一些关于泄漏可能发生位置的数据。

还必须设置一个或两个额外的环境变量,XPCOM_MEM_LOG_CLASSESXPCOM_MEM_LOG_OBJECTS, 以减少正在记录的对象集,从而将性能提高到勉强可以接受的水平。

XPCOM_MEM_LOG_CLASSES

此变量应包含一个逗号分隔的名称列表,这些名称将用于与正在记录的对象的类型进行比较。例如

env XPCOM_MEM_LOG_CLASSES=nsDocShell XPCOM_MEM_REFCNT_LOG=./refcounts.log ./mach run

这将仅记录 nsDocShell 实例的 AddRefRelease 调用,同时使用 mach 运行浏览器,并将日志记录到文件 refcounts.log 中。请注意,设置 XPCOM_MEM_LOG_CLASSES 还会在“膨胀日志”(即 XPCOM_MEM_BLOAT_LOG 变量指定的文件;有关更多详细信息,请参阅 BloatView 文档)中列出每个泄漏对象的序列号。对象的序列号只是一个唯一的数字,从 1 开始,在分配对象时分配给该对象。

您可以使用对象的序列号以及以下变量来进一步限制引用计数跟踪

XPCOM_MEM_LOG_OBJECTS

将此变量设置为一个逗号分隔的对象序列号序列号范围列表,例如 1,37-42,73,165(序列号从 1 开始,而不是 0)。当此变量与 XPCOM_MEM_LOG_CLASSESXPCOM_MEM_REFCNT_LOG 一起设置时,将仅为列出的特定对象生成堆栈跟踪。例如,

env XPCOM_MEM_LOG_CLASSES=nsDocShell XPCOM_MEM_LOG_OBJECTS=2 XPCOM_MEM_REFCNT_LOG=./refcounts.log ./mach run

将为分配的第二个 nsDocShell 对象将堆栈跟踪记录到 refcounts.log 中,其他对象则不会记录。

后处理步骤 1:查找泄漏者

首先,您必须找出哪些对象泄漏了。脚本 tools/rb/find_leakers.py 可以执行此操作。它遍历日志文件,并找出哪些对象被分配了(它知道是因为它们刚刚被分配,因为它们被 AddRef()-ed 并且它们的引用计数变为 1)。它将它们添加到列表中。当它找到一个被释放的对象(它知道是因为它的引用计数变为 0),它会将其从列表中删除。剩下的任何对象都是泄漏的。

脚本输出如下所示。

0x00253ab0 (1)
0x00253ae0 (2)
0x00253bd0 (4)

括号中的数字表示分配的顺序,如果您关心的话。选择其中一个指针用于步骤 2。

后处理步骤 2:过滤日志

选择了一个泄漏的对象后,可以使用 tools/rb/filter-log.pl 过滤日志文件以删除其他对象的调用栈;此过程减少了日志文件的大小,并提高了性能。

perl -w tools/rb/filter-log.pl --object 0x00253ab0 < ./refcounts.log > my-leak.log

符号化堆栈

日志文件通常缺少函数名、文件名和行号。您需要运行一个脚本来修复调用栈。

python3 tools/rb/fix_stacks.py < ./refcounts.log > fixed_stack.log

此外,还可以 本地符号化 在 TreeHerder 上生成的日志。

后处理步骤 3:构建平衡树

现在您已完全准备好了日志文件,可以构建平衡树。此过程获取所有堆栈 AddRef()Release() 堆栈跟踪,并将其转换为调用图。图中的每个节点都表示一个调用站点。每个调用站点都有一个平衡因子,如果在该站点发生的 AddRef() 调用次数多于 Release() 调用次数,则为正;如果 AddRef()Release() 调用次数相等,则为零;如果在该站点发生的 Release() 调用次数多于 AddRef() 调用次数,则为负。

要构建平衡树,请运行 tools/rb/make-tree.pl,并指定感兴趣的对象。例如

perl -w tools/rb/make-tree.pl --object 0x00253ab0 < my-leak.log

这将构建一个缩进树,看起来类似于这样(除了可能更大且更复杂)

.root: bal=1
  main: bal=1
    DoSomethingWithFooAndReturnItToo: bal=2
      NS_NewFoo: bal=1

让我们假设在我们的玩具示例中,NS_NewFoo() 是一个工厂方法,用于创建新的 foo 并返回它。DoSomethingWithFooAndReturnItToo() 是一个在返回给 main()(主程序)之前修改 foo 的方法。

这棵小树告诉您的是,您在对象 0x00253ab0 上总体泄漏了一个引用计数。但是,更具体地说,它显示您

  • NS_NewFoo() “泄漏”了一个引用计数。这可能是“正常的”,因为它是一个创建 AddRef()-ed 对象的工厂方法。

  • DoSomethingWithFooAndReturnItToo() 泄漏了两个引用计数。嗯……这可能不正常,特别是考虑到……

  • main() 又回到了泄漏一个引用计数。

因此,由此我们可以推断,main() 正确地释放了从 DoSomethingWithFooAndReturnItToo() 返回的对象上获得的引用计数,因此泄漏必须在该函数中的某个地方。

所以现在假设我们修复了 DoSomethingWithFooAndReturnItToo() 中的泄漏,重新运行跟踪,手动遍历日志以查找与新运行中 0x00253ab0 对应的对象,并运行 make-tree.pl。我们希望看到一棵看起来像这样的树

.root: bal=0
  main: bal=0
    DoSomethingWithFooAndReturnItToo: bal=1
      NS_NewFoo: bal=1

也就是说,NS_NewFoo() “泄漏”了一个引用计数;此泄漏被 DoSomethingWithFooAndReturnItToo() “继承”;但最终通过 main() 中的 Release() 平衡。

提示

显然,这是一个迭代和分析的过程。以下是一些使其更容易的技巧。

检查来自智能指针的泄漏。如果泄漏来自 XPCOM_MEM_COMPTR_LOG 中记录的智能指针,则 find-comptr-leakers.pl 将为您找到确切的堆栈,您无需查看树。

忽略平衡树。make-tree.pl 脚本接受一个选项 --ignore-balanced,它指示它不要费心打印平衡因子为零的节点的子节点。这有助于消除否则可能很嘈杂的树中的一些杂乱。

忽略来自智能指针的匹配释放。如果您已检查(参见上文)泄漏不是来自智能指针,则可以忽略来自智能指针的引用(我们可以使用智能指针的指针标识来匹配 AddRef 和 Release)。这需要使用同时收集的 XPCOM_MEM_REFCNT_LOG 和 XPCOM_MEM_COMPTR_LOG。有关更多详细信息,请参阅 旧文档(可能应将其合并到此处)。这最好与 --ignore-balanced 一起使用

玩麻将。不平衡的树不一定是坏事。更有可能的是,它表明一个 AddRef() 被代码中其他地方的另一个 Release() 取消了。因此,游戏就是要尝试将它们相互匹配。

排除函数。为了帮助此过程,您可以创建一个“排除文件”,其中列出您想要从树构建过程中排除的函数的名称(可能是因为您已将它们匹配)。make-tree.pl 有一个选项 --exclude [file],其中 [file] 是一个换行符分隔的函数名称列表,这些函数将在构建树时被排除。具体来说,任何包含该调用站点的调用栈都不会对树中平衡因子的计算做出贡献。

如何为引用计数跟踪和平衡检测您的对象

该过程与为 BloatView 检测它们的过程相同,因为 BloatView 和引用计数跟踪共享底层基础设施。