1. 简介
performance.measureUserAgentSpecificMemory()
API,用于估算 web 应用(包括所有 iframe 和 worker)的内存使用情况。这个新 API 旨在用于收集线上内存使用数据,主要用例包括:
-
在 Web 应用新版本发布过程中监测回归,捕捉新的内存泄漏。
-
A/B 测试新特性,评估其内存影响。
-
将内存使用与用户指标相关联,以了解内存使用的整体影响。
1.1. 示例
一次 performance.measureUserAgentSpecificMemory()
调用将返回一个 Promise
,并启动异步测量当前页面分配的内存。
async function run() { const result= await performance. measureUserAgentSpecificMemory(); console. log( result); } run();
对于没有 iframe 和 worker 的简单页面,结果可能如下所示:
此处所有内存都归属于主页面。breakdown 列表中的{ bytes: 1000000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], }
bytes: 0 条目旨在鼓励通用方式处理结果,而不是写死特定条目。只要列表不为空,该条目会被插入到一个随机位置。
其它可能的有效结果:
此时实现只提供总内存使用量。{ bytes: 1000000 , breakdown: [], }
这种情况下实现没有按类型分拆内存。{ bytes: 1000000 , breakdown: [ { bytes: 0 , attribution: [], types: [], }, { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [], }, ], }
对于嵌入了同源 iframe 的页面,结果可能会将部分内存归因到该 iframe,并提供识别该 iframe 的诊断信息:
< html > < body > < iframe id = "example-id" src = "redirect.html?target=iframe.html" ></ iframe > </ body > </ html >
注意 iframe 的{ bytes: 1500000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "DOM" , "JS" ], }, { bytes: 0 , attribution: [], types: [], }, { bytes: 500000 , attribution: [ { url: "https://example.com/iframe.html" container: { id: "example-id" , src: "redirect.html?target=iframe.html" , }, scope: "Window" , } ], types: [ "JS" , "DOM" ], }, ], }
url 和 container.src 字段不同。前者反映当前 iframe 的
location.href,后者是 iframe 元素的 src 属性值。
有时无法有意义地将 iframe 内存和页面内存区分开,允许实现将部分或所有 iframe 内存与主页面内存合并:
{ bytes: 1500000 , breakdown: [ { bytes: 1500000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, { url: "https://example.com/iframe.html" , container: { id: "example-id" , src: "redirect.html?target=iframe.html" , }, scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], };
有些实现可能会将 worker 和页面内存合并统计。如果 worker 是被某个 iframe 创建的,那么该 worker 的归因条目会包含一个与 iframe 元素对应的{ bytes: 1800000 , breakdown: [ { bytes: 0 , attribution: [], types: [], }, { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 800000 , attribution: [ { url: "https://example.com/worker.js" , scope: "DedicatedWorkerGlobalScope" , }, ], types: [ "JS" ], }, ], };
container 字段。
共享 worker 和 service worker 的内存不包含在结果中。
假设有如下结构的页面:
example一个跨域 iframe 嵌套了其他 iframe 并创建了 worker。 所有这些资源的内存都归属于第一个 iframe。. com( 1000000 bytes) | *-- foo. com/ iframe1( 500000 bytes) | *-- foo. com/ iframe2( 200000 bytes) | *-- bar. com/ iframe2( 300000 bytes) | *-- foo. com/ worker. js( 400000 bytes)
< html > < body > < iframe id = "example-id" src = "https://foo.com/iframe1" ></ iframe > </ body > </ html >
注意跨域 iframe 项的{ bytes: 2400000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, { bytes: 1400000 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "DOM" , "JS" ], }, ], }
url 和 scope 字段有特殊取值,表示信息不可用。
如果实现将跨域 iframe 加载在其它地址空间,则无法测量其内存。这类 iframe 的 breakdown 条目会有 bytes: 0,表示未统计:
{ bytes: 100000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], }
location.href。
example. com( 1000000 bytes) | *-- foo. com/ iframe1( 500000 bytes) | *-- example. com/ iframe2( 200000 bytes)
< html > < body > < iframe id = "example-id" src = "https://foo.com/iframe1" ></ iframe > </ body > </ html >
{ bytes: 1700000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "DOM" , "JS" ], }, { bytes: 0 , attribution: [], types: [], }, { bytes: 500000 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "DOM" , "JS" ], }, { bytes: 200000 , attribution: [ { url: "https://example.com/iframe2" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, ], }
如果实现不统计跨域 iframe 的内存,则结果可能如下:
{ bytes: 1200000 , breakdown: [ { bytes: 1000000 , attribution: [ { url: "https://example.com" , scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [ { url: "cross-origin-url" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "cross-origin-aggregated" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 200000 , attribution: [ { url: "https://example.com/iframe2" , container: { id: "example-id" , src: "https://foo.com/iframe1" , }, scope: "Window" , }, ], types: [ "JS" , "DOM" ], }, { bytes: 0 , attribution: [], types: [], }, ], }
2. 数据模型
2.1. 内存测量结果
performance.measureUserAgentSpecificMemory()
函数返回一个 Promise
,该 Promise 解析后为 MemoryMeasurement
字典的实例:
dictionary {MemoryMeasurement unsigned long long ;bytes sequence <MemoryBreakdownEntry >; };breakdown
-
measurement .bytes -
表示总内存使用量的数字。
-
measurement .breakdown -
一个数组,用于分割总的
bytes,并提供归因和类型信息。
dictionary {MemoryBreakdownEntry unsigned long long ;bytes sequence <MemoryAttribution >;attribution sequence <DOMString >; };types
-
breakdown .bytes -
该条目所描述内存的大小。
-
breakdown .attribution -
一个由使用这些内存的JavaScript 域的 URL 和/或容器元素组成的数组。
-
breakdown .types -
一个与该内存相关的实现自定义内存类型的数组。
dictionary {MemoryAttribution USVString ;url MemoryAttributionContainer ;container DOMString ; };scope
-
attribution .url -
如果此归因对应同源的JavaScript 域,则此字段为该域的 URL。 否则,归因为一个或多个跨域的JavaScript 域,此字段为特殊值:
"cross-origin-url"。 -
attribution .container -
描述(可能是间接)包含该JavaScript 域的 DOM 元素。 如果归因的是同源顶层域,该属性可能不存在。注意,跨域域由于跨源隔离,不能是顶层。
-
attribution .scope -
描述同源JavaScript 域的类型:
"Window", "DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope",或在跨域场景下为"cross-origin-aggregated"。
dictionary {MemoryAttributionContainer DOMString ;id USVString ; };src
2.2. 中间内存测量
本规范假设存在一个实现自定义算法,用于测量给定代理集群集合在当前代理集群地址空间内的内存使用情况。 该算法的输出结果叫做中间内存测量值,它是集合,集合内是中间内存分拆条目。
为保证地址空间隔离的安全性,任何代表当前地址空间之外内存的中间内存分拆条目,都必须将其 bytes 设置为 0。
为减小指纹识别风险,结果仅应包括与给定代理集群集合分配或使用的 web 平台对象相关的内存。 例如,这不包括 user-agent 特有扩展的内存及空页面的基线内存。 内存必须按地址空间统计,以排除诸如内存压缩、延迟分配等平台相关内存优化的影响。
- bytes
-
该中间内存分拆条目描述的内存大小,若为当前地址空间外,则为 0。
- realms
-
一个集合,内含归因该内存的JavaScript 域。
- types
本规范定义的算法说明如何将中间内存测量值转换为 MemoryMeasurement
实例。
2.3. 内存归因令牌
嵌入的JavaScript 域与其容器元素之间的链接是临时的,不能保证永久存在。 例如,容器元素导航到其它文档或从 DOM 树中移除,都会断开该链接。
内存归因令牌 提供了一种从JavaScript 域找到其容器元素的方法。 它是一个结构体, 包含如下项:
- container
-
一个
MemoryAttributionContainer实例。 - cross-origin aggregated flag
-
一个布尔标志,表示此令牌是否用于跨域JavaScript 域内存归集。
该令牌会在 WindowOrWorkerGlobalScope
构造时的新内部字段中保存,始终可用于内存上报。
3. 处理模型
3.1.
Performance
接口扩展
partial interface Performance { [Exposed =(Window ,ServiceWorker ,SharedWorker ),CrossOriginIsolated ]Promise <MemoryMeasurement >measureUserAgentSpecificMemory (); };
-
performance .measureUserAgentSpecificMemory() -
一个执行异步内存测量的方法。该方法结果的详细说明见 § 2.1 内存测量结果。
3.2. 顶层算法
measureUserAgentSpecificMemory()
方法的执行步骤如下:
-
断言:当前 Realm 的 settings 对象的 跨域隔离能力为 true。
-
如果 memory measurement allowed predicate 以 当前 Realm 为参数结果为 false,则:
-
返回被拒绝的 Promise,错误类型为 "
SecurityError"DOMException。
-
-
令 agent clusters 为 getting all agent clusters 以 当前 Realm 调用的结果。
-
令 promise 为新的
Promise。 -
以 当前代理集群、agent clusters 和 promise,启动异步实现自定义内存测量。
-
返回 promise。
-
令 global object 为 realm 的全局对象。
-
如果 global object 是
SharedWorkerGlobalScope, 返回 true。 -
如果 global object 是
ServiceWorkerGlobalScope, 返回 true。 -
如果 global object 是
Window,那么:-
令 settings object 为 realm 的 settings object。
-
-
返回 false。
3.3. 将中间内存测量值转换为结果
-
令 bytes 为 0。
-
遍历每个中间内存分拆条目 intermediate entry 于 intermediate measurement 中:
-
将 bytes 设为 bytes 加上 intermediate entry 的 bytes。
-
-
令 breakdown 为一个新的列表。
-
添加到 breakdown 一个新的
MemoryBreakdownEntry,其:-
bytes为 0, -
attribution为 « », -
types为 « »。
-
-
遍历每个中间内存分拆条目 intermediate entry 于 intermediate measurement 中:
-
令 breakdown entry 为以 intermediate entry 为参数调用创建新的 memory breakdown entry 的结果。
-
添加 breakdown entry 至 breakdown。
-
-
将 breakdown 中各项随机排序。
-
返回一个新的
MemoryMeasurement,其:
-
令 attribution 为新的列表。
-
遍历每个 JavaScript 域 realm 于 intermediate entry 的 realms 中:
-
令 attribution entry 为以 realm 为参数调用 创建新的 memory attribution 的结果。
-
添加 attribution entry 到 attribution。
-
-
令 types 为 intermediate entry 的 types。
-
将 types 中各项随机排序。
-
返回一个新的
MemoryBreakdownEntry,其:-
attribution为 attribution, -
types为 types。
-
令 token 为 realm 的 全局对象的 memory attribution token。
-
如果 token 的 跨域聚合标志为 true,则:
-
令 scope name 为 realm 的 identifier(接口类型名)。
-
返回一个新的
MemoryAttribution,其:
3.4. 创建或获取内存归因令牌
HTMLElement
container element,以及 memory attribution token parent token:
-
若 container element 为 null,则:
-
断言:parent origin 为 null。
-
断言:parent token 为 null。
-
断言:origin 等于 parent origin
-
返回一个新的memory attribution token,其:
-
container 为 null,
-
cross-origin aggregated flag 为 false。
-
-
-
若 parent origin 不等于 top-level origin,则:
-
返回 parent token。
-
-
令 container 为以 container element 为参数调用提取容器元素属性的结果。
-
若 origin 等于 top-level origin,则:
-
返回新的memory attribution token,其:
-
container 为 container,
-
cross-origin aggregated flag 为 false。
-
-
-
返回一个新的 memory attribution token,其:
-
container 为 container,
-
cross-origin aggregated flag 为 true。
-
WorkerGlobalScope
worker global scope,以及 environment settings object outside settings:
-
如果 worker global scope 是
DedicatedWorkerGlobalScope, 则返回 outside settings 的 global object 的 memory attribution token。 -
断言: worker global scope 为
SharedWorkerGlobalScope或ServiceWorkerGlobalScope。 -
返回一个新的 memory attribution token,其:
-
container 为 null,
-
cross-origin aggregated flag 为 false。
-
HTMLElement
container element:
-
以 container element 的本地名为分支:
- "iframe"
-
返回新的
MemoryAttributionContainer,其: - "frame"
-
返回新的
MemoryAttributionContainer,其: - "object"
-
返回新的
MemoryAttributionContainer,其:
4. 与现有规范的整合
4.1. 对 WindowOrWorkerGlobalScope
的扩展
在 WindowOrWorkerGlobalScope
中新增一个内部字段:
- 一个 内存归因令牌
-
用于上报此环境内存使用的 内存归因令牌。
4.2. 对现有算法的扩展
运行 worker 的算法,在第 6 步里会为新建全局对象设置 内存归因令牌 字段:
令 realm execution context 为基于 agent 和下列自定义项创建的新 JavaScript realm:
...
将全局对象的 内存归因令牌 设置为以全局对象和 outside settings 调用 获取 worker 内存归因令牌 的结果。
创建与初始化 Document 对象 的算法会为新创建的全局对象设置 内存归因令牌 字段:
否则:
令 token 为一个空的 内存归因令牌。
如果 browsingContext 不是顶级 browsing context,则:
将 token 设为以 origin、parentEnvironment 的 origin、topLevelOrigin、browsingContext 的 container、parentToken 调用 获取 window 内存归因令牌 的结果。
否则,将 token 设为以 origin、null topLevelOrigin、null、null 调用 获取 window 内存归因令牌 的结果。
令 window global scope 为 realm execution context 的 Realm 组件的 全局对象。
将 window global scope 的 内存归因令牌 设为 token。
创建新的浏览上下文 的算法会为新建全局对象设置 内存归因令牌 字段:
令 token 为一个空令牌。
如果 embedder 为 null,则把 token 设为以 origin、null、topLevelOrigin、null、null 调用 获取 window 内存归因令牌 的结果。
否则,将 token 设为以 origin、embedder 的 相关 settings object 的 origin、topLevelOrigin、embedder、embedder 的 相关全局对象 的 内存归因令牌 调用 获取 window 内存归因令牌 的结果。
令 window global scope 为 realm execution context 的 Realm 组件的 全局对象。
将 window global scope 的 内存归因令牌 设为 token。
5. 隐私与安全
5.1. 跨域信息泄露
结果中出现的 URL 及其他字符串值保证为调用 API 的源已知。
仅有跨域暴露的信息是 memoryMeasurement.bytes
和 memoryBreakdownEntry.bytes
中的大小信息。
该 API 依赖 跨源隔离
机制以缓解跨域大小信息泄漏。
具体来说,API 依赖一个不变式:当前地址空间的所有资源都已选择允许被嵌入且能被嵌入源读取。
API 不暴露在不同地址空间加载的跨域资源的大小。
5.2. 指纹识别
API 的结果只依赖于页面本身分配的对象,不计入与空页面基线内存等无关内存。 这意味着同一 user agent 二进制在两台不同设备上访问同一个页面应产生相同的结果。
网页可以推断如下 user agent 的信息:
-
user agent 的位数(32位 vs 64位)。
-
部分 user agent 版本信息。
类似信息也可以从现有 API 获取(navigator.userAgent,
navigator.platform)。
user agent 的位数还可以通过测量 32 位和 64 位运算的执行时间推断。
目前该 API 仅可被顶级域名访问。 未来顶级域可通过权限策略(Permissions Policy)将 API 委托给其它域。 无论哪种情况,跨域 iframe 默认均无法访问该 API。
6. 致谢
感谢 Domenic Denicola 和 Shu-yu Guo 对 API 设计的贡献与本规范的审阅。同时感谢 Adam Giacobbe、Anne van Kesteren、Artur Janc、Boris Zbarsky、Chris Hamilton、Chris Palmer、 Daniel Vogelheim、Dominik Inführ、Hannes Payer、Joe Mason、Kentaro Hara、L. David Baron、Mathias Bynens、 Matthew Bolohan、Michael Lippautz、Mike West、Neil Mckay、Olga Belomestnykh、Per Parker、Philipp Weis 和 Yoav Weiss 的反馈与贡献。