Skip to content

script: Re-initialize fulfilled document.fonts.ready when a web font starts loading - #45382

Merged
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:recreate-document-fonts-ready
Jun 4, 2026
Merged

script: Re-initialize fulfilled document.fonts.ready when a web font starts loading#45382
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:recreate-document-fonts-ready

Conversation

@mrobinson

@mrobinson mrobinson commented Jun 4, 2026

Copy link
Copy Markdown
Member

This change adds an initial implementation of the "switch the
FontFaceSet to loading" steps from the CSS Font Loading specification.
The only thing it does is to reset document.fonts.ready so that it can
be waited on again. In addition, this is called when the addition of a
stylesheet or the DOM modification of a FontFace or FontFaceSet
triggers more font loading. The goal here is to reduce flakiness in the
CSS font loading tests.

Testing: This causes some tests to pass and one to fail (due to missing preload support -- so this is progression actually). The main goal here though is to remove a lot of flakiness.
Fixes: #37467.
Fixes: #40929.
Fixes: #32732.
Fixes: #34624.

…t starts loading

This change adds an initial implementation of the "switch the
FontFaceSet to loading" steps from the CSS Font Loading specification.
The only thing it does is to reset `document.fonts.ready` so that it can
be waited on again. In addition, this is called when the addition of a
stylesheet or the DOM modification of a `FontFace` or `FontFaceSet`
triggers more font loading. The goal here is to reduce flakiness in the
CSS font loading tests.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
@mrobinson
mrobinson force-pushed the recreate-document-fonts-ready branch from 3900339 to 0d1e7dc Compare June 4, 2026 17:05
@mrobinson mrobinson added the T-linux-wpt Do a try run of the WPT label Jun 4, 2026
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

🔨 Triggering try run (#26967146250) for Linux (WPT)

@mrobinson

mrobinson commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

This change is quite large, but most of it is just threading JSContext through methods and replacing old CanGc usages.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

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

Flaky unexpected result (60)
  • TIMEOUT /FileAPI/url/url-in-tags-revoke.window.html (#19978)
    • PASS [expected TIMEOUT] subtest: Fetching a blob URL immediately before revoking it works in &lt;script&gt; tags.
  • OK /IndexedDB/idbdatabase_deleteObjectStore.any.html (#43823)
    • PASS [expected FAIL] subtest: Deleted object store's name should be removed from database's list. Attempting to use a deleted IDBObjectStore should throw an InvalidStateError
  • ERROR [expected OK] /_mozilla/mozilla/focus-scroll-when-visible.html
  • OK /_webgl/conformance/textures/misc/texture-upload-size.html (#21770)
    • FAIL [expected PASS] subtest: WebGL test #45

      assert_true: Texture was smaller than the expected size 2x2 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #47

      assert_true: getError expected: INVALID_VALUE. Was NO_ERROR : when calling texSubImage2D with the same texture upload with offset 1, 1 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #49

      assert_true: Texture was smaller than the expected size 2x2 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #51

      assert_true: getError expected: INVALID_VALUE. Was NO_ERROR : when calling texSubImage2D with the same texture upload with offset 1, 1 expected true got false
      

    • PASS [expected FAIL] subtest: WebGL test #53
    • PASS [expected FAIL] subtest: WebGL test #55
    • PASS [expected FAIL] subtest: WebGL test #57
    • PASS [expected FAIL] subtest: WebGL test #59
    • FAIL [expected PASS] subtest: WebGL test #61

      assert_true: Texture was smaller than the expected size 2x2 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #63

      assert_true: getError expected: INVALID_VALUE. Was NO_ERROR : when calling texSubImage2D with the same texture upload with offset 1, 1 expected true got false
      

    • And 6 more unexpected results...
  • 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 = arraybuffer

      assert_false: expected false got true
      

  • OK /css/css-cascade/layer-font-face-override.html (#35935)
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 1
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 2
  • FAIL [expected PASS] /css/css-ui/appearance-checkbox-001.html
  • OK /fetch/metadata/window-open.https.sub.html (#40339)
    • FAIL [expected PASS] subtest: Cross-site window, forced, reloaded

      The operation is insecure.
      

  • CRASH [expected OK] /html/browsers/browsing-the-web/back-forward-cache/timers.html
  • OK /html/browsers/browsing-the-web/navigating-across-documents/005.html (#27062)
    • PASS [expected FAIL] subtest: Link with onclick navigation and href navigation
  • CRASH [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-without-user-activation.window.html
  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html (#20768)
    • PASS [expected FAIL] subtest: Tests that a fragment navigation in the unload handler will not block the initial navigation
  • OK /html/browsers/browsing-the-web/navigating-across-documents/refresh/same-document-refresh.html (#34597)
    • FAIL [expected PASS] subtest: Same-Document Referrer from Refresh

      assert_equals: original page loads expected "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section#section"
      

  • CRASH [expected OK] /html/browsers/browsing-the-web/unloading-documents/beforeunload-canceling.html
  • OK /html/browsers/history/the-history-interface/traverse_the_history_4.html (#21383)
    • FAIL [expected PASS] subtest: Multiple history traversals, last would be aborted

      assert_array_equals: Pages opened during history navigation expected property 1 to be 5 but got 3 (expected array [6, 5] got [6, 3])
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_5.html (#21383)
    • FAIL [expected PASS] subtest: Multiple history traversals, last would be aborted

      assert_array_equals: Pages opened during history navigation expected property 1 to be 5 but got 3 (expected array [6, 5] got [6, 3])
      

  • OK /html/browsers/windows/embedded-opener-remove-frame.html (#23867)
    • FAIL [expected PASS] subtest: opener of discarded auxiliary browsing context

      assert_object_equals: property "get" expected function "function opener() {
          [native code]
      }" got function "function opener() {
          [native code]
      }"
      

  • OK [expected TIMEOUT] /html/semantics/embedded-content/media-elements/src_object_blob.html (#40340)
    • PASS [expected TIMEOUT] subtest: HTMLMediaElement.srcObject blob
  • OK /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-nav-location-replace.html (#32604)
    • FAIL [expected PASS] subtest: Navigating iframe loading='lazy' before it is loaded: location.replace

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

  • 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
  • CRASH [expected FAIL] /html/semantics/interestfor/interestfor-plain-inline-element.tentative.html
  • OK /infrastructure/testdriver/click_iframe.html
    • FAIL [expected PASS] subtest: TestDriver click on a document in an iframe

      Unhandled rejection: assert_unreached: click failed Reached unreachable code
      

  • TIMEOUT [expected OK] /infrastructure/testdriver/click_nested.html (#43887)
    • NOTRUN [expected FAIL] subtest: TestDriver click method with multiple windows and nested iframe
  • CRASH [expected OK] /resource-timing/render-blocking-status-link.html (#41664)
  • OK /resource-timing/test_resource_timing.html (#25720)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (link)
  • TIMEOUT /trusted-types/trusted-types-navigation.html?06-10 (#37920)
    • PASS [expected FAIL] subtest: Navigate a frame via anchor with javascript:-urls in report-only mode.
  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?11-15 (#44834)
    • PASS [expected TIMEOUT] subtest: Navigate a window via area with javascript:-urls in report-only mode.
  • TIMEOUT [expected OK] /trusted-types/trusted-types-navigation.html?16-20 (#44835)
    • TIMEOUT [expected PASS] subtest: Navigate a frame via area with javascript:-urls w/ default policy in enforcing mode.

      Test timed out
      

    • NOTRUN [expected PASS] subtest: Navigate a frame via area with javascript:-urls in report-only mode.
    • NOTRUN [expected PASS] subtest: Navigate a frame via area with javascript:-urls w/ default policy in report-only mode.
  • OK [expected TIMEOUT] /webdriver/tests/classic/perform_actions/navigation.py (#38822)
  • CRASH [expected OK] /workers/Worker-constructor-proto.any.worker.html
  • CRASH [expected OK] /workers/Worker-formdata.any.worker.html
  • CRASH [expected OK] /workers/Worker-location.sub.any.worker.html
  • CRASH [expected OK] /workers/Worker-multi-port.html
  • CRASH [expected OK] /workers/WorkerLocation_hostname.htm
  • CRASH [expected OK] /workers/WorkerLocation_href.htm
  • CRASH [expected OK] /workers/WorkerLocation_search_empty.htm
  • CRASH [expected OK] /workers/WorkerNavigator.any.worker.html
  • CRASH [expected OK] /workers/constructors/SharedWorker/Infinity-arguments.html
  • CRASH [expected OK] /workers/constructors/SharedWorker/same-origin.html
  • CRASH [expected OK] /workers/constructors/SharedWorker/setting-port-members.html
  • CRASH [expected OK] /workers/constructors/SharedWorker/unresolvable-url.html
  • CRASH [expected OK] /workers/constructors/Worker/ctor-null.html
  • CRASH [expected OK] /workers/constructors/Worker/ctor-undefined.html
  • CRASH [expected OK] /workers/examples/fetch_tests_from_worker.html
  • CRASH [expected OK] /workers/examples/general.worker.html
  • CRASH [expected ERROR] /workers/importscripts_mime.any.sharedworker.html
  • CRASH [expected OK] /workers/importscripts_mime_local.any.worker.html
  • CRASH [expected OK] /workers/interfaces/WorkerGlobalScope/location/setting-members.html
  • CRASH [expected OK] /workers/interfaces/WorkerGlobalScope/onerror/handled.html
  • CRASH [expected OK] /workers/interfaces/WorkerUtils/WindowTimers/004.html
  • CRASH [expected ERROR] /workers/interfaces/WorkerUtils/importScripts/report-error-redirect-to-cross-origin.sub.any.sharedworker.html
  • CRASH [expected OK] /workers/interfaces/WorkerUtils/navigator/003.html
  • CRASH [expected OK] /workers/interfaces/WorkerUtils/navigator/008.worker.html
  • CRASH [expected OK] /workers/modules/shared-worker-import-failure.html
  • CRASH [expected OK] /xhr/XMLHttpRequest-withCredentials.any.html
  • CRASH [expected OK] /xhr/access-control-basic-allow-preflight-cache.any.html
  • CRASH [expected OK] /xhr/access-control-expose-headers-on-redirect.html
  • CRASH [expected OK] /xhr/getallresponseheaders.htm
Stable unexpected results that are known to be intermittent (23)
  • TIMEOUT /content-security-policy/inheritance/auxiliary-blank-document.html (#44821)
    • PASS [expected FAIL] subtest: For transient doc, violation event occurs in auxiliary, not opener window.
  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted serif (drawing text in a canvas)

      assert_equals: quoted serif matches  @font-face rule expected 125 but got 40
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted fantasy (drawing text in a canvas)

      assert_equals: quoted fantasy matches  @font-face rule expected 125 but got 40
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(kai) (drawing text in a canvas)

      assert_equals: quoted generic(kai) matches  @font-face rule expected 125 but got 40
      

    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(khmer-mul) (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted ui-monospace (drawing text in a canvas)
  • OK [expected ERROR] /fetch/fetch-later/quota/same-origin-iframe/multiple-iframes.https.window.html (#35176)
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html (#28691)
    • PASS [expected FAIL] subtest: load event does not fire on window.open('about:blank')
  • TIMEOUT [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit.html (#44098)
    • TIMEOUT [expected FAIL] subtest: Replace before load, triggered by formElement.requestSubmit()

      Test timed out
      

  • OK [expected TIMEOUT] /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit-button-click.html (#44099)
    • FAIL [expected TIMEOUT] subtest: Replace before load, triggered by submitButton.click()

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?thereplacement=" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html?pipe=sub(none)&amp;code=%0A%20%20%20%20const%20form%20%3D%20document.createElement(%22form%22)%3B%0A%20%20%20%20form.action%20%3D%20%22%2Fcommon%2Fblank.html%22%3B%0A%0A%20%20%20%20const%20input%20%3D%20document.createElement(%22input%22)%3B%0A%20%20%20%20input.type%20%3D%20%22hidden%22%3B%0A%20%20%20%20input.name%20%3D%20%22thereplacement%22%3B%0A%20%20%20%20form.append(input)%3B%0A%0A%20%20%20%20const%20button%20%3D%20document.createElement(%22button%22)%3B%0A%20%20%20%20button.type%20%3D%20%22submit%22%3B%0A%20%20%20%20form.append(button)%3B%0A%0A%20%20%20%20document.currentScript.before(form)%3B%0A%20%20%20%20button.click()%3B%0A%20%20"
      

  • TIMEOUT [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit.html (#44028)
  • TIMEOUT [expected OK] /html/browsers/the-windowproxy-exotic-object/windowproxy-prevent-extensions.html (#45376)
  • OK [expected TIMEOUT] /html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.html (#30970)
  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/update-the-rendering.html (#24145)
    • FAIL [expected TIMEOUT] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      assert_array_equals: animationFrame lengths differ, expected array ["autofocus", "scroll", "animationFrame"] length 3, got ["animationFrame"] length 1
      

  • OK /html/semantics/scripting-1/the-script-element/module/choice-of-error-1.html (#44058)
    • FAIL [expected PASS] subtest: Parse errors in different files should be reported depending on different roots

      assert_not_equals: two different parse errors should be reported got disallowed value object "SyntaxError: expected expression, got '%'"
      

  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.html (#33948)
    • PASS [expected FAIL] subtest: Different blob URLs pointing to the same blob resolve to different modules
  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.worker-module.html (#43510)
    • PASS [expected FAIL] subtest: Different blob URLs pointing to the same blob resolve to different modules
    • FAIL [expected PASS] subtest: Revoking a blob URL immediately after calling import will not fail

      promise_test: Unhandled rejection with value: object "TypeError: Module fetching failed"
      

  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.worker.html (#33909)
    • PASS [expected FAIL] subtest: Different blob URLs pointing to the same blob resolve to different modules
  • TIMEOUT [expected OK] /html/user-activation/navigation-state-reset-sameorigin.html (#45151)
    • TIMEOUT [expected PASS] subtest: Post-navigation state reset.

      Test timed out
      

  • OK /html/webappapis/user-prompts/print-during-unload.html (#35944)
    • FAIL [expected PASS] subtest: print() during unload

      assert_array_equals: expected property 1 to be "destination" but got "error: window.print is not a function" (expected array ["start", "destination"] got ["start", "error: window.print is not a function"])
      

  • FAIL [expected PASS] /png/apng/acTL-plays-one.html (#41218)
  • OK /resource-timing/buffer-full-add-then-clear.html (#40819)
    • FAIL [expected PASS] subtest: Test that if the buffer is cleared after entries were added to the secondary buffer, those entries make it into the primary one

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

  • 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 /resource-timing/test_resource_timing.https.html (#25216)
    • FAIL [expected PASS] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (xmlhttprequest)

      assert_equals: expected 43.25 but got 43.24
      

  • OK /sanitizer-api/sanitizer-inert-document.html (#45260)
    • PASS [expected FAIL] subtest: Test whether setHTML loads the image.
    • FAIL [expected PASS] subtest: Test whether setHTMLUnsafe loads the image.

      promise_test: Unhandled rejection with value: "The server observed a request. It shouldn't have."
      

  • TIMEOUT /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • TIMEOUT [expected PASS] subtest: Navigate a frame via form-submission with javascript:-urls in report-only mode.

      Test timed out
      

    • NOTRUN [expected TIMEOUT] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.
  • TIMEOUT [expected OK] /webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.html (#29053)
    • TIMEOUT [expected FAIL] subtest: StorageKey: test 3P about:blank window opened from a 3P iframe

      Test timed out
      

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

✨ Try run (#26967146250) succeeded.

@mrobinson
mrobinson requested a review from simonwuelker June 4, 2026 17:42
@mrobinson
mrobinson marked this pull request as ready for review June 4, 2026 17:42
@mrobinson
mrobinson requested a review from gterzian as a code owner June 4, 2026 17:42
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Jun 4, 2026
Comment on lines +649 to +655
// See <https://github.com/w3c/csswg-drafts/issues/13235>:
// All browsers switch the FontFaceSet to loading, but this is currently missing
// from the specification.
if let Some(font_face_set) = self.font_face_set.get() {
font_face_set.handle_font_face_status_changed(cx, self);
}

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.

Nice find.

/// Fulfill the font ready promise, returning true if it was not already fulfilled beforehand.
pub(crate) fn fulfill_ready_promise_if_needed(&self, cx: &mut JSContext) -> bool {
if self.promise.is_fulfilled() {
let promise = self.promise.borrow().clone();

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.

Why do we need to clone the promise here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I cloned the promise here to defensively avoid a potential borrow hazard during the promise fulfillment that follows. It's hard to reason about re-entrancy here, but I wanted to be sure.

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Jun 4, 2026
@mrobinson

Copy link
Copy Markdown
Member Author

@simonwuelker Thanks for the review!

@mrobinson
mrobinson added this pull request to the merge queue Jun 4, 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 Jun 4, 2026
Merged via the queue into servo:main with commit 357cfa5 Jun 4, 2026
72 checks passed
@mrobinson
mrobinson deleted the recreate-document-fonts-ready branch June 4, 2026 20:59
@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 Jun 4, 2026
niyabits pushed a commit to niyabits/servo that referenced this pull request Jun 8, 2026
…t starts loading (servo#45382)

This change adds an initial implementation of the "switch the
FontFaceSet to loading" steps from the CSS Font Loading specification.
The only thing it does is to reset `document.fonts.ready` so that it can
be waited on again. In addition, this is called when the addition of a
stylesheet or the DOM modification of a `FontFace` or `FontFaceSet`
triggers more font loading. The goal here is to reduce flakiness in the
CSS font loading tests.

Testing: This causes some tests to pass and one to fail (due to missing
preload support -- so this is progression actually). The main goal here
though is to remove a lot of flakiness.
Fixes: servo#37467.
Fixes: servo#40929.
Fixes: servo#32732.
Fixes: servo#34624.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants