HTTP 缓存

本文档描述了 **HTTP 缓存的实现**。

代码位于 /netwerk/cache2 (searchfox)

API

以下是 HTTP 缓存 v2 API 的详细描述,包括示例。本文档仅包含在 IDL 文件 注释中找不到或可能不清楚的内容。

  • 缓存 API 是 **完全线程安全的** 且 **非阻塞的**。

  • **没有 IPC 支持**。它只能在默认的 chrome 进程中访问。

  • 当没有配置文件时,新的 HTTP 缓存可以工作,但所有内容都只存储在内存中,不遵守任何特定限制。

nsICacheStorageService

  • HTTP 缓存的入口点。仅作为服务访问,完全线程安全,可脚本化。

  • nsICacheStorageService.idl (searchfox)

  • "@mozilla.org/netwerk/cache-storage-service;1"

  • 提供访问“存储”对象的方法 - 请参阅下面的 nsICacheStorage - 并进一步访问缓存条目 - 请参阅下面nsICacheEntry - 根据特定的 URL。

  • 目前我们有 3 种类型的存储,所有访问方法都返回一个nsICacheStorage 对象

    • **仅内存** (memoryCacheStorage):仅将数据存储在内存缓存中,此存储中的数据永远不会写入磁盘

    • **磁盘** (diskCacheStorage):将数据存储在磁盘上,但对于现有条目,也会查看仅内存存储;当通过特殊参数指示时,也会主要查看应用程序缓存

    注意

    **应用程序缓存** (appCacheStorage):当使用者手中有特定的 nsIApplicationCache(即组中特定的应用程序缓存版本)时,此存储将提供对该应用程序缓存中条目的读写访问;当未指定应用程序缓存时,此存储将对所有现有应用程序缓存进行操作。**这种存储已弃用,将在** bug 1694662 **中删除**

  • 该服务还提供清除整个磁盘和内存缓存内容或清除任何中间内存结构的方法

    • clear – 在它返回后,所有条目都无法再通过缓存 API 访问;该方法执行速度快,并且不会以任何方式阻塞;实际的擦除在后台进行

    • purgeFromMemory – 删除(计划删除)任何为更快访问而保存在内存中的中间缓存数据(有关Intermediate_Memory_Caching 的更多信息,请参见下文)

nsILoadContextInfo

  • 区分需要打开的存储范围。

  • nsICacheStorageService*Storage 方法的强制参数。

  • nsILoadContextInfo.idl (searchfox)

  • 它是一个辅助接口,将以下四个参数包装到一个参数中

    • **私密浏览** 布尔标志

    • **匿名加载** 布尔标志

    • **源属性** js 值

    注意

    创建 nsILoadContextInfo 对象的辅助函数

    • C++ 使用者:LoadContextInfo.h 导出的头文件中的函数

    • JS 使用者:Services.loadContextInfo,它是 nsILoadContextInfoFactory 的一个实例。

  • 使用相同一组 nsILoadContextInfo 参数创建的两个存储对象是相同的,包含相同的缓存条目。

  • 使用任何方式不同的 nsILoadContextInfo 参数创建的两个存储对象是严格且完全不同的,即使具有相同的 URI,它们中的缓存条目也不会重叠。

nsICacheStorage

  • nsICacheStorage.idl (searchfox)

  • 从对 nsICacheStorageService 上的 *Storage 方法之一的调用中获得。

  • 表示一个独特的存储区域(或范围),用于将通过 URL 映射到其中的缓存条目放入和获取。

  • 与旧缓存的相似之处:此接口在一定限制下可以被认为是 nsICacheSession 的镜像,但不太通用,也不倾向于滥用。

nsICacheEntryOpenCallback

  • nsICacheEntryOpenCallback.idl (searchfox)

  • nsICacheStorage.asyncOpenURI 的结果始终且仅发送到此接口上的回调。

  • asyncOpenURI 返回 NS_OK 时,保证会调用这些回调。

  • 注意

    当缓存条目对象已存在于内存中或作为“强制新建”(又名“打开截断”)打开时,此回调将在 asyncOpenURI 方法返回之前(即立即)调用;目前无法选择退出此功能(请参阅 bug 938186)。

nsICacheEntry

  • nsICacheEntry.idl (searchfox)

  • 通过对 nsICacheStorage.asyncOpenURI 的调用异步或伪异步地获得。

  • 提供访问缓存条目数据和元数据以进行读取或写入,或在某些情况下两者兼而有之,请参见下文。

新条目的生命周期

  • 此类条目最初为空(其中没有存储数据或元数据)。

  • onCacheEntryAvailable 中的 aNew 参数仅对新条目为 true

  • 只有一个使用者(称为“写入者”)可以使用此类条目(通过 onCacheEntryAvailable 获取)。

  • 同一缓存条目的其他并行打开者被阻塞(等待)其 onCacheEntryAvailable 的调用,直到以下情况之一发生

    • 写入者 只是丢弃了该条目:队列中的其他等待打开者再次获得该条目作为“新建”,循环重复。

      注意

      这通常适用,写入者丢弃缓存条目意味着写入缓存条目失败,并且正在再次寻找新的写入者,缓存条目保持为空(又名“新建”)。

    • 写入者 在缓存条目中存储了所有必要的元数据,并在其上调用了 metaDataReady:其他使用者现在可以获取该条目,并可以检查和可能修改元数据以及读取缓存条目的数据(如果有)。

    • 写入者 有数据(即响应有效负载)要写入缓存条目时,它**必须**在调用 metaDataReady **之前**在其上打开输出流。

  • 写入者 仍然保留缓存条目并打开并保持在其上打开输出流时,其他使用者可以在条目上打开输入流。数据将作为写入者 将数据立即写入缓存条目的输出流时可用,甚至在输出流关闭之前。这称为并发读写

并发读写

缓存支持在第一个使用者 - 写入者 仍在写入缓存条目数据时读取它。这只能用于可恢复的响应,这些响应 (bug 960902) 不需要重新验证。原因是当写入者被中断(例如,加载通道的外部取消)时,并发读取器将无法访问其余未读取的内容。

注意

可以通过保持网络加载运行并在写入通道取消后仍将其存储到缓存条目中来改进这一点。

写入者 被中断时,队列中的第一个并发读取器 对剩余的数据进行范围请求 - 并因此成为新的写入者。其余的读取器 仍在并发读取内容,因为缓存条目的输出流再次被打开并由当前的写入者 保留。

仅包含部分内容的现有条目的生命周期

  • 此类缓存条目首先在 nsICacheEntryOpenCallback.onCacheEntryCheck 回调中进行检查,其中必须检查其完整性。

  • 在这种情况下,Content-Length(或其他指示器)标头不等于缓存条目报告的数据大小。

  • 然后,使用者通过从 onCacheEntryCheck 返回 ENTRY_NEEDS_REVALIDATION 来指示缓存条目需要重新验证。

  • 从缓存的角度来看,此使用者扮演了写入者 的角色。

  • 任何其他并行使用者都将被阻塞,直到写入者 在缓存条目上调用 setValid

  • 然后,使用者负责使用网络服务器验证部分内容缓存条目并尝试加载其余数据。

  • 当服务器积极响应(在具有 206 响应代码的 HTTP 服务器的情况下)时,写入者(按此顺序)在缓存条目上打开输出流并调用 setValid 以解除其他挂起打开者的阻塞。

  • 并发读写已启用。

未通过服务器重新验证的现有条目的生命周期

  • 此类缓存条目首先在 nsICacheEntryOpenCallback.onCacheEntryCheck 回调中进行检查,使用者在此发现必须在使用前使用服务器对其进行重新验证。

  • 然后,使用者通过从 onCacheEntryCheck 返回 ENTRY_NEEDS_REVALIDATION 来指示缓存条目需要重新验证。

  • 从缓存的角度来看,此使用者扮演了写入者 的角色。

  • 任何其他并行使用者都将被阻塞,直到写入者 在缓存条目上调用 setValid

  • 然后,使用者负责使用网络服务器验证部分内容缓存条目。

  • 服务器以 200 响应进行回复,这意味着缓存内容不再有效,必须从网络加载新版本。

  • 然后,写入器在缓存条目上调用 recreate。这将返回一个新的空条目以写入元数据和数据,写入器将它的缓存条目替换为此新条目,并将其视为新条目进行处理。

  • 然后,写入器(按此顺序)填充缓存条目的必要元数据,在其上打开输出流,并在其上调用 metaDataReady

  • 如果有任何其他挂起的打开器,现在将向其提供此新条目以将其检查和读取为现有条目。

添加新的存储

如果需要添加一个新的独立存储,而当前的范围模型不足以满足需求 - 请使用以下两种方法之一

  1. [首选]nsICacheStorageService 上添加一个新的 <Your>Storage 方法,如果需要,还可以提供任何参数以更详细地指定存储范围。实现只需要增强上下文密钥生成和解析代码,并增强当前 - 或在需要时创建新的 - nsICacheStorage 实现以将任何其他信息传递到缓存服务。

  2. [推荐]nsILoadContextInfo 添加一个新参数;在此处要小心,因为上下文上的一些参数在加载时可能未知,这可能导致上下文间数据泄漏或实现问题。向 nsILoadContextInfo 添加更多区别也会影响所有现有存储,这可能并不总是理想的。

有关更多信息,请参阅上下文键控详细信息。

线程

缓存 API 完全线程安全。

缓存使用单个后台线程,其中发生任何 IO 操作(如打开、读取、写入和擦除)。此外,内存池管理、逐出、访问循环也发生在此线程上。

该线程支持多个优先级级别。调度到较低编号级别的执行速度快于调度到较高编号级别的执行速度;此外,较低级别的任何循环都会让位于较高级别,以便 1000 个文件的计划删除不会阻止打开缓存条目。

  1. OPEN_PRIORITY:除了打开优先级缓存文件外,文件删除也发生在此处以防止竞争条件

  2. READ_PRIORITY:顶级文档和头部阻塞脚本缓存文件作为第一个打开和读取

  3. OPEN

  4. READ:在此处打开和读取任何普通优先级内容,例如图像

  5. WRITE:写入作为最后处理,我们同时将数据缓存到内存中

  6. MANAGEMENT:内存池和 CacheEntry 后台操作的级别

  7. CLOSE:文件关闭级别

  8. INDEX:索引在此处重建

  9. EVICT:在此处逐出超过磁盘空间使用限制的文件

注意:逐出的特殊情况 - 当在 IO 线程上计划逐出时,首先将 OPEN 级别上所有挂起的操作合并到 OPEN_PRIORITY 级别。然后,将逐出准备操作(即内部 IO 状态的清除)置于 OPEN_PRIORITY 级别的末尾。所有这些都是原子操作。

存储和条目范围

用于映射存储范围的范围密钥字符串基于 nsILoadContextInfo 的参数。表单如下(目前在 bug 968593 中挂起)

a,b,i1009,p,
  • 正则表达式:(.([-,]+)?,)*

  • 第一个字母是标识符,标识符按字母顺序排序,并始终以“,” 结尾。

  • a - 存在时,范围属于匿名加载

  • b - 存在时,范围属于浏览器元素加载

  • i - 存在时,必须具有表示范围所属的应用程序 ID 的十进制整数值,否则没有应用程序(应用程序 ID 被视为 0

  • p - 存在时,范围属于隐私浏览加载,这永远不会持久化

CacheStorageService 通过范围密钥维护一个全局哈希表。此全局哈希表中的元素是缓存条目的哈希表。缓存条目通过传递给 nsICacheStorage.asyncOpenURI 的增强 ID 和 URI 的连接进行映射。因此,当查找条目时,首先使用范围密钥搜索全局哈希表。找到一个条目哈希表。然后,使用 <enhance-id:><uri> 字符串搜索此条目哈希表。此哈希表中的元素是 CacheEntry 类,请参见下文。

哈希表对 CacheEntry 对象保持强引用。从内存中删除 CacheEntry 对象的唯一方法是耗尽 Intermediate_Memory_Caching 的内存限制,这将触发一个清除内存中已过期和最少使用的条目的后台进程。另一种方法是直接调用 nsICacheStorageService.purge 方法。该方法也会在 "memory-pressure" 指示时自动调用。

对哈希表的访问受全局锁保护。我们还以线程安全的方式计算保持对每个条目的引用的使用者的数量。打开回调实际上并没有直接向使用者提供 CacheEntry 对象,而是提供了一个小的包装器类,该类管理其缓存条目上的“使用者引用计数器”。这两种机制都确保线程安全访问,并且无法为单个 <scope+enhanceID+URL> 密钥拥有多个 CacheEntry 实例。

CacheStorage(实现 nsICacheStorage 接口)将所有调用转发到 CacheStorageService 的内部方法,并将自身作为参数传递。然后,CacheStorageService 使用存储的 nsILoadContextInfo 生成范围密钥。注意:CacheStorage 保留传递给 nsICacheStorageService 上的 *Storage 方法的 nsILoadContextInfo 的线程安全副本。

调用打开回调

CacheEntry(实现 nsICacheEntry 接口)负责管理缓存条目内部状态,并正确调用 onCacheEntryCheckonCacheEntryAvaiable 回调到 nsICacheStorage.asyncOpenURI 的所有调用者。

  • 保持所有打开器的 FIFO。

  • 保持其内部状态,如 NOTLOADED、LOADING、EMPTY、WRITING、READY、REVALIDATING。

  • 保持保持对它的引用的使用者的数量。

  • 引用一个 CacheFile 对象,该对象保存实际数据和元数据,并在被告知时将其持久化到磁盘。

打开器 FIFO 是 CacheEntry::Callback 对象的数组。CacheEntry::Callback 对打开器以及打开标志保持强引用。nsICacheStorage.asyncOpenURI 转发到 CacheEntry::AsyncOpen 并触发以下伪代码

CacheStorage::AsyncOpenURI - API 入口点

  • 全局原子

    • CacheStorageService 哈希表中查找给定的 CacheEntry

    • 如果未找到:创建一个新的,将其添加到正确的哈希表,并将它的状态设置为 NOTLOADED

    • 使用者引用++

  • 调用 CacheEntry::AsyncOpen

  • 使用者引用--

CacheEntry::AsyncOpen(条目原子)

  • 打开器被添加到 FIFO,使用者引用++(在打开器从 FIFO 中移除后被丢弃)

  • state == NOTLOADED

    • state = LOADING

    • 当使用 OPEN_TRUNCATE 标志时

      • CacheFile 被创建为“新”,state = EMPTY

    • 否则

      • CacheFile 被创建并在其上启动加载

      • 现在预计 CacheEntry::OnFileReady 通知

  • state == LOADING:什么也不做并退出

  • 调用 CacheEntry::InvokeCallbacks

CacheEntry::InvokeCallbacks(条目原子)

  • 在以下情况下调用

    • 通过 AsyncOpen 调用将新的打开器添加到 FIFO

    • CacheFile 打开的异步结果 CacheEntry::OnFileReady>

    • 写入器丢弃条目 - CacheEntry::OnHandleClosed

    • 条目的输出流打开关闭

    • 已在条目上调用 metaDataReadysetValid

    • 条目已被删除

  • state == EMPTY

    • 在 OPER_READONLY 标志使用:使用 null 作为缓存条目调用 onCacheEntryAvailable

    • 否则

      • state = WRITING

      • 打开器从 FIFO 中移除并被记住为当前“写入器

      • 使用 aNew = true 调用 onCacheEntryAvailable,并为写入器调用此条目(在调用方线程上)

  • state == READY

    • 在 FIFO 中第一个打开器上调用 onCacheEntryCheck,如果需要,在调用方线程上调用

    • result == RECHECK_AFTER_WRITE_FINISHED

      • 打开器保留在 FIFO 中,并带有 RecheckAfterWrite 标志

      • 此类打开器会被跳过,直到条目上的输出流关闭,然后在它们上面重新调用 onCacheEntryCheck

      • 注意:当滥用 RECHECK_AFTER_WRITE_FINISHED 时,这里存在无限循环的可能性

    • result == ENTRY_NEEDS_REVALIDATION

      • state = REVALIDATING,这将阻止调用任何回调,直到调用 CacheEntry::SetValid

      • 继续执行 ENTRY_WANTED 状态(就在下面)

    • result == ENTRY_WANTED

      • 使用者引用++(在使用者释放条目时被丢弃)

      • 使用 aNew = false 和条目调用 onCacheEntryAvailable

      • 打开器从 FIFO 中移除

    • result == ENTRY_NOT_WANTED

      • 使用 null 作为条目调用 onCacheEntryAvailable

      • 打开器从 FIFO 中移除

  • state == WRITING 或 REVALIDATING

    • 什么也不做并退出

  • 状态的任何其他值在此处都是意外的(断言失败)

  • 在 FIFO 中存在打开器时循环此过程

CacheEntry::OnFileReady(条目原子)

  • load 结果 == 失败或磁盘上未找到文件(是新的):state = EMPTY

  • 否则:state = READY,因为已找到缓存文件并且可以使用,其中包含条目的元数据和数据

  • 调用 CacheEntry::InvokeCallbacks

CacheEntry::OnHandleClosed(条目原子)

  • 当任何使用者丢弃缓存条目时调用

  • 如果句柄不是提供给当前写入器的句柄,则退出

  • state == WRITING:写入器未能调用条目上的 metaDataReady - state = EMPTY

  • state == REVALIDATING:写入器重新验证过程失败,并且未能调用条目上的 setValid - state = READY

  • 调用 CacheEntry::InvokeCallbacks

所有使用者释放引用

  • 当在超过 内存池 限制时发现已过期或最少使用时,现在可以将条目从内存中清除(删除)

  • 当这是一个磁盘缓存条目时,它的缓存数据块将从内存中释放,并且仅保留元数据。

中间内存缓存

常用元数据的中间内存缓存(也称为磁盘缓存内存池)。

对于磁盘缓存条目,我们将一些最新和最常用的缓存条目的元数据保留在内存中,以便立即进行零线程循环打开。此元数据内存池的默认大小仅为 250kB,并由新的 browser.cache.disk.metadata_memory_limit 首选项控制。当超过限制时,我们首先清除(丢弃)已过期的条目,然后清除最少使用的条目以再次释放内存。

只有已加载并填充数据且“使用者引用 == 0”(bug 942835)的 CacheEntry 对象可以被清除。

“最少使用”的条目通过我们每次访问其条目时重新计算的最低 frecency 值来识别。衰减时间由 browser.cache.frecency_half_life_hours 首选项控制,默认为 6 小时。最佳衰减时间将基于 实验 结果。

内存池由两个列表(强引用有序数组)的 CacheEntry 对象表示

  1. 按过期时间排序(默认为 0xFFFFFFFF)

  2. 按 frecency 排序(默认为 0)

我们有两个这样的池,一个用于实际表示内存缓存的内存条目,另一个用于磁盘缓存条目,我们仅为其保留元数据。每个池都有不同的限制检查 - 内存缓存池受 browser.cache.memory.capacity 控制,磁盘条目池已在上面描述。只能在缓存后台线程上访问和修改池。