Skip to content

Commit 7a2362a

Browse files
committed
perf(wasm-gui): diagnose boot is notify-gap-bound (dispatch idle) + bidirectional inline peer-notify pairing (net.accept + net.server_accept)
Phase-1 de-risk overturned the plan premise: the single dispatch pump is 99.7% IDLE in both successful and collapsed boots, so a thread-pool is refuted. The boot is bound by peerWait (~8.3ms/hop x thousands of X round-trips); HOPPROF shows the wake+delivery path is 8us, so the 8.3ms is a NOTIFY GAP — peers wake on their ~8ms poll clamp instead of on incoming data (Xvfb 96% deadline-wakes). - SECURE_EXEC_INLINE_PEER_NOTIFY reverse direction: a per-guest-path FIFO so net.connect stashes the client's readiness and the server's accept pairs the accepted socket back to it, so the server's reply wakes the client inline. Wired into BOTH accept arms — net.accept AND net.server_accept (wasm guests accept via net.server_accept, so the fix must live there; the net.accept arm alone was dead for wasm). Proven: xfdesktop deadline 78%->34%, xfwm4 79%->51%. - Plumb SECURE_EXEC_RPC_WATCHDOG_MS/_DUMP_MS + SECURE_EXEC_RPCPROF profiling. - All gated OFF by default. End-to-end boot A/B still pending (blocked by shared-workspace build-env corruption; see plan doc).
1 parent 36f7cf8 commit 7a2362a

5 files changed

Lines changed: 224 additions & 7 deletions

File tree

crates/sidecar/src/execution.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14537,6 +14537,58 @@ fn lookup_unix_listener_readiness(
1453714537
paired
1453814538
}
1453914539

14540+
/// ★ Lever 1, REVERSE direction. Only `client -> server` writes were paired (via the listener registry above),
14541+
/// so the SERVER's reply (`server -> client`) still woke the client up to a poll-clamp (~8ms) late — the
14542+
/// dominant X round-trip hop. This per-guest-path FIFO lets `net.connect` stash the CLIENT's readiness and the
14543+
/// server's `net.accept` pop it, so the accepted socket's `peer_readiness` is the client and the server's write
14544+
/// wakes the client inline too. Accept order matches connect order for a listener, so a FIFO pairs them; a
14545+
/// spurious mismatch only misdirects one inline notify (harmless — the poll clamp still covers it). Keyed by the
14546+
/// GUEST socket path (both connect and the accepted socket's local_path carry it), so no host-path resolution is
14547+
/// needed at accept. No-op unless lever 1 is enabled.
14548+
#[allow(clippy::type_complexity)]
14549+
fn unix_pending_client_readiness_registry(
14550+
) -> &'static std::sync::Mutex<
14551+
std::collections::HashMap<String, std::collections::VecDeque<std::sync::Weak<crate::state::SocketReadiness>>>,
14552+
> {
14553+
static REG: std::sync::OnceLock<
14554+
std::sync::Mutex<
14555+
std::collections::HashMap<
14556+
String,
14557+
std::collections::VecDeque<std::sync::Weak<crate::state::SocketReadiness>>,
14558+
>,
14559+
>,
14560+
> = std::sync::OnceLock::new();
14561+
REG.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
14562+
}
14563+
14564+
fn push_pending_client_readiness(
14565+
guest_path: &str,
14566+
readiness: &Arc<crate::state::SocketReadiness>,
14567+
) {
14568+
if !inline_peer_notify_enabled() {
14569+
return;
14570+
}
14571+
if let Ok(mut map) = unix_pending_client_readiness_registry().lock() {
14572+
map.entry(guest_path.to_string())
14573+
.or_default()
14574+
.push_back(Arc::downgrade(readiness));
14575+
}
14576+
}
14577+
14578+
fn pop_pending_client_readiness(guest_path: &str) -> Option<Arc<crate::state::SocketReadiness>> {
14579+
if !inline_peer_notify_enabled() {
14580+
return None;
14581+
}
14582+
let mut map = unix_pending_client_readiness_registry().lock().ok()?;
14583+
let q = map.get_mut(guest_path)?;
14584+
while let Some(weak) = q.pop_front() {
14585+
if let Some(readiness) = weak.upgrade() {
14586+
return Some(readiness);
14587+
}
14588+
}
14589+
None
14590+
}
14591+
1454014592
impl secure_exec_execution::InlineNetDrain for UnixInlineNetDrain {
1454114593
fn try_poll(&self, socket_ids: &[String], single: bool) -> Option<Value> {
1454214594
// No registry (e.g. a worker-thread drain) => net.poll always routes to
@@ -21211,6 +21263,9 @@ where
2121121263
// ★ Lever 1: pair this client socket with the server listening at host_path, so our writes
2121221264
// wake the server inline (no-op unless the feature is enabled / no server found).
2121321265
socket.peer_readiness = lookup_unix_listener_readiness(&host_path);
21266+
// ★ Lever 1 reverse: stash OUR readiness so the server's net.accept can pair its accepted
21267+
// socket back to us — so the server's REPLY wakes us inline too (the dominant hop).
21268+
push_pending_client_readiness(&guest_path, &process.socket_readiness);
2121421269
let socket_id = process.allocate_unix_socket_id();
2121521270
process.register_inline_unix_socket(&socket_id, &socket);
2121621271
process.unix_sockets.insert(socket_id.clone(), socket);
@@ -22095,13 +22150,19 @@ where
2209522150
"message": error.to_string(),
2209622151
}));
2209722152
}
22098-
let socket = ActiveUnixSocket::from_stream(
22153+
let mut socket = ActiveUnixSocket::from_stream(
2209922154
pending.stream,
2210022155
Some(listener_id.to_string()),
2210122156
pending.local_path.clone(),
2210222157
pending.remote_path.clone(),
2210322158
Arc::clone(&process.socket_readiness),
2210422159
)?;
22160+
// ★ Lever 1 reverse: pair this accepted (server-side) socket back to the client that
22161+
// connected, so the server's writes wake the client inline (keyed by the guest listen path,
22162+
// which is the client's connect path). No-op unless lever 1 is enabled.
22163+
if let Some(ref local_path) = pending.local_path {
22164+
socket.peer_readiness = pop_pending_client_readiness(&normalize_path(local_path));
22165+
}
2210522166
let socket_id = process.allocate_unix_socket_id();
2210622167
if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
2210722168
listener.register_connection(&socket_id);
@@ -22220,13 +22281,24 @@ where
2222022281
"localPath": pending.local_path.clone(),
2222122282
"remotePath": pending.remote_path.clone(),
2222222283
});
22223-
let socket = ActiveUnixSocket::from_stream(
22284+
// ★ Lever 1 reverse: the guest accepts via net.server_accept (NOT net.accept), so this is
22285+
// the arm that must pair the accepted (server-side) socket back to the connecting client.
22286+
// Without it every server→client REPLY write is on an UNPAIRED socket (peer_readiness=None)
22287+
// and wakes the client only up to a poll-clamp late — the dominant X round-trip hop (Xvfb's
22288+
// replies). Keyed by the guest listen path (= the client's connect path), same as net.accept.
22289+
// No-op unless lever 1 is enabled (pop returns None).
22290+
let accept_local_path = pending.local_path.clone();
22291+
let mut socket = ActiveUnixSocket::from_stream(
2222422292
pending.stream,
2222522293
Some(listener_id.to_string()),
2222622294
pending.local_path,
2222722295
pending.remote_path,
2222822296
Arc::clone(&process.socket_readiness),
2222922297
)?;
22298+
if let Some(ref local_path) = accept_local_path {
22299+
socket.peer_readiness =
22300+
pop_pending_client_readiness(&normalize_path(local_path));
22301+
}
2223022302
let socket_id = process.allocate_unix_socket_id();
2223122303
if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
2223222304
listener.register_connection(&socket_id);

experiments/wasm-gui/PARALLEL-SERVICING-PLAN.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,91 @@ threads with NO `sx-vmsvc-*` threads; only after the plumbing fix do 20+ `sx-vms
221221
- **Open decision (needs owner input):** keep `SX_PARALLEL_VMS` OFF and pivot the ≤15s effort to the real
222222
bottleneck — Xvfb render / framebuffer-write throughput (the SAB dataBuffer lever, see the M8.6 futex-storm
223223
memory) — OR run a clean-machine A/B first to be certain per-VM servicing is a net regression before shelving it.
224+
225+
### 2026-07-05 — RESOLVED: controlled A/B confirms per-VM = regression; per-VM stays OFF; Increment 3 shipped as tunables
226+
- **Interleaved A/B (PV=1 vs PV=0 alternating, same load window, RUNS=1 ×5 pairs):** PV=1 = **0/5 FULL, 5/5
227+
identical panel-only (4.8%)**; PV=0 = **4/5 FULL @~52s** (1 load-induced black). The perfectly-systematic
228+
panel-only under PV=1 vs the control's random variance is conclusive: **per-VM servicing is a net regression
229+
for the desktop boot.** The Xvfb single-threaded render is the binding constraint, not dispatch pickup latency.
230+
`SX_PARALLEL_VMS` stays **OFF by default** (code default already false; the feature is preserved for
231+
many-independent-VM workloads, just not the shared-Xvfb desktop).
232+
- **Increment 3 disposition:** the boot is inherently fragile to external scheduling jitter (single-threaded host
233+
broker) — every compressed launch config (C1, WM-only, "modest") collapsed to all-black under the concurrent
234+
external load (other sessions: agentos-sidecars + rustc) that the *generous* defaults survived. A validated
235+
faster DEFAULT needs a quiet machine. So Increment 3 ships the **mechanism** (env-tunable infra/WM/stagger/
236+
settle + the harness plumbing fix) with **robust defaults**; the aggressive compression toward ≤15s is left to
237+
the tunables + a quiet-machine window. **≤15s is not achievable via launch orchestration alone** — it is gated
238+
on the Xvfb render/framebuffer-write throughput lever (the real remaining work).
239+
- **Deadlock fix verified:** the `configure_vm` reentrant re-lock is gone (net_poll_suite no longer panics
240+
`[REENTRANT-VM-LOCK]`; it now reaches a pre-existing fake-wasm-fixture `start_execution` failure, unrelated to
241+
this work). 85 lib + all kernel + 87/88 service integration tests pass.
242+
- **Landed on `wasm-gui-desktop` (PR #104):** `rumlrwpk` (2c-2: per-VM servicing gated OFF + VFS Send refactor +
243+
deadlock fix + test-binary repair) and `yptupqtv` (Increment 3: tunable launch orchestration + plumbing fix).
244+
245+
### 2026-07-05 — Xvfb-throughput lever (T1 SAB fb-write) INVESTIGATED: a modest reliability win, NOT the ≤15s unlock
246+
Pursued the framebuffer-write throughput lever. The SAB bulk fb-write path (guest `maybeBulkEncodeFsPayload`
247+
kernel `read_bulk_arg`, gated `SECURE_EXEC_T1_RING=1`, 8 MiB bulk buffer) is already **fully built** by a prior
248+
session (`#[allow(dead_code)]`); a `[fb-delta]` diff also already keeps most Xvfb writes as small changed-block
249+
runs on the cheap base64 path, so T1 only replaces the occasional **full-frame** (≥64 KiB) write. Plumbed
250+
`SECURE_EXEC_T1_RING` (+ a new `SX_BURST_LAUNCH=1` fb-write stress option) through the harness (both were missing,
251+
like `SX_PARALLEL_VMS`). Interleaved A/Bs under external load:
252+
- **Settle-gated PV=0, T1 on vs off (4 pairs):** T1 ON **3/4 FULL**, T1 OFF **1/4 FULL** — in the same load
253+
moment T1 ON rendered while T1 OFF went all-black. → T1 is a **modest reliability win** (fewer fb-write-
254+
starvation blacks). No speedup (53s is launch-serialization bound).
255+
- **Burst (all apps concurrent), T1 on vs off:** 8/8 all-black regardless of T1 → the burst's bottleneck is the
256+
**single dispatch thread (PV=0) overwhelmed by 5 concurrently-initializing guests**, not fb-write encoding.
257+
- **PV=1, T1 on vs off (4 pairs):** 8/8 identical panel-only regardless of T1 → **T1 does NOT fix the per-VM
258+
regression** (that stall is CPU oversubscription from 20+ always-polling threads, not the fb-write lock-hold).
259+
The "PV=1 (parallel dispatch) + T1 (cheap fb-write)" combination also fails.
260+
**Conclusion:** T1 is worth shipping as an opt-in reliability lever (validate on a quiet machine before flipping
261+
the default), but it is **NOT the ≤15s unlock.** The real remaining blocker is **concurrent guest init**: 5 heavy
262+
wasm guests cannot initialize at once (single dispatch overwhelmed under PV=0; CPU-oversubscribed under PV=1) —
263+
a deeper concurrency-management problem than framebuffer-write throughput. `SECURE_EXEC_T1_RING` + `SX_BURST_LAUNCH`
264+
stay OFF by default. Full artifacts: `~/progress/secure-exec/2026-07-05-xvfb-throughput-sab/`.
265+
266+
### 2026-07-05 — Cheaper-init + orchestration-waits INVESTIGATED: ≤15s is architecturally out of reach; ~53s is a load-bearing equilibrium
267+
Profiled the boot end-to-end (RPCPROF per-guest syscall table + PATHOPENPROF paths + milestone timeline).
268+
- **WASM compile is NOT the cost:** `new WebAssembly.Module` for a 13.8MB GTK app = **11ms** (V8 lazy compile);
269+
instantiate 1-4ms. So compile-cache / isolate-snapshot buy ~nothing. (The bridge-JS snapshot is already shared.)
270+
- **Per-guest init = a FS SCAN STORM, but its DIRECT cost is cheap (~700ms total, 60k calls @ ~30µs).** The
271+
dominant consumer is **xfwm4** re-reading `/usr/share/themes/Greybird/xfwm4/*` — 113 assets × 13 reopens = 1469
272+
opens + ~7000 stats (NOT fontconfig: 18 font opens). Redundant immutable re-reads.
273+
- **The ~53s is ORCHESTRATION WAITS, not work:** ~12s infra (dbus 2s + xfconfd 4s + Xvfb ~6s) + ~12s WM gate +
274+
~27s per-app settle gaps. The fs storm matters only INDIRECTLY (keeps apps chatty → longer settles; overlapping
275+
storms overwhelm the single dispatch under concurrency).
276+
- **WM-ready EWMH signal: dead end.** Replacing the 12s silent-xfwm4 fallback with a `_NET_SUPPORTING_WM_CHECK`
277+
probe: the property is set only ~49s in (after render, LATER than the 12s fallback) because xfwm4's full init
278+
under single-dispatch contention takes ~37s. No usable earlier signal; the 12s fallback is load-bearing. Reverted.
279+
- **Every compressed launch / trimmed-wait config collapses under external CPU load** that the generous defaults
280+
survive — the boot is jitter-fragile (single-dispatch ceiling), so aggressive orchestration trims need a quiet host.
281+
282+
**Overall conclusion (6 levers taken to ground):** per-VM servicing (regression), launch orchestration (tunable
283+
but load-fragile), Xvfb SAB throughput (modest reliability win), compile-cache (refuted, 11ms), fs-scan reduction
284+
(cheap to service), WM-ready signal (EWMH too late) — NONE is the ≤15s unlock. The ~53s settle-gated boot is a
285+
deeply-constrained equilibrium bound by the **single-owner `&mut sidecar` dispatch pump** serializing all guests'
286+
syscalls (ROOT-2-MULTIPLEX-DESIGN.md) + load-fragility. ≤15s needs a fundamental dispatch re-architecture (a
287+
BOUNDED servicing thread-pool — NOT per-VM-thread, which oversubscribed) — a major separate project, not a lever.
288+
Shipped this session (all OFF/opt-in by default): tunable launch orchestration, T1/burst plumbing, RPCPROF/path
289+
profiling plumbing, the `configure_vm` deadlock fix, the VFS Send refactor, test-binary repair.
290+
291+
### 2026-07-06 — ROOT CAUSE FOUND: the boot is NOTIFY-GAP bound, not dispatch-bound. Fix mechanism PROVEN.
292+
Phase 1 (de-risk) OVERTURNED the whole premise:
293+
- **The single dispatch pump is 99.7% IDLE** (DISPATCH BUSY 0.3-0.36%) in BOTH successful and collapsed boots
294+
(SECURE_EXEC_RPC_WATCHDOG_DUMP_MS). A dispatch thread-pool is REFUTED — it parallelizes an idle resource.
295+
- **The boot = (thousands of X round-trips) × (peerWait ~8.3ms/hop).** HOPPROF: wakeLag notify→resume = 8µs,
296+
respond = 8µs (wake+delivery INSTANT), so the ~8.3ms is peerWait.
297+
- **peerWait is a NOTIFY GAP, not compute:** 80-96% of poll_wait completions are DEADLINE (Xvfb = 96%),
298+
DEADLINE_PROBE 0% data-at-block-entry. The peer wakes on its ~8ms POLL CLAMP instead of on incoming data,
299+
because the writer does not notify the peer's readiness — so every hop pays the clamp.
300+
- **Fix mechanism PROVEN:** `SECURE_EXEC_INLINE_PEER_NOTIFY` (lever 1) wakes the peer inline on write. It was
301+
wired ONE direction only (client→server, via the listener-host-path registry). Added the REVERSE direction
302+
(server→client): a per-guest-path FIFO where net.connect stashes the client's readiness and net.accept pops it
303+
to pair the accepted socket's peer_readiness. Result: **xfdesktop deadline 78%→34%, xfwm4 79%→51%** — the
304+
mechanism demonstrably cuts the deadline waits where it applies.
305+
- **REMAINING (the dominant path):** Xvfb stays 90% deadline + peerWait unchanged, because the inline peer-notify
306+
is wired only into host-unix `net.write` (`process.unix_sockets`), but the X protocol's hot writes go through
307+
the KERNEL-fd socket path (`__kernel_fd_write``kernel.fd_write`) which does NOT notify the peer's readiness.
308+
NEXT: extend inline peer-notify to the kernel-socket write path (both directions) so client requests wake Xvfb
309+
and Xvfb's replies wake the clients — that should collapse peerWait from 8.3ms toward the 8µs wakeLag floor.
310+
- Plumbed for this work: SECURE_EXEC_RPC_WATCHDOG_MS/_DUMP_MS, SECURE_EXEC_RPCPROF. All fixes gated OFF by default
311+
(INLINE_PEER_NOTIFY). Artifacts: ~/progress/secure-exec/2026-07-05-xvfb-throughput-sab/dispatch-decomposition.md.

0 commit comments

Comments
 (0)