内存工具架构

内存工具由三个主要部分组成

  1. 实时堆图存在于内存中,并由 C++ 分配器和垃圾收集器管理。为了访问此图的结构,创建了一个专门的接口来表示其状态。 JS::ubi::Node 是此表示的基础。此接口可以从实时堆图创建,也可以从之前某个时间点的序列化离线快照创建。我们各种堆分析(普查、支配树、最短路径等)都在 JS::ubi::Node 图之上运行。名称中的 ubi 代表“无处不在”,并为 C++ 代码中的内存分析提供命名空间。

  2. HeapAnalysesWorker 在工作线程中运行,对快照执行分析并将结果转换为前端可以简单快速渲染的内容。 HeapAnalysesClient 用于在工作线程和主线程之间进行通信。

  3. 最后,最后一个元素是前端,它将从 HeapAnalysesClient 接收到的数据渲染到 DOM,并将用户输入转换为使用 HeapAnalysesClient 的新数据请求。

与其他工具(如 JavaScript 调试器)不同,内存工具很少使用远程 DevTools 服务器及其中的参与者。 MemoryActor 的使用仅限于打开和关闭分配堆栈记录以及将堆快照从被调试程序(位于服务器端)传输到 HeapAnalysesWorker(位于客户端)。自然而然地,一个不错的优势是,支持“旧版”服务器(例如,使用 Firefox Developer Edition 作为客户端来远程调试 Android 版 Firefox 发行版服务器)是不需要额外操作的。随着我们添加新的分析,我们可以毫无问题地在旧服务器上拍摄的快照上运行它们。唯一的要求是快照格式本身的更改必须保持向后兼容。

JS::ubi::Node

JS::ubi::Node 是一种轻量级可序列化接口,可以表示堆图的当前状态。要更深入地了解其工作原理,在 js/public/UbiNode.h 中有非常详细的文档。

“堆快照”是在某个特定过去时间点对堆图的表示。

“堆分析”是在 JS::ubi::Node 堆图上运行的算法。通常,分析可以在实时堆图或反序列化的快照上运行。示例分析包括“普查”,它将节点聚合并计数到各种用户指定的桶中;“支配树”,它计算堆图中所有节点的 支配 关系和保留大小;以及“最短路径”,它查找从 GC 根到节点子集的最短路径。

保存堆快照

保存堆快照有一些要求

  1. 二进制格式必须保持向后兼容并可扩展。

  2. 在序列化过程中,实时堆图不能发生变化。

  3. 保存堆快照的行为应尽可能减少内存开销。如果我们正在拍摄快照以调试频繁的内存不足错误,我们不希望自己触发内存不足错误!

为了解决 (1),我们使用 protobuf 消息格式。消息定义本身位于 devtools/shared/heapsnapshot/CoreDump.proto 中。我们始终使用 optional 字段,以便我们将来可以更改哪些字段是必需的。反序列化会检查反序列化的 protobuf 消息的语义完整性。

对于 (2),我们依靠 SpiderMonkey 的 GC 根危害静态分析和 AutoCheckCannotGC 动态分析来确保 JS 和 GC 都不运行并修改对象或将它们从内存中的一个地址移动到另一个地址。对于 循环收集器,没有等效的抑制和静态分析技术,因此必须注意不要调用可能启动循环收集或从循环收集器的角度修改堆图的方法。在撰写本文时,我们尚不支持在快照中保存循环收集器的那部分堆图,但这项工作被认为非常重要且优先级很高。

最后,(3) 要求我们不要在内存中构建序列化的堆快照二进制 blob,而是边生成边将其流式传输到磁盘。

一旦所有这些都考虑在内,保存快照就变得非常简单。我们使用 JS::ubi::NodeJS::ubi::BreadthFirst 遍历实时堆图,为每个节点及其每个节点的边创建 protobuf 消息,并在继续遍历到下一个节点之前将这些消息写入磁盘。

此功能作为 ChromeUtils.saveHeapSnapshot 函数公开给 chrome JavaScript。有关 API 文档,请参阅 dom/webidl/ChromeUtils.webidl

读取堆快照

读取堆快照的限制少于保存堆快照。构成核心转储的 protobuf 消息逐个反序列化,存储为一组 DeserializedNode 和一组 DeserializedEdge,结果是一个 HeapSnapshot 实例。

DeserializedNodeDeserializedEdge 类实现了 JS::ubi::Node 接口。在离线堆快照而不是实时堆图上运行的分析会对这些类进行操作(当然,它们并不知道这一点)。

有关更多详细信息,请参阅 mozilla::devtools::HeapSnapshotmozilla::devtools::Deserialized{Node,Edge} 类。

堆分析

堆分析在 JS::ubi::Node 图上运行,而不知道该图是由实时堆图还是离线堆快照支持。它们必须确保永远不会分配 GC 对象或修改实时堆图。

通常,分析在自己的 js/public/Ubi{AnalysisName}.h 头文件中实现(例如 js/public/UbiCensus.h),并通过 HeapSnapshot webidl 接口上的方法公开给 chrome JavaScript 代码。

对于我们在 HeapSnapshot webidl 接口上公开给 chrome JavaScript 的每个分析,Gecko 中都有一小部分胶水代码。 mozilla::devtools::HeapSnapshot C++ 类实现了 webidl 接口。分析方法(例如 ComputeDominatorTree)获取堆快照中的反序列化节点和边,从中创建 JS::ubi::Node,调用来自 js/public/Ubi*.h 的分析,并将结果包装成可以在 JavaScript 中表示的内容。

有关运行特定分析的 API 文档,请参阅 HeapSnapshot webidl 接口。

测试 JS::ubi::Node、快照和分析

大部分测试位于 devtools/shared/heapsnapshot/tests/** 中。对于读取和保存堆快照,大多数测试都是 gtests。可以使用 mach gtest DevTools.* 命令运行 gtests。其余的是集成健全性测试,以确保我们可以在各种环境中读取和保存快照,例如 xpcshell 或工作线程。这些可以使用通常的 mach test $PATH 命令运行。

js/src/jit-test/tests/heap-analysis/*js/src/jit-test/tests/debug/Memory-*js/src/jsapi-tests/testUbiNode.cpp 中还有与 JS::ubi::Node 相关的单元测试。有关运行 JIT 测试,请参阅 https://firefox-source-docs.mozilla.ac.cn/js/test.html#running-jit-tests-locally。

HeapAnalysesWorker

HeapAnalysesWorker 负责协调对快照运行特定的分析,并将结果转换为前端可以简单快速渲染的内容。 这些分析可能需要一些时间才能运行(有时需要几秒钟),因此在工作线程中执行它们可以使界面保持响应。 HeapAnalysisClient 为主线程提供了与 worker 的接口。

HeapAnalysesWorker 本身并没有做太多事情;主要只是对数据进行整理,将其从一种表示形式转换为另一种表示形式,或者调用 WebIDL 公开的 C++ 实用函数来执行这些操作。 其中大部分实现为对生成的普查或支配树的遍历。

有关 HeapAnalysesWorker 委托的各种数据转换和整理的详细信息,请参阅以下文件。

  • devtools/shared/heapsnapshot/CensusUtils.js

  • devtools/shared/heapsnapshot/CensusTreeNode.js

  • devtools/shared/heapsnapshot/DominatorTreeNode.js

测试 HeapAnalysesWorkerHeapAnalysesClient

HeapAnalysesWorkerHeapAnalysesClient 的测试位于 devtools/shared/heapsnapshot/tests/** 中,可以使用通常的 mach test $PATH 命令运行。

前端

内存工具的前端使用 React 和 Redux 构建。

React 拥有详尽的文档。

Redux 拥有详尽的文档。

我们在 devtools/client/memory/components/* 中有 React 组件。

我们在 devtools/client/memory/reducers/* 中有 Redux reducers。

我们在 devtools/client/memory/actions/* 中有 Redux actions 和创建 action 的任务。

React 组件应该从它们的 props 到渲染的(虚拟)DOM 是纯函数。 Redux reducers 也应该具有可观察的纯度。

前端的非纯性仅限于创建和分派 action 的任务。 所有与外部世界(例如 HeapAnalysesWorker、远程 DevTools 服务器或文件系统)的通信都限制在这些任务内。

快照状态

在 JavaScript 端,快照表示对底层堆转储和各种分析的引用。 下图表示描述快照状态的有限状态机。 这些状态中的任何一个都可能进入 ERROR 状态,并且从该状态永远无法离开。

SAVING → SAVED → READING → READ
                  ↗
         IMPORTING

每种报告类型(普查、差异、树状图、支配者)都有自己的状态,并在 devtools/client/memory/constants.js 中有文档说明。 这些报告状态会在 UI 中更新各种过滤和选择选项时更新。

测试前端

React 组件的单元测试位于 devtools/client/memory/test/chrome/* 中。

action、reducer 和状态更改的单元测试位于 devtools/client/memory/test/xpcshell/* 中。

前端和整个内存工具的整体集成测试位于 devtools/client/memory/test/browser/* 中。

所有测试都可以使用通常的 mach test $PATH 命令运行。