测量内存 API

社区组报告草案,

该版本:
https://wicg.github.io/performance-measure-memory/
编辑者:
(Google)
参与方式:
GitHub WICG/performance-measure-memory (新议题, 开放议题)

摘要

本规范定义了一个使 Web 应用能够测量其内存使用情况的 API。

本文档状态

本规范由 Web 平台孵化社区组 发布。 它不是 W3C 标准,也不属于 W3C 标准轨道。 请注意,根据 W3C 社区贡献者许可协议(CLA),存在有限的选择退出权,且有其它适用条件。 了解更多 W3C 社区和业务组 信息。

1. 简介

许多算法和数据结构中都固有存在内存与性能的权衡。当前,Web 开发者有多种方式度量时间信息,但没有标准方式来测量内存使用。本规范定义了 performance.measureUserAgentSpecificMemory() API,用于估算 web 应用(包括所有 iframe 和 worker)的内存使用情况。这个新 API 旨在用于收集线上内存使用数据,主要用例包括:

1.1. 示例

一次 performance.measureUserAgentSpecificMemory() 调用将返回一个 Promise ,并启动异步测量当前页面分配的内存。

async function run() {
  const result = await performance.measureUserAgentSpecificMemory();
  console.log(result);
}
run();

对于没有 iframe 和 worker 的简单页面,结果可能如下所示:

{
  bytes: 1000000,
  breakdown: [
    {
      bytes: 1000000,
      attribution: [
        {
          url: "https://example.com",
          scope: "Window",
        },
      ],
      types: ["JS", "DOM"],
    },
    {
      bytes: 0,
      attribution: [],
      types: [],
    },
  ],
}
此处所有内存都归属于主页面。breakdown 列表中的 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>
{
  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"],
    },
  ],
}
注意 iframe 的 urlcontainer.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: [],
    },
  ],
};
对于创建了 web worker 的页面,结果会包含该 worker 的 URL。
{
  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"],
    },
  ],
};
有些实现可能会将 worker 和页面内存合并统计。如果 worker 是被某个 iframe 创建的,那么该 worker 的归因条目会包含一个与 iframe 元素对应的 container 字段。

共享 worker 和 service worker 的内存不包含在结果中。

如需获取 shared/service worker 的内存使用,需要在该 worker 的上下文中调用 performance.measureUserAgentSpecificMemory() 。 结果可能如下:
{
  bytes: 1000000,
  breakdown: [
    {
      bytes: 1000000,
      attribution: [
        {
          url: "https://example.com/service-worker.js",
          scope: "ServiceWorkerGlobalScope",
        },
      ],
      types: ["JS"],
    },
    {
      bytes: 0,
      attribution: [],
      types: [],
    },
  ],
}
如果页面嵌入了跨域 iframe, 那么不会暴露该 iframe 的 URL,以避免信息泄露。结果中仅包含已知的容器元素。 并且,如果跨域 iframe 内还嵌套了其它跨域 iframe 或创建了 worker, 这些内容的内存都会归属于最顶层的跨域 iframe。

假设有如下结构的页面:

example.com (1000000 bytes)
  |
  *--foo.com/iframe1 (500000 bytes)
       |
       *--foo.com/iframe2 (200000 bytes)
       |
       *--bar.com/iframe2 (300000 bytes)
       |
       *--foo.com/worker.js (400000 bytes)
一个跨域 iframe 嵌套了其他 iframe 并创建了 worker。 所有这些资源的内存都归属于第一个 iframe。
  <html>
    <body>
      <iframe id="example-id" src="https://foo.com/iframe1"></iframe>
    </body>
  </html>
{
  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"],
    },
  ],
}
注意跨域 iframe 项的 urlscope 字段有特殊取值,表示信息不可用。

如果实现将跨域 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: [],
    },
  ],
}
如果一个跨域 iframe 嵌套了与主页面同源的 iframe,则该同源 iframe 会在结果中呈现。 注意不存在信息泄露风险,因为主页面可以找到并读取同源 iframe 的 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;
};
container . id

容器元素的 id 属性。

container . src

容器元素的 src 属性。如果容器元素是 object 元素, 则此字段保存 data 属性的值。

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() 方法的执行步骤如下:
  1. 断言当前 Realmsettings 对象跨域隔离能力为 true。

  2. 如果 memory measurement allowed predicate当前 Realm 为参数结果为 false,则:

    1. 返回被拒绝的 Promise,错误类型为 "SecurityError" DOMException

  3. 当前代理集群当前 Realmagent代理集群

  4. agent clustersgetting all agent clusters当前 Realm 调用的结果。

  5. promise 为新的 Promise

  6. 当前代理集群agent clusterspromise,启动异步实现自定义内存测量

  7. 返回 promise

计算memory measurement allowed predicate(给定 JavaScript 域 realm)的步骤如下:
  1. global objectrealm全局对象

  2. 如果 global objectSharedWorkerGlobalScope, 返回 true。

  3. 如果 global objectServiceWorkerGlobalScope, 返回 true。

  4. 如果 global objectWindow ,那么:

    1. settings objectrealmsettings object

    2. 如果 settings objectorigin顶级 origin 相同,则返回 true。

  5. 返回 false。

get all agent clusters(给定 JavaScript 域 realm)的步骤如下:
  1. 如果 realm全局对象Window, 则:

    1. group 为包含 realm全局对象浏览上下文浏览上下文组

    2. 返回 groupagent cluster map所有值的结果。

  2. 返回 « realmagent代理集群 »。

实现自定义内存测量(参数为 代理集群 当前代理集群,一组代理集群 agent clusters,以及 Promise promise),并并行执行如下步骤:
  1. intermediate memory measurement 为使用 当前代理集群agent clusters 执行实现自定义中间内存测量的结果。

  2. 在 TODO 任务源上,排入全局任务队列,以 promise相关全局对象为上下文,resolve promise 的值为以 intermediate memory measurement 作为参数调用创建新内存测量的结果。

3.3. 将中间内存测量值转换为结果

若要创建新的内存测量,给定一个中间内存测量值intermediate measurement
  1. bytes 为 0。

  2. 遍历每个中间内存分拆条目 intermediate entryintermediate measurement 中:

    1. bytes 设为 bytes 加上 intermediate entrybytes

  3. breakdown 为一个新的列表

  4. 添加breakdown 一个新的 MemoryBreakdownEntry ,其:

  5. 遍历每个中间内存分拆条目 intermediate entryintermediate measurement 中:

    1. breakdown entry 为以 intermediate entry 为参数调用创建新的 memory breakdown entry 的结果。

    2. 添加 breakdown entrybreakdown

  6. breakdown 中各项随机排序。

  7. 返回一个新的 MemoryMeasurement ,其:

若要创建新的 memory breakdown entry,给定一个中间内存分拆条目 intermediate entry
  1. attribution 为新的列表

  2. 遍历每个 JavaScript 域 realmintermediate entryrealms 中:

    1. attribution entry 为以 realm 为参数调用 创建新的 memory attribution 的结果。

    2. 添加 attribution entryattribution

  3. typesintermediate entrytypes

  4. types 中各项随机排序。

  5. 返回一个新的 MemoryBreakdownEntry ,其:

若要创建新的 memory attribution,给定一个JavaScript 域 realm
  1. tokenrealm全局对象memory attribution token

  2. 如果 token跨域聚合标志为 true,则:

    1. 返回一个新的 MemoryAttribution ,其:

  3. scope namerealmidentifier(接口类型名)。

  4. 返回一个新的 MemoryAttribution ,其:

3.4. 创建或获取内存归因令牌

若要获取 window 内存归因令牌,给定一个 origin originorigin parent originorigin top-level originHTMLElement container element,以及 memory attribution token parent token
  1. container element 为 null,则:

    1. 断言parent origin 为 null。

    2. 断言parent token 为 null。

    3. 断言origin 等于 parent origin

    4. 返回一个新的memory attribution token,其:

  2. parent origin 不等于 top-level origin,则:

    1. 返回 parent token

  3. container 为以 container element 为参数调用提取容器元素属性的结果。

  4. origin 等于 top-level origin,则:

    1. 返回新的memory attribution token,其:

  5. 返回一个新的 memory attribution token,其:

若要获取 worker 内存归因令牌,给定 WorkerGlobalScope worker global scope,以及 environment settings object outside settings
  1. 如果 worker global scopeDedicatedWorkerGlobalScope, 则返回 outside settingsglobal objectmemory attribution token

  2. 断言: worker global scopeSharedWorkerGlobalScopeServiceWorkerGlobalScope

  3. 返回一个新的 memory attribution token,其:

若要提取容器元素属性,给定 HTMLElement container element
  1. container element本地名为分支:

    "iframe"

    返回新的 MemoryAttributionContainer ,其:

    • idcontainer elementid 属性,

    • srccontainer elementsrc 属性,

    "frame"

    返回新的 MemoryAttributionContainer ,其:

    • idcontainer elementid 属性,

    • srccontainer elementsrc 属性,

    "object"

    返回新的 MemoryAttributionContainer ,其:

    • idcontainer elementid 属性,

    • srccontainer elementdata 属性,

4. 与现有规范的整合

4.1. WindowOrWorkerGlobalScope 的扩展

WindowOrWorkerGlobalScope 中新增一个内部字段:
一个 内存归因令牌

用于上报此环境内存使用的 内存归因令牌

4.2. 对现有算法的扩展

运行 worker 的算法,在第 6 步里会为新建全局对象设置 内存归因令牌 字段:

  1. 令 realm execution context 为基于 agent 和下列自定义项创建的新 JavaScript realm:

创建与初始化 Document 对象 的算法会为新创建的全局对象设置 内存归因令牌 字段:

  1. 否则:

    1. token 为一个空的 内存归因令牌

    2. 如果 browsingContext 不是顶级 browsing context,则:

      1. parentTokenparentEnvironment全局对象内存归因令牌

      2. token 设为以 originparentEnvironment 的 origin、topLevelOriginbrowsingContext 的 container、parentToken 调用 获取 window 内存归因令牌 的结果。

    3. 否则,将 token 设为以 origin、null topLevelOrigin、null、null 调用 获取 window 内存归因令牌 的结果。

    4. window global scoperealm execution context 的 Realm 组件的 全局对象

    5. window global scope内存归因令牌 设为 token

创建新的浏览上下文 的算法会为新建全局对象设置 内存归因令牌 字段:

  1. token 为一个空令牌。

  2. 如果 embedder 为 null,则把 token 设为以 origin、null、topLevelOrigin、null、null 调用 获取 window 内存归因令牌 的结果。

  3. 否则,将 token 设为以 originembedder相关 settings object 的 origin、topLevelOriginembedderembedder相关全局对象内存归因令牌 调用 获取 window 内存归因令牌 的结果。

  4. window global scoperealm execution context 的 Realm 组件的 全局对象

  5. window global scope内存归因令牌 设为 token

5. 隐私与安全

5.1. 跨域信息泄露

结果中出现的 URL 及其他字符串值保证为调用 API 的源已知。

仅有跨域暴露的信息是 memoryMeasurement.bytesmemoryBreakdownEntry.bytes 中的大小信息。 该 API 依赖 跨源隔离 机制以缓解跨域大小信息泄漏。 具体来说,API 依赖一个不变式:当前地址空间的所有资源都已选择允许被嵌入且能被嵌入源读取。 API 不暴露在不同地址空间加载的跨域资源的大小。

5.2. 指纹识别

API 的结果只依赖于页面本身分配的对象,不计入与空页面基线内存等无关内存。 这意味着同一 user agent 二进制在两台不同设备上访问同一个页面应产生相同的结果。

网页可以推断如下 user agent 的信息:

类似信息也可以从现有 API 获取(navigator.userAgentnavigator.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 的反馈与贡献。

索引

本规范定义的术语

引用定义的术语

参考文献

规范性引用

[DOM]
Anne van Kesteren. DOM 标准. 实时标准. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript 语言规范. URL: https://tc39.es/ecma262/
[HR-TIME-2]
Ilya Grigorik. 高精度计时 Level 2. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; 等. HTML 标准. 实时标准. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 标准. 实时标准. URL: https://infra.spec.whatwg.org/
[SERVICE-WORKERS-1]
Alex Russell; 等. Service Workers 1. URL: https://w3c.github.io/ServiceWorker/
[WebIDL]
Boris Zbarsky. Web IDL. URL: https://heycam.github.io/webidl/

IDL 索引

dictionary MemoryMeasurement {
  unsigned long long bytes;
  sequence<MemoryBreakdownEntry> breakdown;
};

dictionary MemoryBreakdownEntry {
  unsigned long long bytes;
  sequence<MemoryAttribution> attribution;
  sequence<DOMString> types;
};

dictionary MemoryAttribution {
  USVString url;
  MemoryAttributionContainer container;
  DOMString scope;
};

dictionary MemoryAttributionContainer {
  DOMString id;
  USVString src;
};

partial interface Performance {
  [Exposed=(Window,ServiceWorker,SharedWorker), CrossOriginIsolated] Promise<MemoryMeasurement> measureUserAgentSpecificMemory();
};