├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── IMPLEMENTATION_NOTES.md ├── LICENSE.md ├── ORIGIN_TRIAL.md ├── README.md ├── index.src.html └── w3c.json /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Web Platform Incubator Community Group 2 | 3 | This repository is being used for work in the W3C Web Platform Incubator Community Group, governed by the [W3C Community License 4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions, 5 | you must join the CG. 6 | 7 | If you are not the sole contributor to a contribution (pull request), please identify all 8 | contributors in the pull request comment. 9 | 10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 11 | 12 | ``` 13 | +@github_username 14 | ``` 15 | 16 | If you added a contributor by mistake, you can remove them in a comment with: 17 | 18 | ``` 19 | -@github_username 20 | ``` 21 | 22 | If you are making a pull request on behalf of someone else but you had no part in designing the 23 | feature, you can remove yourself with the above syntax. 24 | -------------------------------------------------------------------------------- /IMPLEMENTATION_NOTES.md: -------------------------------------------------------------------------------- 1 | # Implementation Notes 2 | 3 | ## Approach 1: process isolation of web pages 4 | A fast implementation is possible if the browser puts a cross-origin isolated web page into a separate process. 5 | Such an implementation can return the sizes of the relevant heaps with empty attributions. 6 | Inter-process communication may be required if cross-origin iframes are hosted in different processes. 7 | 8 | ## Approach 2: heap segregation by JS realm 9 | If the browser puts multiple web pages in the same process and the same heap, then it needs a way to distinguish objects belonging to different pages. 10 | An object that is allocated by JavaScript code can be attributed to the JS realm of [the running execution context](https://www.ecma-international.org/ecma-262/10.0/index.html#running-execution-context). 11 | Segregating objects on the heap by realm during allocation provides a way to tell objects of different pages apart and additionally enables fine-grained per-frame attribution. 12 | This comes at the cost of a more complex allocator and heap organisation with a separate space/partition for each realm. 13 | Note that attribution is not precise for objects shared between multiple realms. 14 | Another subtlety is that the realm that keeps an object alive (i.e. retains the object) is not necessarily the same realm that allocated the object. 15 | Moreover, both realms may differ from [the realm of the object's constructor](https://tc39.es/ecma262/#sec-getfunctionrealm). 16 | This is because realms of the same JavaScript agent can synchronously script with each other and can pass objects to each other. 17 | The implementation can sidestep this problem by making attribution more coarse-grained and merging the memory usage of all realms of the same JavaScript agent. 18 | 19 | ## Approach 3: accounting during garbage collection 20 | An alternative to heap segregation is dynamic attribution of object during garbage collection. 21 | The following algorithm shows how to carry out per-realm memory measurement in the marking phase of a Mark-Sweep garbage collector. 22 | 23 | Setup: 24 | 25 | 1. Assume that there is a partial function `InferRealm(object)` that uses implementation dependent heuristics to quickly compute the realm of the object or fails if that is not possible. 26 | For example, the function could return [the realm of the object's constructor](https://tc39.es/ecma262/#sec-getfunctionrealm). 27 | 2. Let `realms` be the set of realms present on the heap at the start of garbage collection. 28 | 3. For each `realm` in `realms` create a marking worklist `worklist[realm]`. 29 | 4. Create a special marking worklist `worklist[unknown]` for shared/unattributed objects. 30 | 5. Iterate roots and push the discovered objects onto `worklist[unknown]`. 31 | 32 | Marking worklist draining: 33 | 1. Pop an `object` from one of the non-empty worklists `worklist[realm]`, where `realm` can also be `unknown`. 34 | 2. If `InferRealm(object)` succeeds, then change `realm` to its result. 35 | 3. If `realm` is not `realms`, then it was created after the start of garbage collection and it does not have a worklist. In that case change `realm` to `unknown`. 36 | 4. Account the size of `object` to `realm`. 37 | 5. Iterate the reference in the object and push newly discovered to `worklist[realm]`. 38 | 39 | The algorithm precisely attributes objects with known realms. 40 | Additionally, objects that are not shared between multiple realms are accounted for precisely. 41 | However, attribution of shared objects that do not have known realms is non-deterministic. 42 | Shared strings and code objects are likely to be affected, so it might be worthwhile to add step 3a for such objects: 43 | 44 | 3.a. if `object` can be shared between multiple realms, then change `realm` to `unknown`. 45 | 46 | The algorithm [was implemented](https://bugs.chromium.org/p/chromium/issues/detail?id=973627) in Chrome/V8 and it adds 10%-20% overhead to garbage collection. 47 | The overhead applies only if there is a pending memroy measurement request. 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Reports in this Repository are licensed by Contributors under the 2 | [W3C Software and Document 3 | License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). Contributions to 4 | Specifications are made under the [W3C CLA](https://www.w3.org/community/about/agreements/cla/). -------------------------------------------------------------------------------- /ORIGIN_TRIAL.md: -------------------------------------------------------------------------------- 1 | # Origin Trial in Chrome 2 | 3 | The `performance.measureUserAgentSpecificMemory()` API is available as [an origin trial](https://developers.chrome.com/origintrials/#/view_trial/1281274093986906113) in Chrome 82 to 87. 4 | The origin trial ends on January 13, 2021. 5 | Follow [these instructions](https://web.dev/monitor-total-page-memory-usage/#enabling-support-during-the-origin-trial-phase) to register for the origin trial. 6 | 7 | Since the specification of the API is evolving and not finalized yet, the origin trial implementation differs from the specification in some aspects. 8 | 9 | ## Result differences 10 | The `attribution` field of the result has evolved from a simple URL string to an object with the `url`, `container`, and `scope` fields. 11 | 12 | The origin trial implementation uses the old format: 13 | ``` 14 | attribution: ['https://example.com/iframe.html'] 15 | ``` 16 | 17 | The new format defined in the specification provides more information: 18 | ``` 19 | attribution: [ 20 | { 21 | url: "https://example.com/iframe.html" 22 | container: { 23 | id: "example-id", 24 | src: "redirect.html?target=iframe.html", 25 | }, 26 | scope: "Window", 27 | } 28 | ] 29 | ``` 30 | 31 | Chrome 89 will start using the new format. 32 | 33 | ## Security differences 34 | The origin trial implementation relies on [Site Isolation](https://developers.google.com/web/updates/2018/07/site-isolation) for security whereas the explainer requires [cross-origin isolation](https://developers.google.com/web/updates/2018/07/site-isolation). 35 | In practice this means that the API will likely be available on desktop Chrome than on mobile Chrome because Site Isolation is enabled by default on desktop Chrome. 36 | 37 | Chrome 89 will be gated behind cross-origin isolation. 38 | 39 | ## Scope differences 40 | The implementation measures only JavaScript memory of the main window and all **same-site** iframes and related windows. 41 | (`foo.com`, `a.foo.com`, `b.foo.com` are same-site; `foo.com` and `bar.com` are not same-site). 42 | The API ignores the memory usage of non-JavaScript objects. 43 | 44 | Chrome 87 additionally measures memory usage of dedicated workers, but does not provide their URLs. 45 | Chrome 89 will add support for cross-site iframes and will provide URLs for workers. 46 | 47 | ## Other caveats 48 | The implementation may take up to 20 seconds to complete the memory measurement. 49 | This is because the implementation waits for the next garbage collection to perform the measurement. 50 | The API forces a garbage collection after 20 seconds. 51 | Starting Chrome with the `--enable-blink-features='ForceEagerMeasureMemory'` command-line flag reduces the timeout to zero and is useful for local debugging and testing. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # performance.measureUserAgentSpecificMemory API 2 | 3 | [Draft specification](https://wicg.github.io/performance-measure-memory/) 4 | 5 | [Origin trial and how it differs from the specification](ORIGIN_TRIAL.md) 6 | 7 | ## tl;dr 8 | We propose a new `peformance.measureUserAgentSpecificMemory` API that estimates memory usage of a web page including all its iframes and workers. The API is available only for [cross-origin isolated](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/crossOriginIsolated) web pages that opt in using [the COOP+COEP headers](https://docs.google.com/document/d/1zDlfvfTJ_9e8Jdc8ehuV4zMEu9ySMCiTGMS9y0GU92k/edit) preventing cross-origin information leaks. 9 | 10 | Example: 11 | ```JavaScript 12 | async function run() { 13 | const result = await performance.measureUserAgentSpecificMemory(); 14 | console.log(result); 15 | } 16 | run(); 17 | // Console output: 18 | { 19 | bytes: 2300000, 20 | breakdown: [ 21 | { 22 | bytes: 1000000, 23 | attribution: [ 24 | { 25 | url: "https://example.com", 26 | scope: "Window", 27 | }, 28 | ], 29 | types: ["JS", "DOM"], 30 | }, 31 | { 32 | bytes: 500000, 33 | attribution: [ 34 | { 35 | url: "https://example.com/iframe.html" 36 | container: { 37 | id: "example-id", 38 | src: "redirect.html?target=iframe.html", 39 | }, 40 | scope: "Window", 41 | } 42 | ], 43 | types: ["DOM", "JS"], 44 | }, 45 | { 46 | bytes: 800000, 47 | attribution: [ 48 | { 49 | url: "https://example.com/worker.js", 50 | scope: "DedicatedWorkerGlobalScope", 51 | }, 52 | ], 53 | types: ["JS"], 54 | }, 55 | { 56 | bytes: 0, 57 | attribution: [], 58 | types: [], 59 | }, 60 | ], 61 | } 62 | ``` 63 | 64 | ## Problem 65 | As shown in [this collection of use cases](https://docs.google.com/document/d/1u21oa3-R1FhHgrPsh8-mpb8dIFVj60wcFiM5FFrfIQA/edit#heading=h.6si74uwp7sq8) there is a need for an API that measures memory footprint of web pages in production. 66 | The use cases include a) analysis of correlation between memory usage and user metrics, b) detection of memory regressions, c) evaluation of feature launches in A/B tests, d) memory optimization. 67 | 68 | Currently developers resort to the non-standard `performance.memory` API that [is used in 20%](https://www.chromestatus.com/metrics/feature/timeline/popularity/884) of page loads in Chrome. 69 | The API reports the size of the JS heap and thus has two major drawbacks: 70 | 1) it may greatly _overestimate_ the actual memory usage if other large web pages share the same heap. 71 | 2) it may greatly _underestimate_ the actual memory usage if the web page spawns workers and/or embeds cross-site iframes that are allocated in separate heaps. 72 | 73 | In this proposal we aim to fix these drawbacks and set the following requirements for the new API. 74 | 75 | ### Requirements 76 | - The API does not leak cross-origin information. 77 | - The API measures memory usage of the web page including all its iframes and workers. 78 | - The API accounts only the objects allocated by the web page. Other web pages do not affect the result. 79 | - The API provides breakdown of the result by type and owner with implementation-specific granularity. 80 | - The API has no overhead when it is not used. 81 | - The API is defined based on the standard concepts from HTML and ECMAScript specifications and does not assume any particular process model. 82 | 83 | ### Non-Goals 84 | - Precise memory measurement. Implementations are allowed to return an estimate because computing the precise result may be expensive. 85 | - Complete memory measurement. Implementations are free to account as many memory types (JS, DOM, CSS) as possible, but they are not required to account all memory types. Memory types that cannot be isolated per page can be omitted. 86 | - Comparing memory usage across browsers. The API results are browser implementation specific. 87 | - Synchronous memory measurement before and after a specific action. The API has an asynchronous interface to allow folding the measurement into garbage collection and perform necessary interprocess communication. It may take seconds or minutes until the result is available. 88 | 89 | ## Related Work 90 | ### JavaScript agent memory API 91 | The current proposal is generalization and extension of the previous [JavaScript agent memory API proposal](https://github.com/ulan/javascript-agent-memory/blob/master/explainer.md) that was presented at [WebPerf WG F2F June 2019](https://docs.google.com/document/d/12ANc7fbKpjs__Qw_0DxM74u49276vTwRwCPyBxUkfBw/edit#heading=h.nraz045xllk0) meeting. 92 | 93 | The main difference is that the current proposal relies on COOP+COEP for security. 94 | This allows us to increase the scope of the API beyond a single JavaScript agent to cover cross-origin iframes, workers as well as other kinds of memory. 95 | 96 | ### Process memory API 97 | There was a proposal for [process memory API](https://github.com/WICG/performance-memory/blob/master/explainer.md) that estimates memory footprint of the site and accounts different types of memory: JS, DOM, CSS, Web Workers spawned by the page, etc. 98 | The proposal [was initially blocked](https://github.com/mozilla/standards-positions/issues/85#issuecomment-426382208) by information leak of opaque cross-origin resources and is currently abandoned. 99 | 100 | Our proposal combines this process memory API with the JS agent memory API. 101 | The main difference between our proposal and the process memory API is that the scope of the process memory API is limited to the current [JS agent cluster](https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-cluster-formalism) (The proposal uses the old term -- "related similar-origin browsing contexts"). 102 | This means that the API measures only same-site memory and does not measure cross-site iframes. 103 | In contrast to that, our API measures all JS agent clusters of the current [browsing context group](https://html.spec.whatwg.org/multipage/browsers.html#browsing-context-group). 104 | Besides that, our API provides breakdown of the result by type and owner. 105 | 106 | ### Memory pressure API 107 | There is a proposal for [memory pressure API](https://github.com/WICG/memory-pressure/blob/master/explainer.md) that notifies the application about system memory pressure events. 108 | This gives the application an opportunity to change its behavior at runtime to reduce its memory usage if possible e.g. by freeing up caches and unused resources. 109 | Our proposal has different [use cases](https://docs.google.com/document/d/1u21oa3-R1FhHgrPsh8-mpb8dIFVj60wcFiM5FFrfIQA/edit#heading=h.6si74uwp7sq8) such as collecting telemetry data and detecting regressions. 110 | Thus the two proposals are orthogonal. 111 | 112 | ## API Proposal 113 | The API consists of a single asynchronous method `performance.measureUserAgentSpecificMemory` that estimates memory usage of the web page and provides breakdown of the result by type and owner. 114 | 115 | ```JavaScript 116 | async function run() { 117 | const result = await performance.measureUserAgentSpecificMemory(); 118 | console.log(result); 119 | } 120 | run(); 121 | ``` 122 | 123 | ### Example 1 124 | For a simple page without iframes and workers the result might look as follows: 125 | ```JavaScript 126 | { 127 | bytes: 1000000, 128 | breakdown: [ 129 | { 130 | bytes: 1000000, 131 | attribution: [ 132 | { 133 | url: "https://example.com", 134 | scope: "Window", 135 | }, 136 | ], 137 | types: ["JS", "DOM"], 138 | }, 139 | { 140 | bytes: 0, 141 | attribution: [], 142 | types: [], 143 | }, 144 | ], 145 | } 146 | ``` 147 | *Note: the format of the result has changed recently and the origin trail running in Chrome 82-87 uses the old format. See [Origin Trial](ORIGIN_TRIAL.md) for details.* 148 | 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. 149 | Such an entry is inserted at a random position if the list is not empty. 150 | 151 | Providing only the total memory usage is also a valid implementation: 152 | ```JavaScript 153 | { 154 | bytes: 1000000, 155 | breakdown: [], 156 | } 157 | ``` 158 | 159 | Similarly, the implementation might return empty `attribution: []` and/or empty `types: []`. 160 | 161 | The top-level `bytes` field contains the total estimate of the web page's memory usage. 162 | Each entry of the `breakdown` array describes some portion of the memory and attributes it to a set of windows and workers. 163 | The entries are disjoint and their sizes sum up to the total `bytes`. 164 | 165 | The `types` field lists memory types associated with the memory portion. 166 | As the name suggests each memory type is entirely implementation specific. 167 | In other words, memory types are not comparable across different browsers and may even change between different versions of the same browser. 168 | The order of memory types in the list is not significant and also depends on the implementation. 169 | An implementation may (but is not required to) use the following as a memory type: 170 | 171 | - the name of a JavaScript object type or constructor: `Function`, `SharedArrayBuffer`, `String`, etc. 172 | - the name of a WebIDL interface: `HTMLCanvasElement`, `HTMLElement`, etc. 173 | - implementation specific names: `JS`, `DOM`, `GPU`, `Detached`, `Code`, etc. 174 | 175 | ### Example 2 176 | 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. 177 | 178 | ```HTML 179 | 180 |
181 | 184 | 185 | 186 | ``` 187 | 188 | ```JavaScript 189 | { 190 | bytes: 1500000, 191 | breakdown: [ 192 | { 193 | bytes: 0, 194 | attribution: [], 195 | types: [], 196 | }, 197 | { 198 | bytes: 1000000, 199 | attribution: [ 200 | { 201 | url: "https://example.com", 202 | scope: "Window", 203 | }, 204 | ], 205 | types: ["JS", "DOM"], 206 | }, 207 | { 208 | bytes: 500000, 209 | attribution: [ 210 | { 211 | url: "https://example.com/iframe.html" 212 | container: { 213 | id: "example-id", 214 | src: "redirect.html?target=iframe.html", 215 | }, 216 | scope: "Window", 217 | } 218 | ], 219 | types: ["DOM", "JS"], 220 | }, 221 | ], 222 | } 223 | ``` 224 | 225 | 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. 226 | 227 | 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: 228 | 229 | ```JavaScript 230 | { 231 | bytes: 1500000, 232 | breakdown: [ 233 | { 234 | bytes: 0, 235 | attribution: [], 236 | types: [], 237 | }, 238 | { 239 | bytes: 1500000, 240 | attribution: [ 241 | { 242 | url: "https://example.com", 243 | scope: "Window", 244 | }, 245 | { 246 | url: "https://example.com/iframe.html", 247 | container: { 248 | id: "example-id", 249 | src: "redirect.html?target=iframe.html", 250 | }, 251 | scope: "Window", 252 | }, 253 | ], 254 | types: ["JS", "DOM"], 255 | }, 256 | ], 257 | }; 258 | ``` 259 | 260 | ### Example 3 261 | For a page that spawns a web worker the result includes the URL of the worker. 262 | ```JavaScript 263 | { 264 | bytes: 1800000, 265 | breakdown: [ 266 | { 267 | bytes: 1000000, 268 | attribution: [ 269 | { 270 | url: "https://example.com", 271 | scope: "Window", 272 | }, 273 | ], 274 | types: ["DOM", "JS"], 275 | }, 276 | { 277 | bytes: 0, 278 | attribution: [], 279 | types: [], 280 | }, 281 | { 282 | bytes: 800000, 283 | attribution: [ 284 | { 285 | url: "https://example.com/worker.js", 286 | scope: "DedicatedWorkerGlobalScope", 287 | }, 288 | ], 289 | types: ["JS"], 290 | }, 291 | ], 292 | }; 293 | ``` 294 | 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. 295 | 296 | Memory of shared and service workers is not included in the result. 297 | 298 | ### Example 4 299 | 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: 300 | ```JavaScript 301 | { 302 | bytes: 1000000, 303 | breakdown: [ 304 | { 305 | bytes: 1000000, 306 | attribution: [ 307 | { 308 | url: "https://example.com/service-worker.js", 309 | scope: "ServiceWorkerGlobalScope", 310 | }, 311 | ], 312 | types: ["JS"], 313 | }, 314 | { 315 | bytes: 0, 316 | attribution: [], 317 | types: [], 318 | }, 319 | ], 320 | } 321 | ``` 322 | 323 | ### Example 5 324 | 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. 325 | 326 | Consider a page with the following structure: 327 | ``` 328 | example.com (1000000 bytes) 329 | | 330 | *--foo.com/iframe1 (500000 bytes) 331 | | 332 | *--foo.com/iframe2 (200000 bytes) 333 | | 334 | *--bar.com/iframe2 (300000 bytes) 335 | | 336 | *--foo.com/worker.js (400000 bytes) 337 | ``` 338 | 339 | A cross-origin iframe embeds to other iframes and spawns a worker. All memory of these resources is attributed to the first iframe. 340 | ```HTML 341 | 342 | 343 | 346 | 347 | 348 | ``` 349 | 350 | ```JavaScript 351 | { 352 | bytes: 2400000, 353 | breakdown: [ 354 | { 355 | bytes: 0, 356 | attribution: [], 357 | types: [], 358 | }, 359 | { 360 | bytes: 1000000, 361 | attribution: [ 362 | { 363 | url: "https://example.com", 364 | scope: "Window", 365 | }, 366 | ], 367 | types: ["DOM", "JS"], 368 | }, 369 | { 370 | bytes: 1400000, 371 | attribution: [ 372 | { 373 | url: "cross-origin-url", 374 | container: { 375 | id: "example-id", 376 | src: "https://foo.com/iframe1", 377 | }, 378 | scope: "cross-origin-aggregated", 379 | }, 380 | ], 381 | types: ["JS", "DOM"], 382 | }, 383 | ], 384 | } 385 | ``` 386 | 387 | Note that the `url` and `scope` fields of the cross-origin iframe entry have special values indicating that information is not available. 388 | 389 | ### Example 6 390 | 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. 391 | 392 | ``` 393 | example.com (1000000 bytes) 394 | | 395 | *--foo.com/iframe1 (500000 bytes) 396 | | 397 | *--example.com/iframe2 (200000 bytes) 398 | ``` 399 | 400 | ```HTML 401 | 402 | 403 | 406 | 407 | 408 | ``` 409 | 410 | ```JavaScript 411 | { 412 | bytes: 1700000, 413 | breakdown: [ 414 | { 415 | bytes: 1000000, 416 | attribution: [ 417 | { 418 | url: "https://example.com", 419 | scope: "Window", 420 | }, 421 | ], 422 | types: ["JS", "DOM"], 423 | }, 424 | { 425 | bytes: 0, 426 | attribution: [], 427 | types: [], 428 | }, 429 | { 430 | bytes: 500000, 431 | attribution: [ 432 | { 433 | url: "cross-origin-url", 434 | container: { 435 | id: "example-id", 436 | src: "https://foo.com/iframe1", 437 | }, 438 | scope: "cross-origin-aggregated", 439 | }, 440 | ], 441 | types: ["DOM", "JS"], 442 | }, 443 | { 444 | bytes: 200000, 445 | attribution: [ 446 | { 447 | url: "https://example.com/iframe2", 448 | container: { 449 | id: "example-id", 450 | src: "https://foo.com/iframe1", 451 | }, 452 | scope: "Window", 453 | }, 454 | ], 455 | types: ["JS", "DOM"], 456 | }, 457 | ], 458 | } 459 | ``` 460 | 461 | ### Alternatives 462 | Adding a `userAgentSpecific` prefix to the `bytes` and `attribution` fields would emphasize that the API result is implementation dependent: 463 | ```JavaScript 464 | {userAgentSpecificBytes: 1000000, ...}, 465 | ``` 466 | Our preference however is to communicate the message in documentation and keep the API concise and consistent with other Performance APIs. 467 | 468 | ### Scope 469 | The API is available for windows, shared workers, and service workers. 470 | When invoked in a window context, the API estimates memory usage of all [JS agent clusters](https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-cluster-formalism) of the [browsing context group](https://html.spec.whatwg.org/multipage/browsers.html#browsing-context-group) of the window. 471 | (The scope will be restricted to the current address space for security. See Issues #5 and #20.) 472 | Note that the result accounts for all iframes and nested dedicated workers. 473 | In the shared/service worker case, the API estimates memory usage of the worker's JS agent cluster that includes all nested dedicated workers. 474 | 475 | The API is not available in the context of a dedicated worker. 476 | There are two reasons for this design choice. 477 | First, the intended use of the API is to have a global memory monitor for a web page that performs statistical sampling of memory usage and sends the samples to the server for A/B testing and regression detection. 478 | Since the top-level context (a window or a shared/service worker) already measures all nested dedicated workers, invoking the API in a dedicated worker is likely a misuse of the API. 479 | The second reason is to simplify implementation of the API by ensuring that at most one thread in a JS agent cluster can request memory measurement for the cluster. 480 | 481 | ## Security Considerations 482 | ### Cross-origin information leaks 483 | The URLs and other string values that appear in the result are guaranteed to be known to the origin that invokes the API. 484 | 485 | The only information that is exposed cross-origin is the size information provided in the `bytes` fields. 486 | The API relies on the [cross-origin isolation](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/crossOriginIsolated) mechanism to mitigate cross-origin size information leaks. 487 | Specifically, the API relies on the invariant that all loaded resources have opted in to be embeddable and legible by their embedding origin. 488 | 489 | ### Fingerprinting 490 | 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. 491 | This means the same user agent binary running on two different devices should produce the same results for a fixed web page. 492 | 493 | A web page can infer the following information about the user agent: 494 | - the bitness of the user agent (32-bit vs 64-bit). 495 | - the version of the user agent to some extent. 496 | 497 | Similar information can be obtained from the existing APIs (`navigator.userAgent`, `navigator.platform`). 498 | The bitness of the user agent can also be inferred by measuring the runtime of 32-bit and 64-bit operations. 499 | 500 | ## Performance Considerations 501 | The API with coarse-grained breakdown can be implemented efficiently provided that the browser does not collocate a cross-origin web page with other web pages in the same process. 502 | In such a case the API can return the sizes of the heaps (JS, DOM, CSS, worker, etc) with empty attributions. 503 | 504 | Fine-grained attribution requires more expensive computation because objects from different frames may be allocated on the same heap. 505 | One possible implementation is to segregate objects by frame during allocation. 506 | Alternative implementation is to infer the object's frame while traversing the object graph during garbage collection. 507 | This was implemented in Chrome/V8 and introduces 10%-20% overhead to garbage collection (only if there is a pending memory measurement request). 508 | See [Implementation Notes](IMPLEMENTATION_NOTES.md) for more details. 509 | 510 | ## API Usage 511 | The API is intended for A/B testing, regressions detection, and general analysis of aggregate memory usage data from production. 512 | The results of individual calls are less useful because they are sensitive to the timing of web page events, user actions, and garbage collection. 513 | We recommend calling the API periodically every `N` minutes. 514 | Even better would be to use statistical sampling such as Poisson sampling to avoid the bias of a fixed sampling interval as shown below. 515 | 516 | ```JavaScript 517 | // Starts statistical sampling of the memory usage. 518 | function scheduleMeasurement() { 519 | if (!performance.measureUserAgentSpecificMemory) { 520 | console.log('performance.measureUserAgentSpecificMemory is not available.'); 521 | return; 522 | } 523 | let interval = measurementInterval(); 524 | console.log('Scheduling memory measurement in ' + 525 | `${Math.round(interval / 1000)} seconds.`); 526 | setTimeout(performMeasurement, interval); 527 | } 528 | 529 | async function performMeasurement() { 530 | // 1. Invoke performance.measureUserAgentSpecificMemory(). 531 | let result; 532 | try { 533 | result = await performance.measureUserAgentSpecificMemory(); 534 | } catch (error) { 535 | if (error instanceof DOMException && 536 | error.name === 'SecurityError') { 537 | console.log(`Cannot measure memory: ${error.message}.`); 538 | return; 539 | } 540 | throw error; 541 | } 542 | // 2. Record the result. 543 | console.log(`Memory usage: ${result.bytes} bytes`); 544 | console.log('Memory breakdown: ', result.breakdown); 545 | // 3. Schedule the next measurement. 546 | scheduleMeasurement(); 547 | } 548 | 549 | // Returns a random interval in milliseconds that is 550 | // sampled with a Poisson process. It ensures that on 551 | // average there is one measurement every five minutes. 552 | function measurementInterval() { 553 | const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000; 554 | return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS; 555 | } 556 | ``` 557 | 558 | ## See Also 559 | 560 | Links for [the previous version](https://github.com/ulan/javascript-agent-memory/blob/master/explainer.md) of the API that did not rely on COOP+COEP for security: 561 | - [WebPerf WG F2F June 2019 presentation](https://docs.google.com/document/d/1uQ7pXwuBv-1jitYou7TALJxV0tllXLxTyEjA2n1mSzY/edit#heading=h.x0ih8vnl6qwo) 562 | - [TAG design review](https://github.com/w3ctag/design-reviews/issues/386) 563 | - [WICG discussion thread](https://discourse.wicg.io/t/proposal-javascript-agent-memory-api/3586) 564 | -------------------------------------------------------------------------------- /index.src.html: -------------------------------------------------------------------------------- 1 |2 | Title: Measure Memory API 3 | Status: CG-DRAFT 4 | Group: WICG 5 | Repository: WICG/performance-measure-memory 6 | Shortname: performance-measure-memory 7 | Markup Shorthands: markdown yes, css no 8 | Level: None 9 | URL: https://wicg.github.io/performance-measure-memory/ 10 | Editor: Ulan Degenbaev, Google https://www.google.com/, ulan@google.com 11 | Abstract: 12 | This specification defines an API that allows web applications to measure their memory usage. 13 | Indent: 2 14 | Default Biblio Status: current 15 | Boilerplate: omit conformance, omit feedback-header 16 | Complain About: accidental-2119 yes, missing-example-ids yes 17 | !Participate: GitHub WICG/performance-measure-memory (new issue, open issues) 18 |19 | 20 | 21 |
22 | urlPrefix: https://tc39.es/ecma262/; spec: ECMAScript 23 | type: dfn 24 | text: Realm; url: sec-code-realms 25 | text: JavaScript realm; url: sec-code-realms 26 | text: JavaScript realms; url: sec-code-realms 27 | text: agent cluster; url: sec-agent-clusters 28 | text: agent cluster; for: agent; url: sec-agent-clusters 29 | text: agent; for: Realm; url: sec-agents 30 | text: the current Realm; url: sec-agent 31 | urlPrefix: https://w3c.github.io/hr-time/; spec: HR-TIME-2 32 | type: interface 33 | text: Performance; url: #sec-performance 34 | urlPrefix: https://html.spec.whatwg.org/ 35 | type: dfn 36 | text: agent cluster map; for: browsing context group; url: multipage/browsers.html#agent-cluster-map 37 | type: element-attr 38 | text: id; for: element; url: multipage/dom.html#the-id-attribute 39 | text: src; for: frame; url: multipage/obsolete.html#dom-frame-src 40 | type: abstract-op 41 | text: run a worker; url: multipage/workers.html#run-a-worker 42 | text: set up a window environment settings object; url: multipage/browsing-the-web.html#set-up-a-window-environment-settings-object 43 | text: create and initialize a Document object; url: multipage/browsing-the-web.html#initialise-the-document-object 44 | text: create a new browsing context; url: multipage/browsers.html#creating-a-new-browsing-context 45 | urlPrefix: https://github.com/heycam/webidl 46 | type: extended-attribute 47 | text: CrossOriginIsolated; url: pull/883 48 | 49 |50 | 51 |
52 | spec:html; type:dfn; text: browsing context group 53 | spec:infra; type:dfn; for:/; text:set 54 | spec:infra; type:dfn; for:/; text:list 55 | spec:infra; type:dfn; for:list; text:for each 56 | spec:html; type:dfn; for:Realm; text:settings object 57 | spec:webidl; type:dfn; text:resolve 58 | spec:webidl; type:dfn; text:identifier 59 | spec:dom; type:dfn; for:Element; text:local name 60 |61 | 62 | 63 | 94 | 95 | 96 | Introduction {#intro} 97 | ===================== 98 | 99 |
594 | dictionary MemoryMeasurement { 595 | unsigned long long bytes; 596 | sequence<MemoryBreakdownEntry> breakdown; 597 | }; 598 |599 |
measurement . {{MemoryMeasurement/bytes}}
601 | :: A number that represents the total memory usage.
602 |
603 | : measurement . {{MemoryMeasurement/breakdown}}
604 | :: An array that partitions the total {{MemoryMeasurement/bytes}} and provides attribution and type information. 607 | dictionary MemoryBreakdownEntry { 608 | unsigned long long bytes; 609 | sequence<MemoryAttribution> attribution; 610 | sequence<DOMString> types; 611 | }; 612 |613 |
breakdown . {{MemoryBreakdownEntry/bytes}}
615 | :: The size of the memory that this entry describes.
616 |
617 | : breakdown . {{MemoryBreakdownEntry/attribution}}
618 | :: An array of URLs and/or container elements of the [=JavaScript realms=] that use the memory.
619 |
620 | : breakdown . {{MemoryBreakdownEntry/types}}
621 | :: An array of [=implementation-defined=] memory types associated with the memory. 624 | dictionary MemoryAttribution { 625 | USVString url; 626 | MemoryAttributionContainer container; 627 | DOMString scope; 628 | }; 629 |630 |
attribution . {{MemoryAttribution/url}}
632 | :: If this attribution corresponds to a same-origin [=JavaScript realm=], then this field contains realm's URL.
633 | Otherwise, the attribution is for one or more cross-origin [=JavaScript realms=] and this field contains
634 | a sentinel value: `"cross-origin-url"`.
635 |
636 | : attribution . {{MemoryAttribution/container}}
637 | :: Describes the DOM element that (maybe indirectly) contains the [=JavaScript realms=].
638 | This property might be absent if the attribution is for the same-origin top-level realm.
639 | Note that cross-origin realms cannot be top-level due to [=environment settings object/cross-origin isolated capability|cross-origin isolation=].
640 |
641 | : attribution . {{MemoryAttribution/scope}}
642 | :: Describes the type of the same-origin [=JavaScript realm=]:
643 | "Window", "DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope"
644 | or "cross-origin-aggregated"
for the cross-origin case. 647 | dictionary MemoryAttributionContainer { 648 | DOMString id; 649 | USVString src; 650 | }; 651 |652 |
container . {{MemoryAttributionContainer/id}}
654 | :: The `id` attribute of the container element.
655 |
656 | : container . {{MemoryAttributionContainer/src}}
657 | :: The `src` attribute of the container element. If the container element is an <{object}> element,
658 | then this field contains the value of the <{object/data}> attribute.712 | partial interface Performance { 713 | [Exposed=(Window,ServiceWorker,SharedWorker), CrossOriginIsolated] Promise<MemoryMeasurement> measureUserAgentSpecificMemory(); 714 | }; 715 |716 |
performance . {{Performance/measureUserAgentSpecificMemory()}}
718 | :: A method that performs an asynchronous memory measurement. Details about the result of the method are in [[#memory-measurement-result]].