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.measureMemory() 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.measureMemory() call returns a Promise and starts an asynchronous measurement of the memory allocated by the page.

async function run() {
  const result = await performance.measureMemory();
  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",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
  ],
}
Here all memory is attributed to the main page.

Other possible valid results:

{
  bytes: 1000000,
  breakdown: [],
}
Here the implementation provides only the total memory usage.
{
  bytes: 1000000,
  breakdown: [
    {
      bytes: 1000000,
      attribution: [
        {
          url: "https://example.com",
          scope: "Window",
        },
      ],
      userAgentSpecificTypes: [],
    },
  ],
}
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",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
    {
      bytes: 500000,
      attribution: [
        {
          url: 'https://example.com/iframe.html'
          container: {
            id: "example-id",
            src: 'redirect.html?target=iframe.html',
          },
          scope: 'Window',
        }
      ],
      userAgentSpecificTypes: ['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",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
  ],
};
For a page that spawns a web worker the result includes the URL of the worker.
{
  bytes: 1800000,
  breakdown: [
    {
      bytes: 1000000,
      attribution: [
        {
          url: "https://example.com",
          scope: "Window",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
    {
      bytes: 800000,
      attribution: [
        {
          url: "https://example.com/worker.js",
          scope: "DedicatedWorkerGlobalScope",
        },
      ],
      userAgentSpecificTypes: ["JS"],
    },
  ],
};
An implementation might lump together worker and page memory. If a worker is spawned by an iframe, then 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.measureMemory() 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",
        },
      ],
      userAgentSpecificTypes: ["JS"],
    },
  ],
}
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",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
    {
      bytes: 1400000,
      attribution: [
        {
          url: "cross-origin-url",
          container: {
            id: "example-id",
            src: "https://foo.com/iframe1",
          },
          scope: "cross-origin-aggregated",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
  ],
}
Note that the url and scope fields of the cross-origin iframe entry have special values indicating that information is not available.
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",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
    {
      bytes: 500000,
      attribution: [
        {
          url: "cross-origin-url",
          container: {
            id: "example-id",
            src: "https://foo.com/iframe1",
          },
          scope: "cross-origin-aggregated",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
    {
      bytes: 200000,
      attribution: [
        {
          url: "https://example.com/iframe2",
          container: {
            id: "example-id",
            src: "https://foo.com/iframe1",
          },
          scope: "Window",
        },
      ],
      userAgentSpecificTypes: ["JS", "DOM"],
    },
  ],
}

2. Data model

2.1. Memory measurement result

The performance.measureMemory() 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> userAgentSpecificTypes;
};
breakdown . bytes

The size of the memory that this entry describes.

breakdown . attribution

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

breakdown . userAgentSpecificTypes

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. It may be empty 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 contains "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 existences of an implementation-defined algorithm that can measure the memory of objects allocated by a set of JavaScript agent clusters. The result of such algorithm is an intermediate memory measurement, which is a set of intermediate memory breakdown entries.

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.

realms

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

user agent specific 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 JavaSript 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 {
  Promise<MemoryMeasurement> measureMemory();
};
self . measureMemory()

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 measureMemory() method steps are:
  1. Assert: current Realm's agent's agent cluster's cross-origin isolated is true.

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

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

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

  4. Let promise be a new Promise.

  5. Start asynchronous implementation-defined memory measurement given agent clusters and promise.

  6. 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 a set of agent clusters agent clusters and a Promise promise run these steps in parallel:
  1. Let intermediate memory measurement be an implementation-defined intermediate memory measurement of 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. 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.

  5. 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. 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, an 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. Let container be the result of extracting container element attributes given container element.

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

    1. Return a new memory attribution token whose:

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

    1. Return a new memory attribution token whose:

  5. Return parent token.

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. Acknowledgements

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

Also thanks to Anne van Kesteren, Boris Zbarsky, Dominik Inführ, Chris Hamilton, Hannes Payer, Joe Mason, Kentaro Hara, L. David Baron, Mathias Bynens, Matthew Bolohan, Michael Lippautz, 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> userAgentSpecificTypes;
};

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

dictionary MemoryAttributionContainer {
  DOMString id;
  USVString src;
};

partial interface Performance {
  Promise<MemoryMeasurement> measureMemory();
};