From d21f0915ec254053815a69571670e4c80ef7de62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 06:52:50 +0000 Subject: [PATCH 1/5] fix(web): keep GUI avatar decoding out of shared storage tests --- crates/gui/src/session/avatar/mod.rs | 4 +++- crates/gui/src/session/avatar/web.rs | 10 ---------- crates/gui/src/session/avatar/web_platform.rs | 13 +++++++++++++ 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 crates/gui/src/session/avatar/web_platform.rs diff --git a/crates/gui/src/session/avatar/mod.rs b/crates/gui/src/session/avatar/mod.rs index ed7d0a69..63b1c27b 100644 --- a/crates/gui/src/session/avatar/mod.rs +++ b/crates/gui/src/session/avatar/mod.rs @@ -14,6 +14,8 @@ mod native; mod web; #[cfg(target_family = "wasm")] mod web_lock; +#[cfg(target_family = "wasm")] +mod web_platform; #[cfg(all(test, not(target_family = "wasm")))] mod probe; @@ -21,7 +23,7 @@ mod probe; #[cfg(not(target_family = "wasm"))] use native as imp; #[cfg(target_family = "wasm")] -use web as imp; +use web_platform as imp; use portable_atomic::{AtomicU64, Ordering}; use std::collections::{HashMap, HashSet}; diff --git a/crates/gui/src/session/avatar/web.rs b/crates/gui/src/session/avatar/web.rs index 759c9a7e..7cb5c55b 100644 --- a/crates/gui/src/session/avatar/web.rs +++ b/crates/gui/src/session/avatar/web.rs @@ -27,16 +27,6 @@ static STORAGE_GENERATION: portable_atomic::AtomicU64 = portable_atomic::AtomicU static ACTIVE_ACCOUNT: RwLock> = RwLock::new(None); -pub(super) async fn decode_avatar( - bytes: Vec, - _priority: super::DecodePriority, - current: impl FnOnce() -> bool + Send + 'static, -) -> Option<(std::sync::Arc, u64)> { - current() - .then(|| crate::session::media::decode_avatar(&bytes)) - .flatten() -} - pub fn set_active_account(account: Option) { if let Ok(mut lock) = ACTIVE_ACCOUNT.write() { if *lock != account { diff --git a/crates/gui/src/session/avatar/web_platform.rs b/crates/gui/src/session/avatar/web_platform.rs new file mode 100644 index 00000000..8a796be6 --- /dev/null +++ b/crates/gui/src/session/avatar/web_platform.rs @@ -0,0 +1,13 @@ +//! Web avatar decoding and the persistent storage implementation. + +pub use super::web::*; + +pub(super) async fn decode_avatar( + bytes: Vec, + _priority: super::DecodePriority, + current: impl FnOnce() -> bool + Send + 'static, +) -> Option<(std::sync::Arc, u64)> { + current() + .then(|| crate::session::media::decode_avatar(&bytes)) + .flatten() +} From 0da555ad0a04a5779780c7c84afab69c09acb149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 06:52:50 +0000 Subject: [PATCH 2/5] test(plugin-host): exhaust fuel without fifty million debug dispatches --- crates/plugin-host/src/tests.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/plugin-host/src/tests.rs b/crates/plugin-host/src/tests.rs index 79fc2111..43c6b6a3 100644 --- a/crates/plugin-host/src/tests.rs +++ b/crates/plugin-host/src/tests.rs @@ -285,7 +285,12 @@ const PONG_WITHOUT_PERMISSION: &str = r#"(module )"#; fn spins() -> String { - versioned(SPINS) + // Wasmi charges const operators before folding constant/drop pairs away. + // Burn the real per-call fuel budget with fewer interpreter dispatches: + // a bare branch needs 50 million debug dispatches and races the 10 s + // publication deadline on a busy CI runner. The loop, fuel budget, + // deadline and fuel-trap assertion remain unchanged. + versioned(&SPINS.replace("$FUEL_WORK", &"i32.const 0 drop\n".repeat(64))) } const SPINS: &str = r#"(module @@ -293,7 +298,7 @@ const SPINS: &str = r#"(module (memory (export "memory") 1) (func (export "oxi_abi_version") (result i32) (i32.const $ABI_VERSION)) (func (export "oxi_init") (result i32) (call $subscribe (i64.const 2)) (i32.const 0)) - (func (export "oxi_on_event") (param i32 i32) (result i32) (loop (br 0)) (i32.const 0)) + (func (export "oxi_on_event") (param i32 i32) (result i32) (loop $FUEL_WORK (br 0)) (i32.const 0)) )"#; /// Asks for one page and then for far more than the limit allows. From a1758d06675e2a4a978da5bcf34472da21c6d6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 06:55:01 +0000 Subject: [PATCH 3/5] docs: record baseline web failure and native fuel test reproduction --- docs/measurements/ci-288-289-triage.md | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/measurements/ci-288-289-triage.md diff --git a/docs/measurements/ci-288-289-triage.md b/docs/measurements/ci-288-289-triage.md new file mode 100644 index 00000000..af95231e --- /dev/null +++ b/docs/measurements/ci-288-289-triage.md @@ -0,0 +1,105 @@ +# CI failures shared by the profiling PRs + +Checkpoint for #288 and #289, against main `33108ce`, core `af2e018c`. +These are baseline issues, independent of the SQL and phone-metadata patches. +No production dependencies, fuel limits, test deadlines or assertions change. + +## Web compilation failure + +The [#288 web job](https://github.com/oxidezap/client/actions/runs/40998832174/job/123878204949) +and [#289 web job](https://github.com/oxidezap/client/actions/runs/41000719222/job/123884222100) +fail before launching a browser. Both report the same errors: + +```text +error[E0433]: cannot find `session` in the crate root + avatar/web.rs:36: crate::session::media::decode_avatar(&bytes) +error[E0425]: cannot find type `DecodePriority` in module `super` + avatar/web.rs:32: _priority: super::DecodePriority +error[E0433]: cannot find module or crate `gpui` in this scope + avatar/web.rs:34: Arc +``` + +`daemon::media::avatar_web_tests` imports the actual GUI storage source by +path so it can exercise Cache Storage and Web Locks without linking GPUI. +#282 added a GUI decode function to that file. The daemon has neither those +GUI modules/types nor a GPUI dependency. Main already has both the import +and the GUI function; neither profiling PR changes them. A clean baseline +compilation reproduced all three errors with the same library-test target: + +```sh +RUSTFLAGS='--cfg web_sys_unstable_apis' \ + cargo test -p oxidezap-daemon --lib --target wasm32-unknown-unknown --no-run +``` + +The fix moves only the GUI decode function into `avatar/web_platform.rs`. +The GUI selects that adapter as `imp`; it reexports the existing `web` +storage implementation. The daemon still imports `web.rs`, unchanged at its +call site. Storage implementation bytes are identical after removing the +GUI function. No browser tests, data-isolation checks or storage logic are +removed or stubbed, and the daemon gains no GUI dependency. + +## Native fuel test flake + +The [#288 Check job](https://github.com/oxidezap/client/actions/runs/40998832174/job/123878205132) +passed workspace Clippy and then failed at: + +```text +tests::a_plugin_that_loops_forever_runs_out_of_fuel_and_is_stopped +crates/plugin-host/src/tests.rs:110:13: +timed out waiting for the plugin to be stopped +``` + +Nextest reported failure after 11.004 seconds. The adjacent self-waking +plugin test took 21.454 seconds. The failed fixture is a bare infinite +branch, draining the real 50,000,000-unit per-call fuel budget one branch +at a time in the debug interpreter. Its publication poll has a ten-second +wall-clock deadline. Both plugin-host sources and its dependency graph are +unchanged by #288; chat-store is not a plugin-host dependency. + +The unchanged fixture passed isolated on #288 in 8.44 seconds and on main +in 8.47 seconds. Pinning the main test and one synthetic busy Python process +to the same CPU reproduced the exact timeout; the process finished after +16.75 seconds. The competing process was terminated in a `finally` block. +This establishes a load-sensitive baseline flake, not a SQL regression. + +Reproduction uses the test executable reported by Cargo: + +```sh +cargo test -p oxidezap-plugin-host --all-features --lib \ + tests::a_plugin_that_loops_forever_runs_out_of_fuel_and_is_stopped -- --exact +# To reproduce contention, pin this executable and one CPU-busy process +# to the same allowed CPU; always terminate that process after the test. +``` + +The fixture now inserts 64 constant/drop pairs inside the same infinite +loop. Wasmi 2.0 meters each constant operator before folding the pair away; +`engine/translator/func/fuel.rs` and `engine/costs.rs` in the locked wasmi +source define this behavior. The loop consumes the same production fuel +budget with fewer interpreter dispatches. Its unconditional branch, +ten-second deadline, stopped-state assertion and fuel-error assertion all +remain. No runtime sandbox setting or budget changes. + +With the same CPU contention, the adjusted fixture passed five consecutive +runs in 0.60–0.68 seconds. Isolated, it passed in 0.25 seconds. The full +plugin-host suite passed, 135 tests and one existing ignored test. Clippy +for all plugin-host targets/features passed with warnings denied. + +## Browser validation + +The existing daemon and session library tests were built and run with the +CI flags, stable Rust 1.99.1 and wasm-bindgen-test-runner 0.2.129. A matching +Chromium/ChromeDriver 156.0.8078.73 pair was unpacked into `/tmp` for the +synthetic browser runs; no installed browser or user profile was modified. + +```sh +RUSTFLAGS='--cfg web_sys_unstable_apis' \ +CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ + cargo test -p oxidezap-daemon -p oxidezap-session --lib \ + --target wasm32-unknown-unknown +``` + +All 16 daemon tests and 32 session tests passed, including the real avatar +Cache Storage recovery/account-isolation tests. Existing unrelated WASM +warnings remain. The production GUI also needs its normal Pages build; +Windows, macOS and the remaining CI jobs remain the PR's checks. No merge +is performed as part of this investigation. From 0e3e4b8405daca9e99d730163968b666993ba9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 07:01:48 +0000 Subject: [PATCH 4/5] test(plugin-host): retain explicit bare-branch fuel coverage --- crates/plugin-host/src/tests.rs | 31 ++++++++++++++++++++++++++ docs/measurements/ci-288-289-triage.md | 6 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/plugin-host/src/tests.rs b/crates/plugin-host/src/tests.rs index 43c6b6a3..dffea915 100644 --- a/crates/plugin-host/src/tests.rs +++ b/crates/plugin-host/src/tests.rs @@ -613,6 +613,37 @@ fn a_command_outside_the_declared_capabilities_never_reaches_the_daemon() { assert!(surface.is_running(), "but it is not stopped for trying"); } +/// Keep the original empty-loop case covered independently of the cheaper +/// host fixture: adding billed work must not hide a free-branch regression. +#[test] +fn a_bare_branch_loop_consumes_fuel() { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let worker = std::thread::spawn(move || { + let mut config = wasmi::Config::default(); + config.consume_fuel(true); + let engine = wasmi::Engine::new(&config); + let bytes = wat::parse_str("(module (func (export \"spin\") (loop (br 0))))") + .expect("valid bare-loop fixture"); + let module = wasmi::Module::new(&engine, &bytes[..]).expect("the module validates"); + let mut store = wasmi::Store::new(&engine, ()); + store.set_fuel(32).expect("fuel metering is enabled"); + let instance = wasmi::Linker::new(&engine) + .instantiate_and_start(&mut store, &module) + .expect("the module has no start function"); + let spin = instance.get_typed_func::<(), ()>(&store, "spin").unwrap(); + let error = spin + .call(&mut store, ()) + .expect_err("the loop exhausts fuel"); + tx.send(error.as_trap_code()).unwrap(); + }); + assert_eq!( + rx.recv_timeout(Duration::from_secs(10)) + .expect("bare branches must consume fuel"), + Some(wasmi::TrapCode::OutOfFuel) + ); + worker.join().expect("the fuel probe completed"); +} + /// The number that makes running a stranger's code in this process /// defensible. #[test] diff --git a/docs/measurements/ci-288-289-triage.md b/docs/measurements/ci-288-289-triage.md index af95231e..ff949658 100644 --- a/docs/measurements/ci-288-289-triage.md +++ b/docs/measurements/ci-288-289-triage.md @@ -77,11 +77,13 @@ loop. Wasmi 2.0 meters each constant operator before folding the pair away; source define this behavior. The loop consumes the same production fuel budget with fewer interpreter dispatches. Its unconditional branch, ten-second deadline, stopped-state assertion and fuel-error assertion all -remain. No runtime sandbox setting or budget changes. +remain. A separate bounded 32-unit probe retains the original bare-branch +case and asserts the precise OutOfFuel trap, so adding other billed work +cannot hide a free-branch regression. No runtime sandbox setting or budget changes. With the same CPU contention, the adjusted fixture passed five consecutive runs in 0.60–0.68 seconds. Isolated, it passed in 0.25 seconds. The full -plugin-host suite passed, 135 tests and one existing ignored test. Clippy +plugin-host suite passed, 136 tests and one existing ignored test. Clippy for all plugin-host targets/features passed with warnings denied. ## Browser validation From 71130e9f153a4b3dde3b121f466ec5caaf9a0be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 07:09:23 +0000 Subject: [PATCH 5/5] fix(ci): avoid duplicating existing avatar probe instrumentation --- .github/workflows/avatar-restore.yml | 9 ++++++--- docs/measurements/ci-288-289-triage.md | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/avatar-restore.yml b/.github/workflows/avatar-restore.yml index 5ee33dbe..671476e9 100644 --- a/.github/workflows/avatar-restore.yml +++ b/.github/workflows/avatar-restore.yml @@ -56,13 +56,16 @@ jobs: text = mod.read_text() marker = 'mod native;' assert marker in text - mod.write_text(text.replace(marker, marker + '\n#[cfg(all(test, not(target_family = "wasm")))]\nmod probe;', 1)) + if 'mod probe;' not in text: + mod.write_text(text.replace(marker, marker + '\n#[cfg(all(test, not(target_family = "wasm")))]\nmod probe;', 1)) native = root / 'native.rs' text = native.read_text() marker = 'pub async fn read_persistent(key: &str) -> Option> {' assert marker in text - text = text.replace(marker, marker + '\n #[cfg(test)]\n PERSISTENT_READS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);', 1) - text += '\n#[cfg(test)]\npub(super) static PERSISTENT_READS: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(0);\n' + if 'PERSISTENT_READS.fetch_add' not in text: + text = text.replace(marker, marker + '\n #[cfg(test)]\n PERSISTENT_READS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);', 1) + if 'static PERSISTENT_READS:' not in text: + text += '\n#[cfg(test)]\npub(super) static PERSISTENT_READS: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(0);\n' native.write_text(text) PY cargo test -p oxidezap-gui --bin oxidezap --locked native_restore_probe -- --ignored --nocapture diff --git a/docs/measurements/ci-288-289-triage.md b/docs/measurements/ci-288-289-triage.md index ff949658..ad22cfb6 100644 --- a/docs/measurements/ci-288-289-triage.md +++ b/docs/measurements/ci-288-289-triage.md @@ -105,3 +105,21 @@ Cache Storage recovery/account-isolation tests. Existing unrelated WASM warnings remain. The production GUI also needs its normal Pages build; Windows, macOS and the remaining CI jobs remain the PR's checks. No merge is performed as part of this investigation. + +## Avatar comparison workflow instrumentation + +Changing an avatar file activates the existing `Avatar restore` comparison +workflow. Its base step exposed another baseline harness problem on #290: +E0428, duplicate `probe` module and duplicate `PERSISTENT_READS` static. +The workflow unconditionally injected instrumentation that #282 had already +committed into main. It also would have double-counted reads by adding a +second increment. This failed while measuring the unchanged base, before +running the candidate. + +The injection now adds each declaration/increment only when absent. Both +legacy uninstrumented inputs and current main were exercised through the +actual Python block extracted from the workflow. Each produced one probe, +one counter and one increment; applying the block twice was byte-identical. +For current main the entire output was byte-identical to the input. Both +base/candidate measurements and PNG/JPEG probe runs remain enabled. This is +an idempotency fix, with no deletion of the performance or lifecycle checks.