Skip to content

feat(blitz-dom): expose stylo thread count via DocumentConfig#436

Closed
HenriqueAnzoategui wants to merge 1 commit into
DioxusLabs:mainfrom
Kigo-Digital:kigo/stylo-thread-config
Closed

feat(blitz-dom): expose stylo thread count via DocumentConfig#436
HenriqueAnzoategui wants to merge 1 commit into
DioxusLabs:mainfrom
Kigo-Digital:kigo/stylo-thread-config

Conversation

@HenriqueAnzoategui
Copy link
Copy Markdown
Contributor

Summary

Stylo's STYLE_THREAD_POOL is a process-global LazyLock<ThreadPool>. Each rayon worker thread in that pool holds a thread-local AtomicRefCell<SharingCache> borrowed mutably for the entire duration of a style traversal landing on that worker (see sharing/mod.rs:563-619). The architectural contract is "only one style traversal at a time touches the global pool" — true for Servo and Gecko (one layout thread per Document) but false for any embedder driving multiple Documents concurrently. When a second traversal lands on a worker mid-traversal of the first, it tries borrow_mut on an already-borrowed cell and panics with already mutably borrowed.

This PR adds an additive escape hatch on DocumentConfig so embedders can pin style traversal to a single thread (the calling thread, where each Document's stylo work has uniquely-owned thread-locals). It does not change blitz's default behaviour.

Repro

use std::thread;
use blitz_dom::{BaseDocument, DocumentConfig};

let handles: Vec<_> = (0..8).map(|_| thread::spawn(|| {
    let _doc = BaseDocument::new(DocumentConfig::default());
    // ... drive a style traversal on _doc ...
})).collect();
for h in handles { h.join().unwrap(); }
// → some thread panics with `already mutably borrowed`

With this PR:

let config = DocumentConfig {
    stylo_thread_count: Some(1),
    ..Default::default()
};
// ...
// → no panic; per-render parallelism within stylo is disabled but
//   parallelism across renders is preserved.

Changes

  • packages/blitz-dom/src/config.rs
    • New pub stylo_thread_count: Option<i32> field on DocumentConfig, with rustdoc covering the rationale and the "one-shot" gotcha (STYLE_THREAD_POOL is a LazyLock; the first Document::new call's value wins for the whole process).
    • 1 inline unit test guarding default_stylo_thread_count_is_none (regression-pin: None is load-bearing for the historical default).
  • packages/blitz-dom/src/document.rs:353
    • Change set_pref!("layout.threads", -1) to set_pref!("layout.threads", config.stylo_thread_count.unwrap_or(-1)). unwrap_or(-1) preserves the historical auto-detect default for every existing caller.

Compatibility

Fully additive: new field on a struct that already derives Default, so every existing call site is unaffected. Default value None preserves the historical -1 (auto, capped at 6) behaviour.

Test plan for reviewers

  • Existing tests pass (no regressions in cargo test --workspace).
  • cargo fmt --check, cargo clippy --workspace -- -D warnings, RUSTDOCFLAGS="-D warnings" cargo doc -p blitz-dom --no-deps, cargo build --workspace on MSRV 1.92 — all unchanged from pre-PR baseline.
  • Optional: run the repro above against main (panics) vs. this PR with stylo_thread_count: Some(1) (no panic).

Stylo's `STYLE_THREAD_POOL` is a process-global LazyLock-initialised
rayon pool. Style traversal on each worker holds a thread-local
`AtomicRefCell<SharingCache>` borrowed mutably for the traversal's
lifetime, so if two concurrent traversals (from separate `Document`s
on separate OS threads) land on the same rayon worker, the second
panics with `already mutably borrowed`.

Embedders that drive many `Document`s concurrently (e.g. servers
rendering documents per request via `tokio::task::spawn_blocking`)
need to disable parallel traversal to stay correct. The previous
hardcoded `layout.threads = -1` (auto) gave them no way to do so.

Add an additive `stylo_thread_count: Option<i32>` field to
`DocumentConfig`. `None` preserves the historical default; embedders
opt into serial traversal with `Some(1)`. Documents the one-shot
nature of `STYLE_THREAD_POOL` initialisation in the rustdoc so callers
aren't surprised that only the first `Document` constructed in the
process gets to choose the value.
@HenriqueAnzoategui HenriqueAnzoategui force-pushed the kigo/stylo-thread-config branch from 72757eb to eb35068 Compare May 12, 2026 18:17
@nicoburns
Copy link
Copy Markdown
Member

I think I'm going to use the approach in #437 instead, as it has the nice property of being per-Document.

@nicoburns nicoburns closed this May 12, 2026
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.

2 participants