开源软件的性能
MemShrink

凯尔·休伊

简介

Firefox 长期以来一直以占用过多内存而闻名。这种声誉的准确性多年来有所不同,但它一直伴随着这款浏览器。过去几年中的每个 Firefox 版本都遭到了用户的质疑,他们会问:“他们修复内存泄漏了吗?”我们在经过漫长的测试周期和几次错过的发布日期后,于 2011 年 3 月发布了 Firefox 4——它也遭到了同样的质疑。虽然 Firefox 4 在开放视频、JavaScript 性能和加速图形等领域是网络发展的一大进步,但不幸的是,它在内存使用方面却是一大倒退。

近年来,网页浏览器领域竞争变得非常激烈。随着移动设备的兴起、Google Chrome 的发布以及微软对网络的重新投资,Firefox 发现自己不得不与众多优秀且资金雄厚的竞争对手竞争,而不仅仅是垂死的 Internet Explorer。特别是 Google Chrome,它竭尽全力提供快速且精简的浏览体验。我们开始艰难地认识到,成为一个优秀的浏览器已经不够了;我们需要成为一个卓越的浏览器。正如 Mozilla 工程副总裁(当时)兼长期 Mozilla 贡献者迈克·谢弗所说:“这就是我们想要的世界,也是我们创造的世界。”

这就是我们在 2011 年初所面临的境地。Firefox 的市场份额持平或下降,而 Google Chrome 则迅速崛起。尽管我们已经开始缩小性能差距,但在内存消耗方面,我们仍然处于明显的竞争劣势,因为 Firefox 4 为了获得更快的 JavaScript 和加速图形,往往以增加内存消耗为代价。Firefox 4 发布后,由尼古拉斯·内瑟科特领导的一组工程师启动了 MemShrink 项目,以控制内存消耗。今天,近一年半后,这一集中努力彻底改变了 Firefox 的内存消耗和声誉。在大多数用户的脑海中,“内存泄漏”已成为过去,Firefox 在对比中经常被评为最精简的浏览器之一。在本章中,我们将探讨为改进 Firefox 内存使用而做出的努力以及在此过程中获得的经验教训。

架构概述

要理解我们遇到的问题和找到的解决方案,您需要对 Firefox 的功能和工作原理有一个基本的了解。

现代网页浏览器从根本上来说是运行不受信任代码的虚拟机。此代码是第三方提供的 HTML、CSS 和 JavaScript (JS) 的某种组合。还有来自 Firefox 加载项和插件的代码。虚拟机提供计算、文本布局和样式、图像、网络访问、离线存储,甚至硬件加速图形访问等功能。其中一些功能是通过专为手头任务设计的 API 提供的;许多其他功能是通过已重新用于全新用途的 API 提供的。由于网络的演变方式,网页浏览器必须对它们接受的内容非常宽容,而 15 年前浏览器设计用于处理的内容可能不再与当今提供高性能体验相关。

Firefox 由 Gecko 布局引擎和 Spidermonkey JS 引擎提供支持。两者主要为 Firefox 开发,但它们是独立且可独立重用的代码片段。与所有广泛使用的布局和 JS 引擎一样,两者都是用 C++ 编写的。Spidermonkey 实现 JS 虚拟机,包括垃圾回收和多种即时编译 (JIT) 方式。Gecko 实现对网页可见的大多数 API,包括 DOM、通过软件或硬件管道进行图形渲染、页面和文本布局、完整的网络堆栈等等。它们共同提供了 Firefox 所构建的平台。Firefox 用户界面(包括地址栏和导航按钮)只是一系列具有增强权限的特殊网页。这些权限允许它们访问普通网页无法看到的各种功能。我们将这些特殊的内置特权页面称为chrome(与 Google Chrome 无关),而不是content或普通网页。

就我们而言,关于 Spidermonkey 和 Gecko 最有趣的细节是如何管理内存的。我们可以根据两个特征对浏览器中的内存进行分类:如何分配和如何释放。动态分配的内存()是从操作系统获取的大块内存,并由堆分配器将其划分为请求的数量。有两个主要的堆分配器:用于 Spidermonkey 中垃圾回收内存(GC 堆)的专用垃圾回收堆分配器,以及由 Spidermonkey 和 Gecko 中的所有其他内容使用的 jemalloc。还有三种释放内存的方法:手动、通过引用计数和通过垃圾回收。

Figure 5.1 - Memory management in Firefox

图 5.1 - Firefox 中的内存管理

Spidermonkey 中的 GC 堆包含对象、函数以及运行 JS 创建的大多数其他内容。我们还在 GC 堆中存储其生命周期与这些对象相关联的实现细节。此堆使用一个相当标准的增量标记-清除收集器,该收集器已针对性能和响应能力进行了大量优化。这意味着垃圾收集器每隔一段时间就会唤醒并查看 GC 堆中的所有内存。从一组“根”(例如您正在查看的页面的全局对象)开始,它“标记”堆中所有可访问的对象。然后它“清除”所有未标记的对象,并在需要时重用该内存。

在 Gecko 中,大多数内存都是引用计数的。使用引用计数,会跟踪对给定内存块的引用次数。当该数字达到零时,内存将被释放。虽然引用计数从技术上讲是一种垃圾回收形式,但在此讨论中,我们将它与需要专用代码(即垃圾收集器)定期回收内存的垃圾回收方案区分开来。简单的引用计数无法处理循环,其中一个内存块 A 引用另一个内存块 B,反之亦然。在这种情况下,A 和 B 的引用计数均为 1,并且永远不会被释放。Gecko 具有专门的跟踪垃圾收集器,专门用于收集这些循环,我们将其称为循环收集器。循环收集器仅管理已知参与循环并选择加入循环收集的某些类,因此我们可以将循环收集堆视为引用计数堆的子集。循环收集器还与 Spidermonkey 中的垃圾收集器协同工作,以处理跨语言内存管理,以便 C++ 代码可以保存对 JS 对象的引用,反之亦然。

Spidermonkey 和 Gecko 中也存在大量手动管理的内存。这包括从数组和哈希表的内部内存到图像和脚本源数据缓冲区的所有内容。在手动管理的内存之上,还存在其他专门的分配器。一个例子是竞技场分配器。当大量单独的分配都可以同时释放时,就会使用竞技场。竞技场分配器从主堆分配器获取内存块,并根据需要将其细分。当不再需要竞技场时,竞技场会将这些块返回到主堆,而无需单独释放许多较小的分配。Gecko 使用竞技场分配器进行页面布局数据,当不再需要页面时,可以一次性丢弃这些数据。竞技场分配还允许我们实现诸如中毒之类的安全功能,在中毒中,我们覆盖已释放的内存,以便它不能用于安全漏洞利用。

Firefox 的小部分中还有其他几个自定义内存管理系统,用于各种不同的原因,但它们与我们的讨论无关。现在您已经对 Firefox 的内存架构有了简要的概述,我们可以讨论我们发现的问题以及如何解决这些问题。

你衡量什么,你就得到什么

解决问题的第一步是找出问题是什么。内存泄漏的严格定义是从操作系统 (OS) 分配内存而不将其释放回 OS,但这并不涵盖我们感兴趣的所有改进情况。我们遇到的一些情况并非严格意义上的“泄漏”

更复杂的是,Firefox 堆中的大部分内存都受到某种形式的垃圾回收的影响,因此,不再使用的内存将在下次 GC 运行之前不会被释放。我们开始非常宽泛地使用“泄漏”一词来涵盖导致 Firefox 的内存效率低于其合理水平的任何情况。这与我们的用户使用该术语的方式一致:大多数用户甚至网页开发者都无法判断高内存使用是由于真正的泄漏还是浏览器中正在运行的其他众多因素造成的。

MemShrink 开始时,我们对浏览器的内存使用情况了解不多。确定内存问题的性质通常需要使用复杂的工具(如 Massif)或更低级的工具(如 GDB)。这些工具有几个缺点

为了弥补这些缺点,您获得了一些非常强大的工具。为了解决这些缺点,我们随着时间的推移构建了一套自定义工具,以便更轻松地深入了解浏览器的行为。

这些工具中的第一个是about:memory。它最初是在 Firefox 3.6 中引入的,最初显示有关堆的简单统计信息,例如已映射和已提交的内存量。后来添加了对某些特定开发人员感兴趣的事物的测量,例如嵌入式 SQLite 数据库引擎使用的内存量以及加速图形子系统使用的内存量。我们将这些测量称为内存报告程序。除了这些一次性添加之外,about:memory 仍然是一个原始工具,它提供了一些关于内存使用情况的汇总统计信息。大多数内存没有内存报告程序,并且未在about:memory中专门说明。即便如此,任何人都可以通过在浏览器的地址栏中键入它来使用about:memory,而无需任何特殊工具或 Firefox 版本。这将成为“杀手级功能”。

早在 MemShrink 出现之前,Firefox 中的 JavaScript 引擎就已经重构,将单一的全局 GC 堆拆分为多个较小的子堆,称为隔间(compartments)。这些隔间将 Chrome 和内容(分别为特权和非特权代码)内存以及不同网站的内存隔离开来。此更改的主要动机是安全性,但事实证明它对 MemShrink 非常有用。在实现此功能后不久,我们创建了一个名为 about:compartments 的工具原型,用于显示所有隔间、它们使用了多少内存以及它们如何使用这些内存。about:compartments 从未直接集成到 Firefox 中,但在 MemShrink 启动后,它被修改并合并到 about:memory 中。

在将此隔间报告添加到 about:memory 时,我们意识到,将类似的报告纳入其他分配中,将能够在没有 Massif 等专用工具的情况下进行有用的堆分析。about:memory 进行了更改,不再生成一系列汇总统计信息,而是显示一个树状结构,将内存使用情况细分为大量不同的用途。然后,我们开始为其他类型的较大堆分配添加报告程序,例如布局子系统。我们最早的几个基于指标的努力之一是减少堆未分类内存的数量,即没有内存报告程序涵盖的内存。我们选择了一个相当随意的数字,即总堆的 10%,并着手在平均使用场景中将堆未分类降低到这个数量。最终事实证明,10% 的目标太低了。浏览器中存在太多小的单次分配,无法可靠地将堆未分类降低到大约 15% 以下。减少堆未分类的数量可以提高对浏览器如何使用内存的洞察力。

为了减少堆未分类的数量,我们编写了一个名为暗物质检测器(DMD)的工具,帮助跟踪未报告的堆分配。它的工作原理是替换堆分配器,并将自身插入 about:memory 报告过程中,并将报告的内存块与分配的块进行匹配。然后,它按调用站点汇总未报告的内存分配。在 Firefox 会话上运行 DMD 会生成负责堆未分类的调用站点的列表。一旦确定了分配的来源,找到负责的组件和开发人员来为其添加内存报告程序的过程就会很快。在几个月内,我们拥有了一个可以告诉您诸如“浏览器中的所有 Facebook 页面正在使用 250 MB 内存,以及内存使用情况的细目”之类的工具。

我们还开发了另一个工具(称为测量和保存),用于在识别内存问题后对其进行调试。此工具会将 JS 堆和循环收集的 C++ 堆的表示形式转储到一个文件中。然后,我们编写了一系列分析脚本,可以遍历组合的堆并回答诸如“是什么让这个对象保持活动状态?”之类的问题。这使得许多有用的调试技术成为可能,从仅检查堆图中应该已断开的链接,到进入调试器并在感兴趣的特定对象上设置断点。

这些工具的一个主要优势是,与 Massif 等工具不同,您可以在问题出现后才使用该工具。许多堆分析器(包括 Massif)必须在程序启动时启动,而不是在问题出现后中途启动。这些工具的另一个优势是,可以在没有问题重现的情况下分析和使用这些信息。它们共同允许用户捕获他们看到的错误信息并将其发送给开发人员,即使开发人员无法重现该问题。期望 Web 浏览器用户,即使是那些足够精通以在错误跟踪器中提交错误报告的用户,在浏览器上使用 GDB 或 Massif 通常要求过高。但是,加载 about:memory 或运行一小段 JavaScript 代码以获取要附加到错误报告的数据要轻松得多。通用堆分析器捕获大量信息,但代价也很高。我们能够编写一组针对我们特定需求量身定制的工具,这些工具为我们提供了比通用工具显著的优势。

并非总是值得投资定制工具;我们使用 GDB 而不是为我们构建的每个软件编写新的调试器是有原因的。但对于现有工具无法以您想要的方式提供您需要的信息的情况,我们发现定制工具可以带来很大的收益。我们在 about:memory 上花费了大约一年的兼职工作才达到我们认为它已完成的程度。即使在今天,我们仍在必要时添加新功能和报告程序。定制工具是一项重大投资。对该主题的广泛讨论超出了本章的范围,但在编写自定义工具之前,您应该仔细权衡其利弊。

唾手可得的成果

我们构建的工具使我们比以前更清楚地了解了浏览器中的内存使用情况。使用它们一段时间后,我们开始了解什么是正常的,什么是不正常的。发现不正常且可能是错误的事情变得非常容易。大量堆未分类表明使用了我们尚未添加内存报告程序的更深奥的 Web 功能,或者 Gecko 内部存在泄漏。JS 引擎中奇怪位置的高内存使用率可能表明代码遇到了某些未经优化的或病态的情况。我们能够利用这些信息来跟踪并修复 Firefox 中最严重的错误。

我们早期发现的一个异常情况是,有时一个隔间会停留在已关闭的页面上,即使强制垃圾收集器多次运行也是如此。有时这些隔间最终会自行消失,有时则会无限期地存在。我们将这些泄漏称为僵尸隔间。这些是我们一些最严重的泄漏,因为网页可以使用的内存量是无限的。我们在 Gecko 和 Firefox UI 代码中修复了许多此类错误,但很快发现僵尸隔间最大的来源是附加组件。处理附加组件中的泄漏使我们困惑了几个月,直到我们找到了一种解决方案,该解决方案将在本章后面讨论。Firefox 和附加组件中的大多数这些僵尸隔间都是由长期存在的 JS 对象维护对短期 JS 对象的引用引起的。长期存在的 JS 对象通常是附加到浏览器窗口的对象,甚至是全局单例,而短期存在的 JS 对象可能是来自网页的对象。

由于 DOM 和 JS 的工作方式,对网页中单个对象的引用会使整个页面及其全局对象(以及从中可以访问的任何内容)保持活动状态。这很容易累积到许多兆字节的内存。垃圾回收系统的一个更微妙的方面是,GC 仅在内存不可访问时才会回收内存,而不是在程序完成使用它时回收。程序员有责任确保不再使用的内存不可访问。当引用者和被引用者的生命周期预计会有很大差异时,未能删除对对象的所有引用会产生更严重的后果。应该相对快速回收的内存(例如网页使用的内存)反而与更长生命周期的引用者(例如浏览器窗口或应用程序本身)的生命周期绑定在一起。

JS 堆中的碎片也是我们面临的一个问题,原因与此类似。我们经常发现,关闭大量网页并不会导致操作系统报告的 Firefox 内存使用量显着下降。JS 引擎从操作系统中以兆字节大小的块分配内存,并根据需要在不同的隔间之间细分该块。只有在这些块完全未使用时,才能将其释放回操作系统。我们发现,新块的分配几乎总是由 Web 内容要求更多内存引起的,但阻止块释放的最后一件事通常是 Chrome 隔间。将一些长期存在的对象混合到充满短期存在的对象的块中,阻止我们在关闭网页时回收该块。我们通过隔离 Chrome 和内容隔间来解决此问题,以便任何给定块都包含 Chrome 或内容分配。这大大增加了我们在关闭选项卡时可以返回给操作系统的内存量。

我们发现了另一个部分由减少碎片的技术引起的问题。Firefox 的主要堆分配器是 jemalloc 的一个版本,经过修改可在 Windows 和 Mac OS X 上运行。Jemalloc 旨在减少由于碎片而导致的内存损失。它使用的一种技术是将分配四舍五入到各种大小类别,然后在连续的内存块中分配这些大小类别。这确保了当空间被释放时,它以后可以重复用于类似大小的分配。它还意味着浪费了一些用于舍入的空间。我们将这种浪费的空间称为冗余。对于某些大小类别,最坏的情况可能涉及浪费近 50% 的已分配空间。由于 jemalloc 大小类别的结构方式,这通常发生在超过 2 的幂之后(例如,17 会四舍五入到 32,而 1025 会四舍五入到 2048)。

通常在分配内存时,您对请求的数量没有太多选择。为类的新的实例添加额外的字节到分配中很少有用。其他时候,您有一定的灵活性。如果您正在为字符串分配空间,您可以使用额外的空间来避免在以后追加字符串时必须重新分配缓冲区。当这种灵活性出现时,请求与大小类别完全匹配的数量是有意义的。这样,本来会作为冗余“浪费”的内存就可以免费使用。通常代码被编写为请求 2 的幂,因为它们非常适合所有编写的分配器,并且不需要分配器的特殊知识。

我们在 Gecko 中发现了大量旨在利用此技术的代码,以及一些尝试过但出错的地方。多段代码尝试分配一个不错的圆形内存块,但数学计算略有错误,最终分配的数量超出了预期。由于 jemalloc 的大小类别的构建方式,这通常会导致浪费近 50% 的已分配空间作为冗余。一个特别恶劣的例子是在用于布局数据结构的区域分配器实现中。该区域尝试从堆中获取 4 KB 块。它还为簿记目的附加了一些字,导致它请求略大于 4 KB,这被四舍五入到 8 KB。修复该错误仅在 GMail 上就节省了超过 3 MB 的冗余。在一个特别依赖布局的测试用例中,它节省了超过 700 MB 的冗余,将浏览器的总内存消耗从 2 GB 降低到 1.3 GB。

我们在SQLite中遇到了类似的问题。Gecko使用SQLite作为历史记录和书签等功能的数据库引擎。SQLite的设计初衷是让嵌入式应用程序能够对内存分配进行大量控制,并且非常细致地测量自身的内存使用情况。为了保留这些测量结果,它添加了几个词,从而将分配推入到下一个大小类别。具有讽刺意味的是,用于跟踪内存消耗的检测机制最终导致消耗加倍,同时造成严重的低估。我们将这类错误称为“小丑鞋”,因为它们既滑稽可笑又导致大量浪费的空闲空间,就像小丑的鞋子一样。

不是你的错不等于不是你的问题

在几个月的时间里,我们在改善Firefox的内存消耗和修复内存泄漏方面取得了长足的进步。但并非所有用户都感受到了这些工作的益处。很明显,用户遇到的许多内存问题源于附加组件。我们用于跟踪有内存泄漏附加组件的错误最终记录了超过100个关于导致泄漏的附加组件的确认报告。

从历史上看,Mozilla一直试图在附加组件方面两全其美。我们一直将Firefox推销为一个可扩展的浏览器,拥有丰富的附加组件选择。但是,当用户报告这些附加组件的性能问题时,我们只是告诉用户不要使用它们。导致内存泄漏的附加组件数量之多使得这种情况变得无法持续。许多Firefox附加组件都是通过Mozilla的addons.mozilla.orgAMO)分发的。AMO具有旨在捕获附加组件中常见问题的审查政策。当AMO审查人员开始使用about:memory等工具测试附加组件的内存泄漏时,我们开始了解问题的严重程度。许多经过测试的附加组件被证明存在诸如僵尸隔间等问题。我们开始联系附加组件作者,并整理了一份导致泄漏的最佳实践和常见错误列表。不幸的是,这取得的效果相当有限。虽然一些附加组件确实得到了作者的修复,但大多数都没有。

这被证明无效的原因有很多。并非所有附加组件都会定期更新。附加组件作者是志愿者,有自己的时间安排和优先事项。调试内存泄漏可能很困难,尤其是在您无法重现问题的情况下。我们之前描述过的堆转储工具非常强大,可以轻松收集信息,但分析输出仍然很复杂,而且期望附加组件作者执行此操作要求过高。最后,修复泄漏没有强烈的动机。没有人愿意发布糟糕的软件,但你无法总是修复所有问题。人们也可能更感兴趣于做他们想做的事情,而不是我们想让他们做的事情。

长期以来,我们一直在讨论如何为修复泄漏创造激励措施。附加组件也给Mozilla带来了其他性能问题,因此我们讨论过在AMO或Firefox本身中显示附加组件性能数据。理论上,能够让用户了解他们已安装或即将安装的附加组件的性能影响将有助于他们做出明智的决策。但这首先存在一个问题,即像网页浏览器这样的面向消费者的软件的用户通常无法对这些权衡做出明智的决策。Firefox的4亿用户中有多少人理解什么是内存泄漏,并且能够评估是否值得忍受它才能使用某个随机附加组件?其次,以这种方式处理附加组件的性能影响需要来自Mozilla社区许多不同部分的支持。例如,构成附加组件社区的人并不热衷于用封杀大锤来打击附加组件。最后,很大一部分Firefox附加组件根本不是通过AMO安装的,而是与其他软件捆绑在一起的。除了尝试阻止它们之外,我们对这些附加组件几乎没有影响力。由于这些原因,我们放弃了创造这些激励措施的尝试。

我们放弃为附加组件修复泄漏创造激励措施的另一个原因是,我们找到了解决问题的完全不同的方法。我们最终设法找到了一种在Firefox中“清理”有内存泄漏附加组件的方法。长期以来,我们认为如果没有破坏大量附加组件,这是不可行的,但我们还是继续尝试。最终,我们能够实现一种在不影响大多数附加组件的情况下回收内存的技术。我们利用隔间之间的边界,在页面导航或标签关闭时“切断”从chrome隔间到内容隔间的引用。这会留下一个在chrome隔间中漂浮的对象,该对象不再引用任何内容。我们最初认为,当代码尝试使用这些对象时,这将是一个问题,但我们发现大多数情况下这些对象后来不会被使用。实际上,附加组件意外地且毫无意义地缓存了来自网页的内容,并且自动清理它们几乎没有缺点。我们一直在寻找技术问题的社会解决方案。

永恒的持久性是卓越的代价

MemShrink项目在解决Firefox的内存问题方面取得了长足的进步,但仍有很多工作要做。到目前为止,大多数简单的问题都已得到修复——剩下的问题需要大量的工程工作。我们计划继续减少我们的JS堆碎片,使用可以合并堆的移动垃圾回收器。我们正在重新设计处理图像的方式,使其更节省内存。与许多已完成的更改不同,这些更改需要对复杂子系统进行广泛的重构。

同样重要的是,我们不能倒退我们已经取得的改进。自2006年以来,Mozilla一直拥有强大的回归测试文化。随着我们在减少Firefox内存使用方面取得进展,我们对内存使用回归测试系统的需求也越来越大。测试性能比测试功能更难。构建此系统的最困难的部分是为浏览器想出一个现实的工作负载。现有的浏览器内存测试在真实性方面惨败。例如,MemBuster每次都会快速连续地在新的浏览器窗口中加载多个维基和博客。如今,大多数用户使用标签而不是新窗口,并且浏览比维基和博客更复杂的内容。其他基准测试将所有页面加载到同一个标签页中,这对于现代网页浏览器来说也是完全不现实的。我们设计了一个我们认为相当现实的工作负载。它将100个页面加载到一组固定的30个标签页中,并在加载之间设置延迟以模拟用户阅读页面的情况。使用的页面来自Mozilla现有的Tp5页面集。Tp5是一组来自Alexa Top 100的页面,用于在现有的性能测试基础设施中测试页面加载性能。此工作负载已被证明对我们的测试目的很有用。

测试的另一个方面是确定要测量什么。我们的测试系统在测试运行期间的三个不同点测量内存消耗:在加载任何页面之前、加载所有页面之后以及关闭所有标签页之后。在每个点上,我们还在没有活动30秒后以及强制垃圾回收器运行后进行测量。这些测量有助于查看过去遇到的任何问题是否再次出现。例如,+30秒测量值与强制垃圾回收后测量值之间的显着差异可能表明我们的垃圾回收启发式算法过于保守。在加载任何内容之前进行的测量与关闭所有标签页后进行的测量之间的显着差异可能表明我们存在内存泄漏。我们在所有这些点测量了许多数量,包括驻留集大小、“显式”大小(通过malloc()mmap()等请求的内存量)以及属于about:memory中某些类别(例如heap-unclassified)的内存量。

一旦我们构建了这个系统,我们就将其设置为定期在Firefox的最新开发版本上运行。我们还在Firefox的早期版本(大致到Firefox 4)上运行它。结果是伪持续集成,并有一组丰富的历史数据。通过一些不错的网页开发工作,我们最终得到了areweslimyet.com,这是一个基于Web的公共界面,可以访问我们的内存测试基础设施收集的所有数据。自完成以来,areweslimyet.com已经检测到浏览器不同部分的工作导致的几次回归。

社区

MemShrink工作取得成功的最后一个促成因素是更广泛的Mozilla社区的支持。虽然现在大多数(但肯定不是全部)在Firefox上工作的工程师都受雇于Mozilla,但Mozilla充满活力的志愿者社区以测试、本地化、QA、营销等形式提供支持,如果没有这些支持,Mozilla项目将陷入停滞。我们有意将MemShrink设计成能够获得社区支持,并且这已经取得了相当大的成效。MemShrink核心团队由少数几位付费工程师组成,但我们通过错误报告、测试和附加组件修复获得的社区支持放大了我们的努力。

即使在Mozilla社区内部,内存使用长期以来一直是令人沮丧的根源。有些人亲身经历了这些问题。其他人有朋友或家人遇到过这些问题。那些幸运地避免了这些问题的人无疑也看到过关于Firefox内存使用量的抱怨,或者在他们努力开发的新版本中看到“内存泄漏修复了吗?”的评论。没有人喜欢自己的辛勤工作受到批评,尤其是在你没有从事的工作方面。解决大多数社区成员都能感同身受的长期存在的问题是建立支持的绝佳第一步。

然而,仅仅说我们将要修复问题是不够的。我们必须表明我们认真对待完成工作,并且可以在问题上取得真正的进展。我们举办了每周公开会议,对错误报告进行分类并讨论我们正在进行的项目。Nicholas还在每次会议后发布博客文章,以便不在场的人能够了解我们正在做什么。突出显示正在取得的改进、错误数量的变化以及正在提交的新错误清楚地表明了我们投入MemShrink的精力。并且,我们从唾手可得的成果中获得的早期改进极大地表明了我们能够解决这些问题。

最后一部分是关闭更广泛的社区与从事MemShrink开发的开发人员之间的反馈回路。我们之前讨论过的工具将原本会被关闭为无法重现并被遗忘的错误变成了可以并且已被修复的报告。我们还将对我们的进度报告博客文章的评论、意见和回复转化为错误报告,并试图收集必要的信息来修复它们。所有错误报告都经过分类并确定了优先级。我们还努力调查所有错误报告,即使那些被认定为不重要而无需修复的错误报告。这种调查让报告者感到自己的努力更有价值,并且旨在将错误置于一个状态,以便以后有更多时间的人可以来修复它。这些措施共同建立了强大的社区支持基础,为我们提供了优秀的错误报告和宝贵的测试帮助。

结论

在MemShrink项目运行的两年时间里,我们在Firefox的内存使用方面取得了长足的进步。MemShrink团队已将内存使用量从最常见的用户投诉之一转变为浏览器的卖点,并显著改善了许多Firefox用户的用户体验。

我要感谢 Justin Lebar、Andrew McCreight、John Schoenick、Johnny Stenback、Jet Villegas 和 Timothy Nikkel 为 MemShrink 做出的所有工作,以及其他帮助解决内存问题的工程师。最重要的是,我要感谢 Nicholas Nethercote 将 MemShrink 项目启动,投入大量精力减少 Spidermonkey 的内存使用,领导该项目两年,以及太多无法一一列举的其他贡献。我还要感谢 Jet 和 Andrew 审阅本章。