Skip to content

Add Support for the ContainerTiming PaintMetric - #46966

Merged
jdm merged 7 commits into
servo:mainfrom
jasonwilliams:container-timing
Oct 9, 2026
Merged

jdm merged 7 commits into
servo:mainfrom
jasonwilliams:container-timing

Conversation

@jasonwilliams

@jasonwilliams jasonwilliams commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Hi, I'm a spec champion for the Container Timing feature and I wanted to add it to Servo (behind a flag).

The Container Timing API enables monitoring when annotated sections of the DOM are displayed on screen and have finished their initial paint. I am one of the spec authors, and i've been helping this land in both Chromium and Firefox

This PR implements the Container Timing API, following the same layout > paint > constellation pipeline already used for LCP.

Steps for calculating Container Timing:

  1. When an element has the containertiming attribute, a flag is propagated down through its descendants (set on the Node/fragment) so layout can cheaply tell "is this inside a container-timing root" without walking back up the tree on every fragment.
  2. While building the display list, each painted text/image fragment that belongs to a container-timing root reports its viewport-clipped size and rect to a PaintTimingHandler, similar to how LCP candidates are collected.
  3. PaintTimingHandler keeps one running record per container root (keyed by a stable node ID, since the containertiming value itself is optional and not unique), accumulating size/rect across paints, and tracks which containers were actually touched since the last hand-off.
  4. Layout hands the finished per-container records to script as part of the reflow result
  5. once webrender confirms that epoch composited, it sends PaintMetricEvent::ContainerTiming(paint_time, ids) to script, which stamps the already-stored records with the real paint/first-render time and queues the finished PerformanceContainerTiming entries.

Testing:

  • There are plenty of Web Platform Tests for Container Timing. I have been testing the build against these as i have been going along, 19/29 tests pass.
  • There are tests for some components, such as paintedRegion

Follow ups & investigation:

  • lastPaintedElement selection picks by traversal order, not largest area
  • [Tidy Up] The effective_visual_size check was originally written for LCP, but is useful for Container Timing as well, we should make this API generic

@jasonwilliams
jasonwilliams force-pushed the container-timing branch 6 times, most recently from 4f18c08 to bd2d948 Compare August 7, 2026 11:07
@jdm

jdm commented Aug 19, 2026

Copy link
Copy Markdown
Member

@jasonwilliams I've been ignoring this because it's marked as a draft. Are you waiting for someone to review it?

@jasonwilliams

Copy link
Copy Markdown
Contributor Author

@jasonwilliams I've been ignoring this because it's marked as a draft. Are you waiting for someone to review it?

Thanks for reaching out @jdm, no not yet, it’s still a WIP.

Ill mark it as review and ping you when I need reviews.

@jasonwilliams
jasonwilliams force-pushed the container-timing branch 9 times, most recently from 0282ab5 to 83e2dc0 Compare September 21, 2026 16:00
@jasonwilliams
jasonwilliams marked this pull request as ready for review September 21, 2026 16:07
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Sep 21, 2026
@shubhamg13

shubhamg13 commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Yeah, Nice.

Please put a separate PR to enable WPT for container-timing/ in include.ini. So, that we can see the visible effects.

Edit: + With WPT (Failing) Expectations

@jasonwilliams
jasonwilliams force-pushed the container-timing branch 2 times, most recently from 88ae8e7 to f93db82 Compare September 22, 2026 10:38
@jasonwilliams

Copy link
Copy Markdown
Contributor Author

Yeah, Nice.

Please put a separate PR to enable WPT for container-timing/ in include.ini. So, that we can see the visible effects.

Done: #48290

@servo-highfive servo-highfive added the S-needs-rebase There are merge conflict errors. label Sep 29, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Test results for linux-wpt from try job (#37294387426):

Flaky unexpected result (21)
  • OK /IndexedDB/idbtransaction-oncomplete.any.html (#42845)
    • FAIL [expected PASS] subtest: IDBTransaction - complete event

      assert_array_equals: lengths differ, expected array ["upgradeneeded", "complete", "success", "opencursor"] length 4, got ["upgradeneeded", "complete", "success"] length 3
      

  • CRASH [expected OK] /_webgl/conformance2/wasm/readpixels-2gb-in-4gb-wasm-memory.html
  • OK /beacon/beacon-basic.https.window.html (#41723)
    • FAIL [expected PASS] subtest: Payload size restriction should be accumulated: type = string

      assert_false: expected false got true
      

  • FAIL [expected PASS] /css/css-image-animation/image-animation-img-running-to-normal.html (#46567)
  • OK /dom/nodes/moveBefore/iframe-document-preserve.window.html (#43152)
    • FAIL [expected PASS] subtest: moveBefore(): cross-origin iframe is preserved: remove new parent

      assert_equals: iframe does not fire a second load event expected 1 but got 0
      

    • FAIL [expected PASS] subtest: moveBefore(): cross-origin iframe is preserved: remove self via innerHTML

      assert_equals: iframe does not fire a second load event expected 1 but got 0
      

  • CRASH [expected TIMEOUT] /html/browsers/history/the-location-interface/location_replace_session_history.html (#41896)
  • TIMEOUT [expected OK] /html/browsers/windows/consume-user-activation/window-open-multi-global.html (#47399)
    • TIMEOUT [expected FAIL] subtest: window.open() and consuming user activation with multiple globals in play

      Test timed out
      

  • TIMEOUT [expected OK] /html/semantics/embedded-content/media-elements/autoplay-allowed-by-permissions-policy.https.sub.html (#46803)
    • TIMEOUT [expected PASS] subtest: Permissions-Policy header: autoplay * allows same-origin iframes.

      Test timed out
      

  • TIMEOUT [expected OK] /html/semantics/embedded-content/media-elements/src_object_blob.html (#40340)
    • TIMEOUT [expected PASS] subtest: HTMLMediaElement.srcObject blob

      Test timed out
      

  • OK [expected CRASH] /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-reload-location-reload.html (#32595)
    • FAIL [expected PASS] subtest: Reloading iframe loading='lazy' before it is loaded: location.reload

      uncaught exception: Error: assert_equals: expected "http://web-platform.test:8000/html/semantics/embedded-content/the-iframe-element/support/blank.htm?src" but got "about:blank"
      

  • OK /html/semantics/forms/form-submission-0/jsurl-form-submit.tentative.html (#36489)
    • PASS [expected FAIL] subtest: Verifies that form submissions scheduled inside javascript: urls take precedence over the javascript: url's return value.
  • CRASH [expected ERROR] /html/semantics/forms/the-select-element/customizable-select/select-appearance-button-after-span.html
  • OK /html/semantics/scripting-1/the-script-element/execution-timing/077.html (#22139)
    • FAIL [expected PASS] subtest: adding several types of scripts through the DOM and removing some of them confuses scheduler

      assert_array_equals: expected property 1 to be "Script #1 ran" but got "Script #3 ran" (expected array ["Script #2 ran", "Script #1 ran", "Script #3 ran", "Script #4 ran"] got ["Script #2 ran", "Script #3 ran", "Script #4 ran", "Script #1 ran"])
      

  • TIMEOUT [expected OK] /html/user-activation/navigation-state-reset-sameorigin.html (#45151)
    • TIMEOUT [expected FAIL] subtest: Post-navigation state reset.

      Test timed out
      

  • OK /html/webappapis/user-prompts/print-during-unload.html (#35944)
    • PASS [expected FAIL] subtest: print() during unload
  • ERROR [expected OK] /intersection-observer/v2/animated-opacity.html (#40827)
  • OK /preload/link-header-preload-delay-onload.html (#39622)
    • FAIL [expected PASS] subtest: Makes sure that Link headers preload resources and block window.onload after resource discovery

      assert_true: expected true got false
      

  • OK /resource-timing/test_resource_timing.https.html (#25216)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img)
    • FAIL [expected PASS] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (link)

      assert_equals: expected 8.600000000000001 but got 8.6
      

  • OK /upgrade-insecure-requests/gen/sharedworker-classic-data.http-rp/upgrade/fetch.https.html
    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for fetch to cross-http-downgrade origin and downgrade redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for fetch to cross-http-downgrade origin and no-redirect redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for fetch to cross-https origin and downgrade redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for fetch to same-http-downgrade origin and downgrade redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for fetch to same-http-downgrade origin and no-redirect redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for fetch to same-https origin and downgrade redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

  • OK /upgrade-insecure-requests/gen/sharedworker-classic-data.meta/upgrade/websocket.https.html
    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for websocket to cross-ws-downgrade origin and no-redirect redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

    • FAIL [expected PASS] subtest: Upgrade-Insecure-Requests: Expects allowed for websocket to same-ws-downgrade origin and no-redirect redirection from https context.

      assert_equals: The resource request should be 'allowed'. expected "allowed" but got "blocked"
      

  • OK /visual-viewport/resize-event-order.html (#41981)
    • PASS [expected FAIL] subtest: Popup: DOMWindow resize fired before VisualViewport.
Stable unexpected results that are known to be intermittent (11)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window-resize-event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • CRASH [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/005.html (#27062)
  • TIMEOUT [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-with-user-activation-in-parent.window.html (#48205)
    • TIMEOUT [expected FAIL] subtest: Cross-origin top navigation is blocked without user activation, even if the parent has user activation

      Test timed out
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/refresh/same-document-refresh.html (#34597)
    • PASS [expected FAIL] subtest: Same-Document Referrer from Refresh
  • TIMEOUT /html/semantics/embedded-content/media-elements/autoplay-default-permissions-policy.https.sub.html (#46805)
    • TIMEOUT [expected PASS] subtest: Default "autoplay" feature policy ["self"] allows the top-level document.

      Test timed out
      

  • CRASH [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-nav-window-open.html (#32596)
  • OK /preload/prefetch-document.html (#37210)
    • FAIL [expected PASS] subtest: different-site document prefetch with 'as=document' should not be consumed

      assert_equals: expected 2 but got 1
      

  • OK /resource-timing/buffer-full-add-then-clear.html (#40819)
    • PASS [expected FAIL] subtest: Test that if the buffer is cleared after entries were added to the secondary buffer, those entries make it into the primary one
  • OK /resource-timing/buffer-full-then-increased.html (#44408)
    • FAIL [expected PASS] subtest: Test that overflowing the buffer and immediately increasing its limit does not trigger the resourcetimingbufferfull event

      assert_equals: Number of entries does not match the expected value. expected 3 but got 1
      

  • OK /touch-events/single-tap-when-touchend-listener-use-sync-xhr.html (#41175)
    • PASS [expected FAIL] subtest: Click event should be fired when touchend opens synchronous XHR
  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?16-20 (#44835)
    • PASS [expected TIMEOUT] subtest: Navigate a frame via area with javascript:-urls in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via area with javascript:-urls w/ default policy in report-only mode.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

✨ Try run (#37294387426) succeeded.

RichardTjokroutomo pushed a commit to RichardTjokroutomo/servo that referenced this pull request Oct 6, 2026
This enables testing for the experimental [container
timing](https://wpt.fyi/results/container-timing/tentative?label=experimental&label=master&aligned)
within WPT

Part of servo#46966

Signed-off-by: jasonwilliams <jase.williams@gmail.com>
@servo-highfive servo-highfive added the S-needs-rebase There are merge conflict errors. label Oct 8, 2026
@servo-highfive servo-highfive removed the S-needs-rebase There are merge conflict errors. label Oct 8, 2026
@servo-highfive servo-highfive added the S-needs-rebase There are merge conflict errors. label Oct 8, 2026
Initial start by setting experimental runtime flags and support in Node
and Base fragments. This is to reduce the cost of traversal to discover
if container timing is needed

Signed-off-by: jasonwilliams <jase.williams@gmail.com>
Signed-off-by: jasonwilliams <jase.williams@gmail.com>
Signed-off-by: jasonwilliams <jase.williams@gmail.com>
- Update paint_regions within a pending container, then merge into the
  permenent container record when the display list has finished building
- Fix up some WPT's by checking the node itself for the container root
  before starting at the parent

Signed-off-by: jasonwilliams <jase.williams@gmail.com>
`container_id` was always `root_element.id()`.
Key by the root element's `OpaqueNode` instead.

Signed-off-by: jasonwilliams <jase.williams@gmail.com>
Signed-off-by: jasonwilliams <jase.williams@gmail.com>
Instead of walking up the DOM trying to find container roots, use the
traversalState to pass roots down instead. LCP has been doing this with
text decorations etc.
We have a ContainerTimingRoots linked list which starts at the nearest
root then follows on until None. This allows us to implement
containertimingignore and pass more tests.

This should also have much improved performance, because there is no
"lookup" now which iterates every single node up the tree, we know ahead
of time where the roots are.

The identifier is now read later on in script a we have access to the
root element here anyway, we check again if the attribute is there, and
if not we skip.

Signed-off-by: jasonwilliams <jase.williams@gmail.com>
@servo-highfive servo-highfive removed the S-needs-rebase There are merge conflict errors. label Oct 8, 2026

@jdm jdm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice!

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Oct 9, 2026
@jdm
jdm added this pull request to the merge queue Oct 9, 2026
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Oct 9, 2026
Merged via the queue into servo:main with commit e1fe321 Oct 9, 2026
33 checks passed
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Oct 9, 2026
@shubhamg13

Copy link
Copy Markdown
Member

@servo-highfive monthly update

@servo-highfive

Copy link
Copy Markdown

Someone thinks this change could be added to the monthly blog post! To help with this, we need someone to answer the following questions: 😄

  1. Who is most impacted by this change: users, Servo developers, embedders, or some other group?
  2. What observable difference does this change make?
  3. What preferences (if any) need to be enabled to observe this difference?
  4. What (if any) specific URLs are affected?

If this change is part of a broader feature/project please make sure the PR description contains a Fixes: #12345 or Part of: #12345 issue reference.

Please add @servo-highfive monthly update answer when answering these questions so the bot notices your answer (or just quote this comment).

Thanks for helping us prepare the monthly blog post! ❤️

hhhwsqhhh pushed a commit to hhhwsqhhh/servo that referenced this pull request Oct 12, 2026
This enables testing for the experimental [container
timing](https://wpt.fyi/results/container-timing/tentative?label=experimental&label=master&aligned)
within WPT

Part of servo#46966

Signed-off-by: jasonwilliams <jase.williams@gmail.com>
hhhwsqhhh pushed a commit to hhhwsqhhh/servo that referenced this pull request Oct 12, 2026
Hi, I'm a spec champion for the Container Timing feature and I wanted to
add it to Servo (behind a flag).

The Container Timing API enables monitoring when annotated sections of
the DOM are displayed on screen and have finished their initial paint. I
am one of the spec authors, and i've been helping this land in both
[Chromium](https://developer.chrome.com/blog/container-timing-origin-trial)
and [Firefox](https://phabricator.services.mozilla.com/D279373)

This PR implements the [Container Timing
API](https://github.com/WICG/container-timing), following the same
layout > paint > constellation pipeline already used for LCP.

  Steps for calculating Container Timing:

1. When an element has the `containertiming` attribute, a [flag is
propagated down through its
descendants](https://github.com/jasonwilliams/servo/blob/403908fa99f47da3e18faaca88d2abdb01498cf7/components/script/dom/node/node.rs#L378-L387)
(set on the Node/fragment) so layout can cheaply tell "is this inside a
container-timing root" without walking back up the tree on every
fragment.
2. While building the display list, each painted text/image fragment
that belongs to a container-timing root [reports its viewport-clipped
size and rect to a
PaintTimingHandler](https://github.com/jasonwilliams/servo/blob/403908fa99f47da3e18faaca88d2abdb01498cf7/components/layout/display_list/mod.rs#L706-L718),
similar to how LCP candidates are collected.
3. PaintTimingHandler keeps one running record per container root (keyed
by a stable node ID, since the containertiming value itself is optional
and not unique), accumulating size/rect across paints, and tracks which
containers were actually touched since the last hand-off.
4. Layout [hands the finished per-container records to
script](https://github.com/jasonwilliams/servo/blob/403908fa99f47da3e18faaca88d2abdb01498cf7/components/layout/layout_impl.rs#L1561-L1567)
as part of the reflow result
5. once webrender confirms that epoch composited, [it sends
PaintMetricEvent::ContainerTiming(paint_time,
ids)](https://github.com/jasonwilliams/servo/blob/403908fa99f47da3e18faaca88d2abdb01498cf7/components/paint/painter.rs#L585-L589)
to script, which stamps the already-stored records with the real
paint/first-render time and queues the finished
PerformanceContainerTiming entries.

Testing:
- There are plenty of [Web Platform
Tests](https://wpt.fyi/results/container-timing/tentative?label=master&label=experimental&aligned&q=container%20timing)
for Container Timing. I have been testing the build against these as i
have been going along, 19/29 tests pass.
- There are tests for some components, such as paintedRegion

Follow ups & investigation:
- `lastPaintedElement` selection picks by traversal order, not largest
area
- [Tidy Up] The `effective_visual_size` check was [originally written
for LCP](servo#43378), but is useful for
Container Timing as well, we should make this API generic

---------

Signed-off-by: jasonwilliams <jase.williams@gmail.com>
@jasonwilliams

Copy link
Copy Markdown
Contributor Author

@servo-highfive monthly update answer

  1. Who is most impacted? Web developers, mostly people who measure site performance and build real-user monitoring (RUM) tools. It's an experimental API, so it doesn't change anything for regular users yet.
  2. What observable difference does this change make? Servo now has an experimental version of the Container Timing API. If a page adds the containertiming attribute to an element, it can use a PerformanceObserver with type: "container" to get PerformanceContainerTiming entries. Each entry says when that part of the DOM, including the text and images inside it, was painted to the screen and how much area it covers. Entries are only reported after the frame has actually been composited. It works like Largest Contentful Paint, but for any part of the page the author picks instead of just the largest element. Chromium (origin trial)
    and Firefox are also implementing it.
  3. What preferences need to be enabled? container_timing_enabled, e.g. --pref container_timing_enabled.
  4. What specific URLs are affected? None in particular. It affects any page that uses the containertiming attribute. Most of the WPT tests in container-timing/tentative now pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants