广告拦截器性能研究
本文将详细分析一些最流行的内容拦截引擎的性能:uBlock Origin、Adblock Plus、Brave、DuckDuckGo和Cliqz/Ghostery 的高级广告拦截器(自 Ghostery 8 起提供),在本文的其余部分,我们将简称其为Ghostery。
本研究的动机源于近期关于Manifest V3 的争议。其中一项提议的变更涉及削弱 WebRequest API 的功能,以限制其拦截能力。对此提出了两种理由:一是性能方面的考量,二是隐私方面的考量。隐私方面的考量值得单独分析,本文不作赘述。本研究表明,性能方面的考量并不成立。我们的对比实验表明,目前最流行的内容拦截器已经非常高效(每次请求的中位决策时间均低于毫秒级),不会给用户带来任何可察觉的额外开销。我们在另一项研究《追踪器税》中指出,拦截广告和追踪器实际上可以将网站加载时间缩短至多一半。此外,效率还在不断提升,诸如 WebAssembly 之类的技术将进一步推动效率的提升。
本次对比不涉及所有扩展程序,而是专注于网络请求拦截引擎,这是内容拦截器执行的最消耗 CPU 资源的任务(尤其需要注意的是,这不包括界面美化引擎或订阅管理)。以下是所有参与对比的内容拦截器的主页:
我们没有纳入 Chromium 和 Safari 项目的原生拦截器,因为这需要花费大量精力将它们打包,以便与其他库进行基准测试。我们将这项工作留待以后完成。
除了uBlock Origin之外,所有拦截器都以 JavaScript 库的形式提供,可以在 Node.js 中加载。为了能够同时比较uBlock Origin ,我们不得不从扩展程序中提取出静态网络过滤引擎。此基准测试中使用的uBlock Origin版本未使用 WebAssembly版本的域名匹配。
所有基准测试均在 X1 Carbon 2016 (i7 U6600 + 16 GB) 上运行,Node.js 版本为 11.9.0。内存测量是在 Google Chrome 版本 72.0.3626.96 中使用内存快照工具进行的。
结果
在详细介绍结果分析之前,让我们先简要概括一下我们的发现:
- 除了DuckDuckGo之外,所有内容拦截器每次请求的平均决策时间都低于毫秒级。
-
Ghostery 处理请求所需时间(中位数):0.007 毫秒
- 比uBlock Origin快 2.7 倍
- 比Adblock Plus快 2.9 倍
- 比Brave的广告拦截器快 6.3 倍
- 比DuckDuckGo的广告拦截器快 1258.4 倍
-
正在从缓存加载 Ghostery 的阻塞引擎: 0.03 毫秒
- 比Brave的广告拦截器快 368 倍
- 比uBlock Origin快 588 倍
- 比Adblock Plus快 3575 倍
- DuckDuckGo的广告拦截器不提供序列化功能,因此加载成本始终是解析列表的成本。
-
Ghostery 拦截引擎的内存占用(在 Chrome 浏览器中启动时):1.8 MB
- 比uBlock Origin节省 1.6 倍内存
- 比Adblock Plus节省 8.4 倍内存
- 比DuckDuckGo的广告拦截器节省 8.8 倍内存
- 由于无法使用开发者工具评估Brave的内存使用情况,因此本节不包含相关信息。
0. 关于数据集
为了衡量每个内容拦截器的性能,我们对来自热门域名的请求进行了一次重放,并记录了决定是否拦截这些请求所需的时间。然后,我们从三个不同的角度分析了结果:所有请求、仅拦截请求和未拦截请求(均来自同一次运行)。
该请求数据集是使用一组 Chrome 无头浏览器(由puppeteer库驱动)创建的,这些浏览器访问了 Cliqz Search 排名前 500 的域名的首页,以及每个域名的最多 3 个页面(从首页随机选择),并收集了所有看到的网络请求(URL、框架 URL 和类型)。数据集经过随机打乱,使得不同页面的访问顺序是随机的,但每个页面上看到的请求都按照最初记录的方式重放。
该数据集包含 242944 个请求。我们已将数据公开发布在以下 URL:requests_top500.json.gz。用于创建数据集的脚本也已提供:create_dataset.js 。此外,我们还使用了 shuffle_dataset.js来对请求进行随机排序,从而生成最终数据。
1. 请求的组成
为了进行比较,我们假设每个网络请求都可以被内容拦截器阻止或允许;我们将决定是否阻止请求的过程称为“匹配”。我们观察到,在我们的数据集中,只有约 19.2% 的请求被阻止(所有内容拦截器的平均值)。
由此观察可知,如果内容拦截器能够有效地决定不拦截哪些请求,那么它们的平均性能会更好。
用于判断是否阻止请求的过滤器来自Easylist,我们在运行基准测试之前移除了所有无关规则。最终列表包含38978 个网络过滤器,可在此处获取:easylist.txt。
需要注意的是,启用EasyPrivacy等额外的过滤器列表将会阻止更大比例的请求。
2. 匹配所有请求所需时间
我们首先查看所有请求(无论它们最终是否会被拦截)。我们使用对数刻度作为 x 轴(时间单位为毫秒),以便于比较内容拦截器决定是否拦截请求所需时间的累积分布情况。
以下是各内容拦截器第 99 百分位数和中位数的详细数据:
| 99% 的请求 | 中位数 | |
|---|---|---|
| 幽灵 | 0.050毫秒 | 0.007毫秒 |
| uBlock Origin | 0.124毫秒(慢2.5倍) | 0.017毫秒(慢2.7倍) |
| Adblock Plus | 0.103毫秒(慢2.1倍) | 0.019毫秒(慢2.9倍) |
| 勇敢的 | 1.288毫秒(慢了25.9倍) | 0.041毫秒(慢了6.3倍) |
| DuckDuckGo | 12.085毫秒(慢了242.5倍) | 8.270毫秒(慢了1258.4倍) |
下面您可以找到这些时间点的累积分布图:
3. 匹配未被阻止的请求所需时间
下表详细列出了未被阻止的请求的第 99 百分位数和中位数时间:
| 99% 的请求 | 中位数 | |
|---|---|---|
| 幽灵 | 0.049毫秒 | 0.006毫秒 |
| uBlock Origin | 0.112毫秒(慢2.3倍) | 0.018毫秒(慢2.8倍) |
| Adblock Plus | 0.105毫秒(慢2.2倍) | 0.020毫秒(慢3.1倍) |
| 勇敢的 | 1.270毫秒(慢了26.2倍) | 0.038毫秒(慢5.9倍) |
| DuckDuckGo | 11.190毫秒(慢了230.5倍) | 6.781毫秒(慢了1060.5倍) |
4. 匹配被阻止请求所需的时间
下表详细列出了被阻止请求的第 99 百分位数和中位数时间:
| 99% 的请求 | 中位数 | |
|---|---|---|
| 幽灵 | 0.052毫秒 | 0.007毫秒 |
| uBlock Origin | 0.165毫秒(慢3.1倍) | 0.016毫秒(慢2.2倍) |
| Adblock Plus | 0.099毫秒(慢1.9倍) | 0.014毫秒(慢1.9倍) |
| 勇敢的 | 1.468毫秒(慢了28.0倍) | 0.062毫秒(慢8.5倍) |
| DuckDuckGo | 13.025毫秒(慢了248.5倍) | 8.31毫秒(慢了1130.6倍) |
从这些图表中我们可以看到Adblock Plus、Brave和DuckDuckGo的响应速度都趋于平稳。这可以解释为这些引擎内部实现了某种形式的缓存,因此对于一些已经出现过的请求,它们的响应速度非常快(请求冗余既来自多个网站上常见的第三方服务,也来自我们为每个域名加载的多个页面)。这种缓存机制可以应用于任何内容拦截器,因此并不能反映每个拦截器的效率;我们可以将其视为一种用内存和CPU 使用率进行权衡的手段。
从之前的测量结果可以看出,Ghostery 在匹配速度方面优于其他库。这里就不赘述细节了,以下是一些可以解释这些结果的优化:
- Ghostery 使用反向索引将词元与过滤器关联起来。与其他库不同,我们确保在构建时为每个过滤器选择最佳词元(最佳词元定义为出现次数最少的词元)。这会带来一次性的额外开销,但能最大限度地提高分发能力。
- 过滤器以非常紧凑的形式存储在类型化数组中,并且仅在有可能被阻止时(例如,在 URL 中遇到相同的标记)才延迟加载到内存中。
- 加载到内存中的过滤器会进行实时优化,并且可以组合多个过滤器以提高效率。这些优化是根据 Easylist 中观察到的常见情况精心设计的。
5. 序列化和反序列化
本节我们将探讨内容拦截器在序列化其内部表示以加快后续加载速度方面的性能。只有DuckDuckGo的引擎不支持此功能。uBlock Origin、Ghostery、Adblock Plus和Brave都允许将整个拦截引擎序列化或缓存(uBlock Origin称之为“自拍”)到字符串或缓冲区中,从而加速后续加载。
由于这是一次性操作,因此加载时间稍长对桌面用户的影响并不显著。但另一方面,对于移动设备而言,快速初始化内容拦截器至关重要。
这项功能还允许另一种用例,即在后端执行列表解析,并将内容拦截器的序列化形式直接发送给客户端,从而完全消除初始化的成本。
我们对每个内容拦截器执行了 100 次序列化,结果如下所示:
此条形图显示了每个内容拦截器序列化引擎所花费的中位时间:
同样地,我们测量了从序列化形式恢复内容拦截器所需的时间:
以下是中位时间:
最后,我们测量了每个内容拦截器的序列化缓冲区大小:
从这些测量结果可以看出,Ghostery不仅序列化和反序列化速度明显更快,而且缓存大小也更小。
原因如下:内部表示大部分已经以紧凑的形式存储(使用类型化数组);这意味着序列化只需在已有的数组旁边添加少量元数据,而反序列化几乎是瞬间完成的,因为只需在序列化缓冲区之上创建一些类型化数组视图即可(可以想象mmap成使用类型化数组的数组)。这也解释了其极低的内存消耗:初始化之后,内存使用量仅略高于序列化形式的大小。
6. 启动时的内存消耗
这里我们考察了每个内容拦截器在完成一次完整垃圾回收后,从列表(而非缓存)初始化的内存使用情况。测量是使用 Chrome 开发者工具的内存快照进行的。我们没有测量 Brave,因为快照似乎没有考虑 C++ 端使用的内存。此外,请注意,由于内容拦截器可能会缓存常用资源等,因此运行时内存使用情况可能会有所不同。
如前文序列化部分所述, Ghostery极低的内存占用率可归因于其内部表示主要由非常紧凑的类型化数组构成,仅有少量额外元数据开销。再次强调,此处仅指 Ghostery 的网络过滤引擎,而非引言中所述的完整扩展。
7. 解析列表
此图展示了从列表中初始化每个内容拦截器所需的时间(未进行任何预先缓存,这意味着通过解析原始列表来初始化所有内部资源)。我们可以看到,只有 Brave 的速度明显较慢,而uBlock Origin、Ghostery、Adblock Plus和DuckDuckGo的表现都相当不错。
Brave浏览器解析时间过长似乎是一个已知问题,已在其GitHub存储库中进行了跟踪。
现在,如果我们移除 Brave 浏览器,会发现uBlock Origin、Ghostery、Adblock Plus和DuckDuckGo之间仍然存在差异。Ghostery速度比uBlock Origin和Adblock Plus慢的原因之一是,为了在匹配过程中实现最佳性能并最大限度地减少内存占用,它需要预先进行一些额外的工作。实际上,这并不会造成太大影响,因为这只是一次性操作,后续加载都是从缓存中进行的,速度非常快(事实上,我们甚至可以在后端完成解析,然后直接发送序列化的拦截器版本,这样就完全省去了这一步骤)。
8. 结论
本研究深入分析了目前一些最流行的内容拦截器的性能。我们尤其关注其网络过滤引擎的效率,因为这是它们执行的最消耗 CPU 资源的任务。
这项工作的动机源于 Chromium 项目Manifest V3 提案中的一项主张: “扩展程序随后会执行任意(且可能非常缓慢的)JavaScript 代码”,该主张指的是内容拦截器能够处理所有网络请求。根据我们的测量结果,我们认为这一主张并不成立,因为所有流行的内容拦截器都已经非常高效,不会给用户带来任何明显的性能下降。此外,内容拦截器的效率还在不断提高,这得益于更具创新性的方法,以及使用 WebAssembly 等技术来实现原生性能。
虽然大多数内容拦截器确实很有效,但它们并不相同,我们观察到Ghostery在所有方面都表现得同样出色或更好,通常超过其他库。
我们希望这些基准测试能够让内容拦截器开发者有机会将自己的进展与其他流行的库进行比较;随着内容拦截器效率的提高,所有用户,无论他们使用哪个扩展程序,都将从中受益。
文章来源:https://dev.to/remusao/adblockers-performance-study-5b47