Files
zed/crates/gpui_web/examples/hello_web/index.html
T
Antonio Scandurra 2e31de1c44 gpui: Avoid main-thread waits when unlocking web task queues (#64298)
## Problem

GPUI's web dispatcher already uses `spin_send` and `spin_try_pop` on the
browser main thread. Those operations spin on `try_lock()` instead of
blocking while acquiring the queue mutex. However, that does **not**
make releasing a `parking_lot` mutex safe on that thread.

When a worker is waiting for the queue mutex, dropping the main thread's
guard follows this path:

```text
RawMutex::unlock_slow
  → parking_lot_core::unpark_one
    → lock_bucket
      → WordLock::lock_slow
        → ThreadParker::park
          → memory.atomic.wait32
```

`unpark_one` first locks the internal table used to locate sleeping
threads. If another thread holds that bucket lock, the caller can itself
park before reaching the actual wake instruction. See
[`unpark_one`](https://github.com/Amanieu/parking_lot/blob/d7828fff7b5d6327ae608e82db45f888b344449a/core/src/parking_lot.rs#L732-L737),
[`lock_bucket`](https://github.com/Amanieu/parking_lot/blob/d7828fff7b5d6327ae608e82db45f888b344449a/core/src/parking_lot.rs#L358-L366),
and [`WordLock`'s park
call](https://github.com/Amanieu/parking_lot/blob/d7828fff7b5d6327ae608e82db45f888b344449a/core/src/word_lock.rs#L163-L168).

Browsers forbid synchronous atomic waits on the main thread. Chromium
reports `Atomics.wait cannot be called in this context`; Safari can
misleadingly report an out-of-bounds memory access. Ordinary UI activity
can encounter this contention while scheduling background work; no
explicit blocking operation by the application is required.

## Why `std` fixes it

For **atomics-enabled Wasm**, Rust's standard-library mutex uses a futex
directly. Unlocking changes the mutex state with an atomic swap and, if
needed, calls `memory.atomic.notify`. Its condition-variable
notification similarly increments an atomic notification counter and
calls `memory.atomic.notify`. Neither operation acquires a secondary
waiter-table lock or performs an atomic wait.

A contended `std::Mutex::lock()` would still be unsafe on the browser
main thread, so this change **retains the existing `try_lock()` spin
loops**. Workers retain ordinary blocking acquisition and
condition-variable waiting. This relies on the Wasm backend's
implementation, not a portable guarantee that standard mutex operations
never block on every platform.

The production change only replaces the queue synchronization primitives
and adapts their APIs. The high/medium/low queues, weighted selection,
queue methods, and disconnect behavior are unchanged. Poisoned guards
are recovered to preserve the previous non-poisoning behavior. The
main-thread-only mailbox receiver also uses a standard mutex with
`try_lock()` so an unexpected acquisition cannot park it.

## Regression test and verification

One browser/Wasm stress test exchanges work and replies between the
actual browser main thread and configurable `N` Web Workers. It checks
exact-once delivery, cycles between idle and busy queues, and waits for
all workers to stop. An explicit Wasm wait is a negative control
confirming that the main thread rejects waits; a deadline outside the
page detects hangs.

- **Fix stashed:** rebuilt and ran the test against the original queue.
It failed with `Atomics.wait cannot be called in this context`, with
`WordLock::lock_slow → RawMutex::unlock_slow → spin_send` in the stack.
- **Fix restored:** rebuilt and ran the unchanged test with **8 and 16
workers**. Both passed, each exchanging **524,288 items over 128
rounds**.
- Both existing native queue tests passed. Shared-memory Wasm
compilation, Rust formatting, JavaScript syntax, and diff checks passed.
- Browser execution was verified in Chromium, not directly on an iOS
Safari device. This does not update any downstream application's pinned
GPUI dependency.

The test is available through the existing `hello_web` gallery:

```sh
node crates/gpui_web/examples/hello_web/test_queue.mjs http://127.0.0.1:8080 8
```

For the recorded runs, the fixture was built directly with the gallery's
shared-memory flags, `nightly-2026-07-01`, and matching wasm-bindgen
0.2.120, then served with COOP/COEP headers.

## Suggested .rules additions

For code running on the browser main thread, audit mutex unlock and
notification paths as well as acquisition: using `try_lock()` does not
guarantee that releasing the guard cannot park.

Release Notes:

- [GPUI] N/A
2026-09-16 13:17:03 +00:00

114 lines
5.2 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=0" />
<title>GPUI Web Examples</title>
<link data-trunk rel="rust" data-bin="hello_web" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<!-- Trunk's worker pipelines build auxiliary modules without starting them.
The gallery imports these on the main thread, not in Web Workers. -->
<link data-trunk rel="rust" data-type="worker" data-bin="hello_world" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<link data-trunk rel="rust" data-type="worker" data-bin="text" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<link data-trunk rel="rust" data-type="worker" data-bin="text_layout" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<link data-trunk rel="rust" data-type="worker" data-bin="text_wrapper" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<link data-trunk rel="rust" data-type="worker" data-bin="input" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<script data-trunk type="module" src="gallery.js"></script>
<link data-trunk rel="rust" data-type="worker" data-bin="dynamic_fonts" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<link data-trunk rel="rust" data-type="worker" data-bin="font_fallback_test" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<link data-trunk rel="rust" data-type="worker" data-bin="queue_test" data-bindgen-target="web" data-keep-debug data-wasm-opt="0" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
}
body {
font-family: system-ui, sans-serif;
color: #202030;
background: #f5f5f8;
}
main {
max-width: 48rem;
margin: 0 auto;
padding: 3rem 1.5rem;
}
h1, main > p {
margin-bottom: 1.5rem;
}
ul {
list-style: none;
display: grid;
gap: 0.75rem;
}
li a {
display: block;
padding: 1rem;
border: 1px solid #d0d0dc;
border-radius: 0.5rem;
color: inherit;
background: white;
text-decoration: none;
}
li a:hover, li a:focus-visible {
border-color: #5050a0;
}
li span {
display: block;
margin-top: 0.25rem;
color: #555566;
}
#back {
position: fixed;
right: 0.75rem;
bottom: 0.75rem;
z-index: 1;
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
color: #202030;
background: #ffffffee;
}
#status {
position: fixed;
top: 1rem;
left: 1rem;
right: 1rem;
z-index: 1;
padding: 1rem;
background: white;
white-space: pre-wrap;
}
canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
outline: none;
-webkit-user-select: none;
user-select: none;
}
</style>
</head>
<body>
<main id="gallery">
<h1>GPUI Web Examples</h1>
<p>Each example runs in its own WebAssembly instance. Choose an example to open it.</p>
<ul>
<li><a href="/hello-world" data-module="hello_world"><strong>Hello world</strong><span>Basic GPUI layout and styling.</span></a></li>
<li><a href="/text" data-module="text"><strong>Styled text</strong><span>Typography, font sizes, and line heights.</span></a></li>
<li><a href="/text-layout" data-module="text_layout"><strong>Text layout</strong><span>Alignment, decorations, and font weights.</span></a></li>
<li><a href="/text-wrapper" data-module="text_wrapper"><strong>Text wrapping</strong><span>Wrapping and truncation, including CJK sample text.</span></a></li>
<li><a href="/input" data-module="input"><strong>Text input</strong><span>Typing, cursor movement, and selections.</span></a></li>
<li><a href="/dynamic-fonts" data-module="dynamic_fonts"><strong>Dynamic fonts</strong><span>Download a font when missing glyphs are reported.</span></a></li>
<li><a href="/prime-sieve" data-module="hello_web"><strong>Prime sieve</strong><span>Background tasks and progress updates.</span></a></li>
</ul>
</main>
<a id="back" href="/" hidden>All examples</a>
<p id="status" role="status" hidden></p>
</body>
</html>