Measure Memory API

Draft Community Group Report,

This version:
https://wicg.github.io/performance-measure-memory/
Editor:
(Google)
Participate:
GitHub WICG/performance-measure-memory (new issue, open issues)

Abstract

This specification defines an API that allows web applications to measure their memory usage.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

The tradeoff between memory and performance is inherent in many algorithms and data-structures. Web developers today have multiple ways to measure the timing information and no standard way to measure the memory usage. This specification defines a performance.measureUserAgentSpecificMemory() API that estimates the memory usage of the web application including all its iframes and workers. The new API is intended for aggregating memory usage data from production. The main use cases are:

1.1. Examples

A performance.measureUserAgentSpecificMemory() call returns a Promise and starts an asynchronous measurement of the memory allocated by the page.

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

For a simple page without iframes and workers the result might look as follows:

{
  bytes: 1000000,
  breakdown: [
    {
      bytes: 1000000,
      attribution: [
        {
          url: "https://example.com",
          scope: "Window",
        },
      ],
      types: ["JS", "DOM"],
    },
    {
      bytes: 0,
      attribution: [],
      types: [],
    },
  ],
}
Here all memory is attributed to the main page. The entry with bytes: 0 is present in the breakdown list to encourage processing of the result in a generic way without hardcoding specific entries. Such an entry is inserted at a random position if the list is not empty.

Other possible valid results:

{
  bytes: 1000000,
  breakdown: [],
}
Here the implementation provides only the total memory usage.
{
  bytes: 1000000,
  breakdown: [
    {
      bytes: 0,
      attribution: [],
      types: [],
    },
    {
      bytes: 1000000,
      attribution: [
        {
          url: "https://example.com",
          scope: "Window",
        },
      ],
      types: [],
    },
  ],
}
Here the implementation does not break memory down by memory types.

For a page that embeds a same-origin iframe the result might attribute some memory to that iframe and provide diagnostic information for identifying the 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"],
    },
  ],
}
Note how the url and container.src fields differ for the iframe. The former reflects the current location.href of the iframe whereas the latter is the value of the src attribute of the iframe element.

It is not always possible to separate iframe memory from page memory in a meaningful way. An implementation is allowed to lump together some or all of iframe and page memory:

{
  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: [],
    },
  ],
};
For a page that spawns a web worker the result includes the URL of the worker.
{
  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"],
    },
  ],
};
An implementation might lump together worker and page memory. If a worker is spawned by an iframe, then the worker’s attribution entry has a container field corresponding to the iframe element.

Memory of shared and service workers is not included in the result.

To get the memory usage of a shared/service worker, the performance.measureUserAgentSpecificMemory() function needs to be invoked in the context of that worker. The result could be something like:
{
  bytes: 1000000,
  breakdown: [
    {
      bytes: 1000000,
      attribution: [
        {
          url: "https://example.com/service-worker.js",
          scope: "ServiceWorkerGlobalScope",
        },
      ],
      types: ["JS"],
    },
    {
      bytes: 0,
      attribution: [],
      types: [],
    },
  ],
}
If a page embeds a cross-origin iframe, then the URL of that iframe is not revealed to avoid information leaks. Only the container element (which is already known to the page) appears in the result. Additionally, if the cross-origin iframe embeds other cross-origin iframes and/or spawns workers, then all their memory is aggregated and attributed to the top-most cross-origin iframe.

Consider a page with the following structure:

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)
A cross-origin iframe embeds to other iframes and spawns a worker. All memory of these resources is attributed to the first 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"],
    },
  ],
}
Note that the url and scope fields of the cross-origin iframe entry have special values indicating that information is not available.

If the implementation loads cross-origin iframes in a different address space, then their memory usage is not measured. The breakdown entries of such iframes have bytes: 0 indicating that these iframes are not accounted for:

{
  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: [],
    },
  ],
}
If a cross-origin iframe embeds an iframe of the same origin as the main page, then the same-origin iframe is revealed in the result. Note that there is no information leak because the main page can find and read location.href of the same-origin iframe.
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"],
    },
  ],
}

If the implementation omits memory measurement of cross-origin iframes, then the result could look like:

{
  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. Data model

2.1. Memory measurement result

The performance.measureUserAgentSpecificMemory() function returns a Promise that resolves to an instance of MemoryMeasurement dictionary:

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

A number that represents the total memory usage.

measurement . breakdown

An array that partitions the total bytes and provides attribution and type information.

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

The size of the memory that this entry describes.

breakdown . attribution

An array of URLs and/or container elements of the JavaScript realms that use the memory.

breakdown . types

An array of implementation-defined memory types associated with the memory.

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

If this attribution corresponds to a same-origin JavaScript realm, then this field contains realm’s URL. Otherwise, the attribution is for one or more cross-origin JavaScript realms and this field contains a sentinel value: "cross-origin-url".

attribution . container

Describes the DOM element that (maybe indirectly) contains the JavaScript realms. This property might be absent if the attribution is for the same-origin top-level realm. Note that cross-origin realms cannot be top-level due to cross-origin isolation.

attribution . scope

Describes the type of the same-origin JavaScript realm: "Window", "DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope" or "cross-origin-aggregated" for the cross-origin case.

dictionary MemoryAttributionContainer {
  DOMString id;
  USVString src;
};
container . id

The id attribute of the container element.

container . src

The src attribute of the container element. If the container element is an object element, then this field contains the value of the data attribute.

2.2. Intermediate memory measurement

This specification assumes the existence of an implementation-defined algorithm that measures the memory usage of the given set of agent clusters in the address space of the given current agent cluster. The result of such an algorithm is an intermediate memory measurement, which is a set of intermediate memory breakdown entries.

To preserve security guarantees of address space isolation, any intermediate memory breakdown entries that represent memory outside the current address space must have their bytes set to 0.

To reduce fingerprinting risks the result must include only the memory related to the web platform objects allocated or used by the given set of agent clusters. This, for example, excludes the memory of user-agent-specific extensions and the baseline memory of an empty page. The memory must be accounted at the address space level to exclude any platform-specific memory optimizations such memory compression and lazy memory committing.

An intermediate memory breakdown entry is a struct containing the following items:

bytes

The size of the memory that this intermediate memory breakdown entry describes, or 0 if this entry represents memory outside the current address space.

realms

A set of JavaScript realms to which the memory is attributed to.

types

A set of strings specifying implementation-defined memory types associated with the memory.

Algorithms defined in this specification show how to convert an intermediate memory measurement to an instance of MemoryMeasurement.

2.3. Memory attribution token

The link between an embedded JavaScript realm and its container element is ephemeral and is not guaranteed to always exist. For example, navigation to another document in the container element or removal of the container element from the DOM tree severs the link.

A memory attribution token provides a way to get from a JavaScript realm to its container element. It is a struct containing the following items:

container

An instance of MemoryAttributionContainer.

cross-origin aggregated flag

A boolean flag indicating whether the token was created for aggregating the memory usage of cross-origin JavaScript realms.

It is stored in a new internal field of WindowOrWorkerGlobalScope at construction time and is always available for memory reporting.

3. Processing model

3.1. Extensions to the Performance interface

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

A method that performs an asynchronous memory measurement. Details about the result of the method are in § 2.1 Memory measurement result.

3.2. Top-level algorithms

The measureUserAgentSpecificMemory() method steps are:
  1. Assert: the current Realm's settings objects's cross-origin isolated capability is true.

  2. If memory measurement allowed predicate given the current Realm is false, then:

    1. Return a promise rejected with a "SecurityError" DOMException.

  3. Let the current agent cluster be the current Realm's agent's agent cluster.

  4. Let agent clusters be the result of getting all agent clusters given the current Realm.

  5. Let promise be a new Promise.

  6. Start asynchronous implementation-defined memory measurement given the current agent cluster, agent clusters, and promise.

  7. Return promise.

To evaluate memory measurement allowed predicate given a JavaScript realm realm:
  1. Let global object be realm’s global object.

  2. If global object is a SharedWorkerGlobalScope, then return true.

  3. If global object is a ServiceWorkerGlobalScope, then return true.

  4. If global object is a Window then

    1. Let settings object be realm’s settings object.

    2. If settings object’s origin is the same as settings object’s top-level origin, then return true.

  5. Return false.

To get all agent clusters given an JavaScript realm realm:
  1. If realm’s global object is a Window, then:

    1. Let group be the browsing context group that contains realm’s global object's browsing context.

    2. Return the result of getting the values of group’s agent cluster map.

  2. Return « realm’s agent's agent cluster ».

To perform implementation-defined memory measurement given given an agent cluster the current agent cluster, a set of agent clusters agent clusters, and a Promise promise run these steps in parallel:
  1. Let intermediate memory measurement be implementation-defined intermediate memory measurement performed for the current agent cluster and agent clusters.

  2. Queue a global task on the TODO task source given promise’s relevant global object to resolve promise with the result of creating a new memory measurement given intermediate memory measurement.

3.3. Converting an intermediate memory measurement to the result

To create a new memory measurement given an intermediate memory measurement intermediate measurement:
  1. Let bytes be 0.

  2. For each intermediate memory breakdown entry intermediate entry in intermediate measurement:

    1. Set bytes to bytes plus intermediate entry’s bytes.

  3. Let breakdown be a new list.

  4. Append to breakdown a new MemoryBreakdownEntry whose:

  5. For each intermediate memory breakdown entry intermediate entry in intermediate measurement:

    1. Let breakdown entry be the result of creating a new memory breakdown entry given intermediate entry.

    2. Append breakdown entry to breakdown.

  6. Randomize the order of the items in breakdown.

  7. Return a new MemoryMeasurement whose:

To create a new memory breakdown entry given an intermediate memory breakdown entry intermediate entry:
  1. Let attribution a new list.

  2. For each JavaScript realm realm in intermediate entry’s realms:

    1. Let attribution entry be the result of creating a new memory attribution given realm.

    2. Append attribution entry to attribution.

  3. Let types be intermediate entry’s types.

  4. Randomize the order of the items in types.

  5. Return a new MemoryBreakdownEntry whose:

To create a new memory attribution given a JavaScript realm realm:
  1. Let token be realm’s global object's memory attribution token.

  2. If token’s cross-origin aggregated flag is true, then

    1. Return a new MemoryAttribution whose:

  3. Let scope name be identifier of realm’s global object's interface.

  4. Return a new MemoryAttribution whose:

3.4. Creating or obtaining a memory attribution token

To obtain a window memory attribution token given an origin origin, an origin parent origin, an origin top-level origin, an HTMLElement container element, and a memory attribution token parent token:
  1. If container element is null, then:

    1. Assert: parent origin is null.

    2. Assert: parent token is null.

    3. Assert: origin is equal to parent origin

    4. Return a new memory attribution token whose:

  2. If parent origin is not equal to top-level origin, then:

    1. Return parent token.

  3. Let container be the result of extracting container element attributes given container element.

  4. If origin is equal to top-level origin, then:

    1. Return a new memory attribution token whose:

  5. Return a new memory attribution token whose:

To obtain a worker memory attribution token given WorkerGlobalScope worker global scope, an environment settings object outside settings:
  1. If worker global scope is a DedicatedWorkerGlobalScope, then return outside settings’s global object's memory attribution token.

  2. Assert: worker global scope is a SharedWorkerGlobalScope or a ServiceWorkerGlobalScope.

  3. Return a new memory attribution token whose:

To extract container element attributes given an HTMLElement container element:
  1. Switch on container element’s local name:

    "iframe"

    Return a new MemoryAttributionContainer whose:

    • id is container element’s id attribute,

    • src is container element’s src attribute,

    "frame"

    Return a new MemoryAttributionContainer whose:

    • id is container element’s id attribute,

    • src is container element’s src attribute,

    "object"

    Return a new MemoryAttributionContainer whose:

    • id is container element’s id attribute,

    • src is container element’s data attribute,

4. Integration with the existing specification

4.1. Extension to WindowOrWorkerGlobalScope

A new internal field is added to WindowOrWorkerGlobalScope:
A memory attribution token

An memory attribution token that is used for reporting the memory usage of this environment.

4.2. Extensions to the existing algorithms

The run a worker algorithm sets the memory attribution token field of the newly created global object in step 6:

  1. Let realm execution context be the result of creating a new JavaScript realm given agent and the following customizations:

The create and initialize a Document object algorithm sets the memory attribution token field of the newly created global object:

  1. Otherwise:

    1. Let token be an empty memory attribution token.

    2. If browsingContext is not a top-level browsing context, then:

      1. Let parentToken be parentEnvironment’s global object's memory attribution token.

      2. Set token to the result of obtaining a window memory attribution token with origin, parentEnvironment’s origin, topLevelOrigin, browsingContext’s container, parentToken.

    3. Else, set token to the result of obtaining a window memory attribution token with origin, null topLevelOrigin, null, null.

    4. Let window global scope be the global object of realm execution context’s Realm component.

    5. Set window global scope’s memory attribution token to token.

The create a new browsing context algorithm sets the memory attribution token field of the newly created global object:

  1. Let token be an empty token.

  2. If embedder is null, then set token to the result of obtaining a window memory attribution token with origin, null, topLevelOrigin, null, null.

  3. Else, set token to the result of obtaining a window memory attribution token with origin, embedder’s relevant settings object's origin, topLevelOrigin, embedder, embedder’s relevant global object's memory attribution token.

  4. Let window global scope be the global object of realm execution context’s Realm component.

  5. Set window global scope’s memory attribution token to token.

5. Privacy and Security

5.1. Cross-origin information leak

The URLs and other string values that appear in the result are guaranteed to be known to the origin that invokes the API.

The only information that is exposed cross-origin is the size information provided in memoryMeasurement.bytes and memoryBreakdownEntry.bytes. The API relies on the cross-origin isolation mechanism to mitigate cross-origin size information leaks. Specifically, the API relies on the invariant that all resources in the current address space have opted in to be embeddable and legible by their embedding origin. The API does not expose the sizes of cross-origin resources loaded in a different address space.

5.2. Fingerprinting

The result of the API depends only on the objects allocated by the web page itself and does not include unrelated memory such as the baseline memory usage of an empty web page. This means the same user agent binary running on two different devices should produce the same results for a fixed web page.

A web page can infer the following information about the user agent:

Similar information can be obtained from the existing APIs (navigator.userAgent, navigator.platform). The bitness of the user agent can also be inferred by measuring the runtime of 32-bit and 64-bit operations.

Currently the API is available only to the top-level origin. In the future the top-level origin will be able to delegate the API to other origins using Permissions Policy. In both cases, cross-origin iframes do not get access to the API by default.

6. Acknowledgements

Thanks to Domenic Denicola and Shu-yu Guo for contributing to the API design and for reviewing this specification.

Also thanks to 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, and Yoav Weiss for their feedback and contributions.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/
[HR-TIME-2]
Ilya Grigorik. High Resolution Time Level 2. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[SERVICE-WORKERS-1]
Alex Russell; et al. Service Workers 1. URL: https://w3c.github.io/ServiceWorker/
[WebIDL]
Boris Zbarsky. Web IDL. URL: https://heycam.github.io/webidl/

IDL Index

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();
};