Skip to content

fix(browser): warm only the font sources the renderer will consider - #748

Open
ntdatt812 wants to merge 1 commit into
h4ckf0r0day:mainfrom
ntdatt812:fix/667-font-face-src-warmup
Open

fix(browser): warm only the font sources the renderer will consider#748
ntdatt812 wants to merge 1 commit into
h4ckf0r0day:mainfrom
ntdatt812:fix/667-font-face-src-warmup

Conversation

@ntdatt812

Copy link
Copy Markdown

Fixes #667.

What changed

prepare_screenshot_resources collected its work with css_resource_urls, which returns every url( in the stylesheet text. A @font-face src list is a priority order, not a set of resources, so the IE8 form in the report produced six downloads for one font.

The useful part of this is that the engine already resolves that list correctly. Only the warmup did not. obscura-render builds its font rules as:

let sources: Vec<_> = font_face_urls(face)
    .filter(|src| font_source_may_be_supported(src))

and those two encode exactly the two rules the warmup was missing:

  • font_face_declaration, which font_face_urls reads src through, ends in .last(). A rule carrying several src descriptors uses the final one. That is the cascade, and it is what the src: url(.eot); src: url(...) idiom relies on. font_face_uses_the_last_duplicate_src_descriptor already pins it.
  • font_source_may_be_supported drops .eot and .svg after stripping the query and fragment, so probe.eot?#iefix and probe.svg#probe go too.

So on the reported fixture the renderer considers three candidates and the warmup fetched six, including the two the renderer had already ruled out.

css_font_face_rule applies both rules and reports the block as consumed. That is the same shape as the @import skip a few lines above it in the same scanner, which exists for the same reason: the construct belongs to a different fetch path, so scanning it generically issues the wrong requests.

The url(...) recording moved into push_css_url, shared by the generic scan and the font path, so the two cannot drift apart on quoting, fragments, data: or an unresolved var().

An unterminated block returns None and falls back to the generic scanner, matching css_import_rule_len: warming too much is better than dropping the rest of the stylesheet.

Why three and not one, which is what Chrome fetches

Chrome fetches one because it stops at the first candidate it can use. Here fetch_and_decode_font walks the list and tries the next when a decode fails, which is what makes font_face_uses_the_first_decodable_source pass. Warming only the first would leave that fallback fetching cold on exactly the pages that need it, so this PR narrows the warmup to what the renderer will consider, not to what it will end up using. Going to one is a change to the decode strategy rather than to the warmup, and belongs in its own change if you want it.

This also does not touch #662, the other defect in the same function: collecting from the stylesheet text rather than from the render tree. That one is larger and independent, and this does not block it.

Validation

Four tests next to the existing css_resource_discovery_ignores_strings_comments_data_and_fragments, which still passes unchanged.

The first is the reported fixture. Red before the fix, and the red is the report's own number:

left:  ["…/probe.eot", "…/probe.eot?", "…/probe.woff2", "…/probe.woff", "…/probe.ttf", "…/probe.svg"]
right: ["…/probe.woff2", "…/probe.woff", "…/probe.ttf"]

The other three cover local() and data: sources, a } inside a string in the block, and an unterminated block. Two of them pass on main as well, because the generic scanner already ignores local( and data:; they are here to pin the block-skip boundary, which is new code and would silently eat the rule after the block if it were wrong.

5 tests run: 5 passed

Full four-configuration sequence from CONTRIBUTING, in a rust:latest container so the result matches CI rather than my Windows host:

Step Result
cargo build --release -p obscura-cli --bins --features render ok
cargo nextest run --release --features render --no-fail-fast 1510 passed, 4 skipped
cargo build --release -p obscura-cli --bins --no-default-features ok
cargo nextest run --release -p obscura --no-fail-fast 23 passed
cargo nextest run --release --workspace --exclude obscura --exclude obscura-render --no-default-features --no-fail-fast 761 passed, 3 skipped
cargo check --release -p obscura-render --no-default-features ok

I have not run cargo fmt over the tree, per CONTRIBUTING. cargo fmt --check -p obscura-browser reports 33 pre-existing diffs, at page.rs lines 1178 through 6936 and in context.rs and pdf.rs; this PR touches 530-781 and 4392-4474, so none of them are mine.

Rendering

No output change, and no fixture is needed: this removes requests for bytes the renderer never decoded. The three sources it does warm are the three paint.rs would have selected from the same rule, so the font that ends up used is the same one. The render-feature suite is unchanged at 1510 passing, including the @font-face selection tests in obscura-render that own this behaviour.

Performance

This is a reduction. On the reported fixture the screenshot warmup issues three requests where it issued six, and the two .eot fetches it drops are the ones most likely to be large and useless. Nothing is added to a hot path: the new scan runs once per stylesheet, over a block the previous code was already walking character by character, and it walks it once rather than leaving every url( to the generic loop.

The saving is bounded by how many sources a page's @font-face rules declare, so a page using a single woff2 sees no change at all.

Checklist

  • The change is focused and does not remove existing behavior without justification.
  • Tests cover the failure or feature.
  • Existing tests pass, including render and no-render configurations when affected.
  • I checked for CPU, latency, and memory regressions.
  • Public API or user-facing behavior changes are documented.

prepare_screenshot_resources collected its work with css_resource_urls,
which returns every url() in the stylesheet text. A @font-face src list
is a priority order, not a set, so the common IE8 form produced six
downloads for one font.

The renderer already resolves that list correctly. font_face_declaration
in obscura-render ends in .last(), so a rule with several src descriptors
uses the final one, and font_source_may_be_supported drops .eot and .svg
after stripping the query and fragment. The warmup went through neither,
so it fetched bytes the renderer had already ruled out.

css_font_face_rule applies both rules and reports the block as consumed,
the same shape as the existing @import skip in the same scanner. On the
reported fixture that is six fetches down to three.

The url recording moves into push_css_url so the generic scan and the
font path cannot disagree about quoting, fragments, data: or var().

An unterminated block returns None and is left to the generic scanner,
matching css_import_rule_len: warming too much is better than dropping
the rest of the stylesheet.

Fixes h4ckf0r0day#667.
@ntdatt812

Copy link
Copy Markdown
Author

Obstacle course, for the CONTRIBUTING pre-PR item a unit test cannot cover. Companion repo at the ref your CI pins (6ebac82), same host, base and candidate back to back:

revision correctness median latency
main (c138019) 32/33 3016.7ms
this branch 32/33 3020.5ms

No delta. The one failure is observer-intersection, which fails identically on untouched main here and is already tracked as #671, so it is not this branch. It is deterministic rather than flaky on this host: re-running that stage alone with --runs 3 --warmup 1 reproduces it exactly.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The screenshot warmup fetches every entry in a font-face list, not one

1 participant