perf(desktop): channel-switch tracing + high-membership perf harness - #6455
perf(desktop): channel-switch tracing + high-membership perf harness#6455Maxwellimus wants to merge 19 commits into
Conversation
8858a0d to
ea195be
Compare
ea195be to
b08413d
Compare
|
🤖 Comment from Maxwell's AI agent. Adversarial-review findings (5 P2s), all addressed in the latest push:
|
7f5999b to
a5e8e53
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Requesting changes at exact head a5e8e530b9be277691589b616a1264953cc79799 against base d97780b4777f2fe3430b4e30a7d47fc6837ee059.
P1 — Do not record a replaced switch from the replacement trace’s clock.
After switch A reaches settleChannelSwitchTrace() and begins waiting for its deferred render, starting switch B replaces activeTrace. A’s pending callback then calls record() whenever the replacement is non-null, and samples settledAt only at that later callback. A rapid A → B sequence can therefore charge B’s click/main-thread delay to A and emit an A “settled” mark that was never anchored to A’s painted result. This can manufacture the regression the tracer is supposed to diagnose.
Please close or cancel A at replacement time, or retain an A-specific paint completion signal, and add a rapid A → B lifecycle regression for this branch.
P1 — Serialize append and rotation per log path.
Every append runs in an independent spawn_blocking, but metadata → rename → open → append has no per-path serialization. At the 10 MiB boundary, concurrent writers can both decide to rotate; after one renames the file, the other rename fails and its frontend caller silently swallows the error, dropping a trace. Interleavings can also place records into the wrong generation.
Please serialize the complete append-and-rotation transaction and add a concurrent boundary-write test.
c2e89e6 to
99b0a59
Compare
|
🤖 Comment from Maxwell's AI agent. Both P1s fixed in the latest push: P1 — replaced switch recorded from the replacement's clock. P1 — unserialized append/rotation. |
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Re-reviewing exact head 99b0a590035a89f72a1942a48c32b0f4279cc996 against target main@f99532585a0715bac73b4a6361a9b4966bdb5095 (PR-recorded base 0e48ff26915aa32d5f05208847b9aba75f4f19cd; merge base d97780b4777f2fe3430b4e30a7d47fc6837ee059). The previous rapid-switch clock-theft and same-process append race are fixed. The new lifecycle tests and global lock cover those cases. The remaining blockers are:
P1: replace the retained generation portably before rotating. desktop/src-tauri/src/commands/perf_log.rs:70-75 renames the live log directly onto an existing .1. On Windows, rename does not replace the destination. After the first retained generation exists, the next rollover errors before opening/appending the live file, and channelSwitchPerf.ts:112-125 intentionally swallows that error, so subsequent traces disappear without a signal. This repository already documents and tests the same platform rule in managed_agents/storage.rs:663-684 and storage_tests.rs:751-778. Remove the old .1 while holding PERF_LOG_LOCK, then rename, and add a regression that seeds both current and .1 before rotation.
P1: do not settle while the initial deferred timeline is still unpainted. MessageTimeline.tsx:265-305 knows when the deferred snapshot is stale, but its data-render-pending marker exists only inside the message-list branches (:754-760, :845-855). During the initial empty/channel-changed to loaded transition, the skeleton renders and there is no marker. channelSwitchPerf.ts:284-293 therefore sees “not pending” and records after the next frame even though the heavy deferred list may not have committed or painted. Put the readiness signal on an always-mounted channel-scoped wrapper (or expose an explicit commit signal), and cover a real empty-to-loaded browser switch that asserts the emitted CHANNEL_SWITCH_MEASURE lands after the deferred list.
P1: cancel a channel trace when navigation leaves the channel surface. Only another goChannel call replaces the singleton. goProjects, goHome, and the other non-channel navigations do not clear it, and useChannelSwitchTraceMarks.ts has no route-exit cleanup. Repro: begin A, leave for Projects before A settles, then use history back within 30 seconds. History bypasses goChannel, the returning A screen matches the stale trace, and time spent on Projects is recorded as A’s switch latency. Add route-exit cancellation plus a channel → non-channel → history-back regression.
P2: reset before asynchronous community teardown, and attribute only accepted fetches. resetCommunityState() does not clear the trace until after await resetNavigationDeepLinkDrain() (useCommunityInit.ts:59-83), so queued frame callbacks can record during teardown; rejection skips the reset entirely. Clear it before the first await. Separately, messages/hooks.ts:275-289 attributes the window fetch before reconcileFetchedChannelWindow() executes signal.throwIfAborted(). A canceled request can win the singleton windowFetch ??= slot and block the accepted replacement. Attribute only after the abort gate/reconciliation succeeds, with canceled-first/accepted-second coverage.
The PR is also currently conflicted with main, specifically the post-#6456 roster-freshness work in hooks.ts, and GitHub exposes only DCO for this head. Rebase, preserve the 5-minute roster policy while retaining attribution, rerun the perf harness against current main, and run the normal checks after resolving the code findings above.
99b0a59 to
03ca3b1
Compare
03ca3b1 to
2a4d128
Compare
|
🤖 Comment from Maxwell's AI agent. All five findings from the re-review addressed in the latest push: P1 — portable rotation over an existing P1 — settle while the deferred timeline is unpainted: the P1 — trace canceled on leaving the channel surface: P2 — reset ordering: P2 — attribute only accepted fetches: the window fetch is attributed after The conflict noted in the review was already resolved (branch rebased onto current main, preserving the post-#6456 roster-freshness work — |
… sends (#6572) ## Summary Lands the build-now items from the desktop latency plan (#ui-performance-deep-dive) as one change. Every perceived-latency hot path a user hits on launch, channel open, thread open, and reply send drops one or more round trips. **A1 — persisted channel heads (the big one).** Native WAL SQLite cache (`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey, relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap, schema-version reset, corrupt-row tolerance, checkpointed on shutdown. Three blocking-pool commands: `channel_head_cache_load` / `_store` / `_clear`. On the renderer side, `CommunityQueryProvider` kicks off hydration of up to 12 heads when it constructs the query client — the app, splash and relay preconnect mount immediately; only `useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then consumes a one-shot hydrated gate so a hydrated channel pays **zero** `get_channel_window` calls on mount and exactly **one** on the post-subscription refresh, whose response replaces page zero wholesale. That refresh fires whether live-subscription setup succeeds or fails, and is sequenced behind hydration so it is always a distinct authoritative fetch (see Review follow-ups). Bounds-only persisted heads (zero rows) are not hydrated and take the cold loading path. The timeline loading latch recognizes native-hydrated rows as restart-safe so they paint immediately instead of holding a skeleton. The cache is a paint accelerator only — the relay response is always authoritative. Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401 lines). Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or `localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is cleared on community removal and scoped per identity, so a replaced signer never sees the previous identity's rows. **B1 — thread aux in one response.** Relay thread filters accept `include_aux`; the bridge appends the same authorized two-hop reactions/edits/deletions closure a channel window gets (`build_aux_query` shared with the window path). Renderer `useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is computed from reply-kind rows only since aux rows are unpaged. Documented in `docs/bridge-channel-window.md`. Thread queries keep `staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s, which CI's `thread-unread.spec.ts` caught — once the user leaves a channel, the live subscription stops feeding that thread's cache, so a reopen must always take the (now single) authoritative read. **B2 — cached root on reply send.** `send_channel_message` gains `root_event_id`; when the renderer already holds the parent (channel or thread cache) it passes the NIP-10 root, and native signs without the relay round trip that `resolve_thread_ref` used to make. Strict hex parse; `root_event_id` requires `parent_event_id`; absent root falls back to the existing relay resolution. The renderer never sends a guessed root. **B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5** relay preconnect fires as soon as identity is ready instead of waiting for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts` "service restart close resets accumulated backoff") had been relying on the idle-callback batching to skip past its own seeded dial failures before the channel list painted; `8133d70bb` makes it wait for the connected state instead (test-only, still fails with the 1012 backoff reset disabled). **B6** profile freshness 60s→10 min (both the in-memory entry check and the query `staleTime`). Tradeoff: another user's display-name/avatar edit can take up to 10 min to propagate to a client that already holds their profile (relay reconnect refetches `users-batch` but resolves from the still-fresh per-pubkey entry); your own edits still evict the entry immediately (`evictUsersBatchEntries` in `useUpdateProfileMutation`). ### Related issue Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the measurement instrument and is intentionally not folded in. No duplicate PR found. ### Review follow-ups Addressing Carl's reviews [5001114109](#6572 (review)) and [5002596542](#6572 (review)), each pushed as new commits (no rebase): - `4f06b7770` fix(desktop): mount app while channel heads hydrate; always revalidate — provider no longer gates children on the cache load; `refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads skipped at seed; seed merges into an existing window store. +3 tests. - `35834cb31` fix(relay): drain aux closure hops across the page clamp — `query_all_pages` walks the `(created_at, id)` keyset via `until`/`before_id` until a short page (`AUX_PAGE_LIMIT` = `DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so one-shot `limit: 1000` newest-first no longer drops the oldest edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated. - `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no overlap). - `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind channel head hydration — `refreshChannelWindowMessages` awaits `channelHeadHydration()` and, for a hydration-seeded query (`data !== undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before invalidating. Without this, a subscription that settles before the SQLite load invalidated a data-less in-flight query; TanStack dedupes that onto the existing fetch (`query-core` `fetch()` only cancels when `state.data` exists), which returned the seeded snapshot — 0 authoritative fetches. Regression test reproduces Carl's exact ordering (fails at `35834cb31` with 0 calls), plus a cold-channel guard that the fix does not double-fetch. - `b129231c8` fix(desktop): let concurrent post-hydration refreshes share one window fetch — found independently by Max and Wren reviewing `5a5566c0f`: subscribe settlement + reconnect both wake on the same snapshot promise and both invalidate; the second (default `cancelRefetch: true`) cancelled and replaced the first authoritative fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits the relay). The seeded branch now invalidates with `cancelRefetch: false` so a second waker joins the in-flight fetch; cold/warm keep the default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window` relies on it). Concurrent regression test fails at `5a5566c0f` with 3. ### Testing At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD` = `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib` 910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same specs minus affordance); GitHub CI green on every job except Smoke (3) (unrelated project-review row-count + messaging timing flake, per Carl) and Unit Tests (sherpa cache skeleton, below). Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70 + a comments-only commit correcting two `profile/hooks.ts` freshness comments from 60s to 10 min; pre-push desktop check/typecheck/test 5,387/0 re-ran at 0c49236) in one shell; `origin/main` = `040b203f7` at PR open, since moved to `4baccd539` (#6558, mobile only — zero file overlap, `git merge-tree` clean): - `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount then 1 on invalidate with wholesale replacement) - Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` + `channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at `7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec persists a head, reloads into a fresh mock relay with the head fetch held 5s, asserts the persisted row paints within 2s, exactly one `get_channel_window` after open, and the stale row is removed when the authoritative page lands. - `pnpm typecheck`, `pnpm check` — clean At `7acbf951b` (everything except the two-line `useThreadReplies.ts` staleTime revert and the test-only `relay-reconnect.spec.ts` change), also green in one shell: - `just desktop-tauri-test` — 2,859 passed / 0 failed across the workspace (channel_head_cache: wire shape, LRU+caps, schema reset, corrupt-row skip) - `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p buzz-relay --lib` — 908 passed / 0 failed - `just check` components: fmt-check, clippy, desktop-check, desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy, web-check, mobile-check, file-size-check — all green - `just desktop-build`, `web-build`, `desktop-tauri-check`, `mobile-test` (1,661 passed) — all green CI note: the "Unit Tests" job goes red on this PR and on `main` whenever it hits a poisoned `rust-cache` entry (an empty-directory skeleton of `target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts), surfacing as `could not find native static library sherpa-onnx-c-api` in `buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry and rerunning turned the job green at `0c492366d` (28/28); it re-poisons on the next `main` push until the workflow clears that directory after cache restore. Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and line-by-line by me before opening; the staleTime fix re-verified by Wren and me independently; the relay-reconnect test fix bisected and verified by me. --------- Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
… sends (#6572) Lands the build-now items from the desktop latency plan (#ui-performance-deep-dive) as one change. Every perceived-latency hot path a user hits on launch, channel open, thread open, and reply send drops one or more round trips. **A1 — persisted channel heads (the big one).** Native WAL SQLite cache (`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey, relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap, schema-version reset, corrupt-row tolerance, checkpointed on shutdown. Three blocking-pool commands: `channel_head_cache_load` / `_store` / `_clear`. On the renderer side, `CommunityQueryProvider` kicks off hydration of up to 12 heads when it constructs the query client — the app, splash and relay preconnect mount immediately; only `useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then consumes a one-shot hydrated gate so a hydrated channel pays **zero** `get_channel_window` calls on mount and exactly **one** on the post-subscription refresh, whose response replaces page zero wholesale. That refresh fires whether live-subscription setup succeeds or fails, and is sequenced behind hydration so it is always a distinct authoritative fetch (see Review follow-ups). Bounds-only persisted heads (zero rows) are not hydrated and take the cold loading path. The timeline loading latch recognizes native-hydrated rows as restart-safe so they paint immediately instead of holding a skeleton. The cache is a paint accelerator only — the relay response is always authoritative. Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401 lines). Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or `localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is cleared on community removal and scoped per identity, so a replaced signer never sees the previous identity's rows. **B1 — thread aux in one response.** Relay thread filters accept `include_aux`; the bridge appends the same authorized two-hop reactions/edits/deletions closure a channel window gets (`build_aux_query` shared with the window path). Renderer `useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is computed from reply-kind rows only since aux rows are unpaged. Documented in `docs/bridge-channel-window.md`. Thread queries keep `staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s, which CI's `thread-unread.spec.ts` caught — once the user leaves a channel, the live subscription stops feeding that thread's cache, so a reopen must always take the (now single) authoritative read. **B2 — cached root on reply send.** `send_channel_message` gains `root_event_id`; when the renderer already holds the parent (channel or thread cache) it passes the NIP-10 root, and native signs without the relay round trip that `resolve_thread_ref` used to make. Strict hex parse; `root_event_id` requires `parent_event_id`; absent root falls back to the existing relay resolution. The renderer never sends a guessed root. **B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5** relay preconnect fires as soon as identity is ready instead of waiting for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts` "service restart close resets accumulated backoff") had been relying on the idle-callback batching to skip past its own seeded dial failures before the channel list painted; `8133d70bb` makes it wait for the connected state instead (test-only, still fails with the 1012 backoff reset disabled). **B6** profile freshness 60s→10 min (both the in-memory entry check and the query `staleTime`). Tradeoff: another user's display-name/avatar edit can take up to 10 min to propagate to a client that already holds their profile (relay reconnect refetches `users-batch` but resolves from the still-fresh per-pubkey entry); your own edits still evict the entry immediately (`evictUsersBatchEntries` in `useUpdateProfileMutation`). Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the measurement instrument and is intentionally not folded in. No duplicate PR found. Addressing Carl's reviews [5001114109](#6572 (review)) and [5002596542](#6572 (review)), each pushed as new commits (no rebase): - `4f06b7770` fix(desktop): mount app while channel heads hydrate; always revalidate — provider no longer gates children on the cache load; `refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads skipped at seed; seed merges into an existing window store. +3 tests. - `35834cb31` fix(relay): drain aux closure hops across the page clamp — `query_all_pages` walks the `(created_at, id)` keyset via `until`/`before_id` until a short page (`AUX_PAGE_LIMIT` = `DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so one-shot `limit: 1000` newest-first no longer drops the oldest edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated. - `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no overlap). - `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind channel head hydration — `refreshChannelWindowMessages` awaits `channelHeadHydration()` and, for a hydration-seeded query (`data !== undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before invalidating. Without this, a subscription that settles before the SQLite load invalidated a data-less in-flight query; TanStack dedupes that onto the existing fetch (`query-core` `fetch()` only cancels when `state.data` exists), which returned the seeded snapshot — 0 authoritative fetches. Regression test reproduces Carl's exact ordering (fails at `35834cb31` with 0 calls), plus a cold-channel guard that the fix does not double-fetch. - `b129231c8` fix(desktop): let concurrent post-hydration refreshes share one window fetch — found independently by Max and Wren reviewing `5a5566c0f`: subscribe settlement + reconnect both wake on the same snapshot promise and both invalidate; the second (default `cancelRefetch: true`) cancelled and replaced the first authoritative fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits the relay). The seeded branch now invalidates with `cancelRefetch: false` so a second waker joins the in-flight fetch; cold/warm keep the default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window` relies on it). Concurrent regression test fails at `5a5566c0f` with 3. At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD` = `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib` 910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same specs minus affordance); GitHub CI green on every job except Smoke (3) (unrelated project-review row-count + messaging timing flake, per Carl) and Unit Tests (sherpa cache skeleton, below). Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70 + a comments-only commit correcting two `profile/hooks.ts` freshness comments from 60s to 10 min; pre-push desktop check/typecheck/test 5,387/0 re-ran at 0c49236) in one shell; `origin/main` = `040b203f7` at PR open, since moved to `4baccd539` (#6558, mobile only — zero file overlap, `git merge-tree` clean): - `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount then 1 on invalidate with wholesale replacement) - Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` + `channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at `7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec persists a head, reloads into a fresh mock relay with the head fetch held 5s, asserts the persisted row paints within 2s, exactly one `get_channel_window` after open, and the stale row is removed when the authoritative page lands. - `pnpm typecheck`, `pnpm check` — clean At `7acbf951b` (everything except the two-line `useThreadReplies.ts` staleTime revert and the test-only `relay-reconnect.spec.ts` change), also green in one shell: - `just desktop-tauri-test` — 2,859 passed / 0 failed across the workspace (channel_head_cache: wire shape, LRU+caps, schema reset, corrupt-row skip) - `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p buzz-relay --lib` — 908 passed / 0 failed - `just check` components: fmt-check, clippy, desktop-check, desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy, web-check, mobile-check, file-size-check — all green - `just desktop-build`, `web-build`, `desktop-tauri-check`, `mobile-test` (1,661 passed) — all green CI note: the "Unit Tests" job goes red on this PR and on `main` whenever it hits a poisoned `rust-cache` entry (an empty-directory skeleton of `target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts), surfacing as `could not find native static library sherpa-onnx-c-api` in `buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry and rerunning turned the job green at `0c492366d` (28/28); it re-poisons on the next `main` push until the workflow clears that directory after cache restore. Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and line-by-line by me before opening; the staleTime fix re-verified by Wren and me independently; the relay-reconnect test fix bisected and verified by me. --------- Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Requesting changes at exact head 2a4d128a40b41c804f3c13966e60a08c4f0a8cad for three remaining instrumentation-correctness issues:
-
[P1] Do not settle while the lazy channel UI is still suspended.
ChannelScreendrivessettleChannelSwitchTracefrom query readiness, butChannelPanemay still be behindChannelScreenLoadingFallback. The pending marker exists only insideMessageTimeline, so the tracer interprets an absent marker as ready. In an adversarial browser run that delayed the realChannelPanechunk, a deep-history measure landed at 331.7 ms with zero rows while the fallback was still visible; the rows mounted only after releasing the chunk. Make readiness explicit from the mounted timeline or mark the outer fallback pending, and add delayed-chunk coverage. The checked-in localhost smoke test does not exercise this interval. -
[P1] Preserve traces through React StrictMode's effect replay. The new route-exit cleanup in
useChannelSwitchTraceMarksabandons the just-opened trace during StrictMode's development-only mount cleanup, then the effect remount does not recreate it. This breaks the PR's stated dev-build/Performance-panel workflow. Reproduced against the real Vite dev runtime: onebuzz:channel-switch:startmark, zeroclick-to-settledmeasures after rows painted, andswitch-settle-after-paint.spec.tstimed out waiting for a settle. The production-style E2E build passes because it does not run StrictMode effect replay. Route-exit cancellation needs to distinguish an actual route/channel exit from effect replay, with dev-runtime coverage. -
[P2] Enforce the 4 KiB cap after backend metadata is added.
shape_perf_log_linechecks onlyrecord_json.len(), then appends the unboundedBUZZ_PERF_LOG_LABEL. A 13-byte record plus a 1 MiB label serializes to a 1,048,615-byte JSONL line, defeating the documented defensive cap and inflating always-on telemetry I/O and retention. Bound/reject/truncate the label or validate the final serialized line, and cover it.
The prior Windows rotation, concurrent rotation, rapid replacement, deferred timeline marker, route history, pre-await teardown, and canceled-fetch attribution findings are otherwise fixed. Focused JS tests passed (23/23), git diff --check passed, and exact-head CI is green. Local Rust tests could not start because this machine lacks cmake; exact-head CI includes successful Rust and Windows jobs.
Non-code merge note: GitHub reports the PR conflicted with current main. git merge-tree finds one keep-both conflict in projectChannelWindow.test.mjs; preserve both independently appended regression tests when rebasing.
Add a single-active-trace instrument for channel navigation: goChannel opens the trace, ChannelScreen marks route commit and settles it after the timeline loading latch clears (double rAF so the frame painted). The two relay fetches that can sit on the switch path — the message window and the member roster — are attributed to the trace when they run during it; cache-served switches log "cache". Each switch emits one [switch-perf] console line plus User Timing marks/measures (buzz:channel-switch:*) so before/after comparisons work identically in a dev build, the Performance panel, and Playwright perf specs. Signed-off-by: Max Lampert <maxwell@squareup.com>
Add an inflateChannelMembers mock-bridge knob (channel name → target member count; synthetic hex-pubkey members appended once, on first channel read) and a member-heavy-switch perf spec that measures warm switch wall time and longtasks for channel↔channel and channel↔Projects at baseline / 2k / 10k members per channel, holding message volume constant at 150 rows. Method mirrors warm-switch-markdown.perf.ts: in-page click + rAF polling, 4x CPU throttle, medians over 16 switches after an untimed warmup round-trip. Run from desktop/: pnpm build:e2e npx playwright test --config=playwright.perf.config.ts member-heavy-switch.perf.ts Signed-off-by: Max Lampert <maxwell@squareup.com>
Address the three findings from the 2026-08-24 review of #6455: - The lazy ChannelPane's Suspense fallback now carries the data-render-pending marker, so a switch trace can no longer settle while the pane chunk is suspended. New Playwright regression holds the chunk and asserts no measure lands until rows paint. - Route-exit trace abandonment is deferred one microtask and canceled by the effect re-setup, so StrictMode's dev-only effect replay no longer kills a just-opened trace. New jsdom regressions run the real react-dom dev replay; the settle spec also passes against the Vite dev runtime. - The perf-log 4 KiB cap is enforced on the final serialized line, and BUZZ_PERF_LOG_LABEL is truncated to 128 bytes at a char boundary, so a runaway label can neither inflate the sink nor kill every record. Also extracts the timeline-loading latch + trace marks from ChannelScreen into useChannelTimelineLoading (file-size ratchet). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
2a4d128 to
e50edba
Compare
|
🤖 Comment from Maxwell's AI agent. All three findings from the latest review addressed in e50edba, and the branch is rebased onto current main — both independently added P1 — settle while the lazy channel UI is suspended: the ChannelPane Suspense fallback ( P1 — StrictMode effect replay kills the trace: route-exit abandonment is now scheduled one microtask out ( P2 — 4 KiB cap defeated by metadata: Housekeeping: the timeline-loading latch + trace marks moved verbatim from Validation: Rust perf_log tests 9/9 (fmt + clippy clean); desktop unit suite 5450/5450; settle spec 2/2 on the production e2e build and passing on the dev runtime; the |
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Requesting changes at exact head e50edba1021f202df5e5feb052afd468b3056918 against base f6e6617a9dcc2308d5039f8afaab974b49fb9577 for three remaining instrumentation-correctness issues:
-
[P2] Do not leave a trace when channel navigation is refused.
goChannelcallsbeginChannelSwitchTrace(channelId)beforecommitNavigation, butcommitNavigationmay return early whenallowNavigationrejects the switch. That leaves the target trace active even though navigation never occurred. Repro: visit B, return to A, begin editing a thread reply, click B and reject navigation, then resolve the edit and use browser Back to B within 30 seconds. History navigation is deliberately untraced, yet B's mount settles the orphan and emits an inflated measurement spanning the refused click and edit. Start the trace only after the guard accepts, beforenavigate, and cover refused click followed by history navigation. -
[P2] Include the JSONL terminator in the final 4 KiB record cap.
shape_perf_log_lineaccepts a serialized line whose length is exactly 4096 bytes, thenappend_line_rotatingwrites it withwriteln!, producing a 4097-byte on-disk record. Reserve one byte for\nand add an exact-boundary test that measures the bytes written, not only the pre-write string length. -
[P2] Make the channel↔Projects benchmark measure and report the claimed workload. The harness declares Projects ready when
projects-page-headerappears, while repository snapshots and work items can still be loading. Its warmup also populates both surfaces, andrunScenariocombines channel→Projects and Projects→channel samples into one median, which can represent neither direction and hide a one-leg regression. Gate readiness on the query-dependent Projects surface or explicitly relabel this as shell-only warm navigation, and report each direction separately.
Validation: focused JS lifecycle/fetch tests passed 29/29; git diff --check and applicable GitHub checks are green. The Rust boundary test could not run locally because the required Tauri sidecar binary is absent; exact-head Rust and Windows CI passed.
…nchmark Address the three findings from the 2026-08-24 re-review of #6455, plus one finding from a follow-up adversarial review: - The switch trace now opens inside commitGuardedNavigation, only after the navigation guard accepts — a refused click can no longer leave an orphan trace that a later history navigation would settle with inflated time. The commit flow is extracted and dependency-injected; new unit tests pin guard ordering and the refusal→history-back case. - The perf-log 4 KiB cap now bounds bytes on disk: one byte is reserved for the newline writeln! appends. New boundary test measures the file. - The channel↔Projects benchmark gates readiness on a new data-projects-hydrating marker (driven by the surface's query fan via isLoading, so disabled queries never wedge it) instead of the shell header, verifies member inflation on both inflated channels, uses a prefixed ready-selector for general, and reports each switch direction as its own median so a one-leg regression cannot hide. - A settle wait that hits its 5s deadline while the render is still pending now records with settleWaitTruncated instead of posing as an honest settled paint — the >deadline tail is what the tracer exists to expose. Also removes the dead ChannelSwitchFetchTrace type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
|
🤖 Comment from Maxwell's AI agent. All three findings from the latest review addressed in 41b8159, then the full cumulative diff was re-reviewed by two independent reviewers with no authoring context (details at the end). P2 — trace left behind by a guard-refused navigation: the trace now opens inside the shared commit flow ( P2 — 4 KiB cap vs the newline terminator: P2 — channel↔Projects benchmark honesty: the Projects surface now exposes Fresh-eyes review round: after the fixes, a blind adversarial reviewer (no authoring context, prompted to refute) and Deferred (P3, tracked here deliberately): (a) the settle wait polls the global Validation: desktop unit suite 5,456/5,456 (incl. 4 new |
…esty Blind-review round 2 (Opus) findings: - P1: rAF suspends in hidden windows, so a queued settle fired only on the user's return and charged the whole absence to the switch as a clean record (reproduced at runtime). Settle now drops when the window is already hidden, poisons the wait on visibilitychange, and drops any trace older than the entry timeout plus the render wait — nothing legitimate can reach that age. - The JSONL append is a single write_all: writeln! issues two write syscalls and the lock is process-local while the log path is not, so a second process sharing the log dir could interleave mid-line. - The User Timing buffer keeps only the latest switch's mark/measure — desktop sessions run for weeks and the buffer is never GC'd. - The route-commit mark moved to a layout effect so it stamps commit time, not first-paint time. - Comment honesty: the member-heavy harness now states that mock-mode IPC hands live objects (parse cost not exercised) and that the Projects hydration marker guards cold/invalidated samples only; the goChannel trace-anchor comment describes what the same-channel guard actually skips; the Rust concurrency test's byte arithmetic corrected. - Longtask sampling drains PerformanceObserver.takeRecords() at sample time so a longtask ending just before the resolve frame isn't lost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 3 (Sonnet) finding: beginChannelSwitchTrace marked unconditionally but only record() cleared, so every abandoned/dropped trace (forum visits, route exits, hidden-window and frame-starved drops) leaked a permanent buzz:channel-switch:start entry. Clearing the previous start mark at begin bounds the buffer to one entry no matter how the prior trace ended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
…uffer at begin Blind-review round 4 (Opus xhigh) findings: - A not-pending frame landing past the wait deadline is rAF starvation (system suspend, App Nap — no visibilitychange), not render time: a fast settle followed by a stall recorded up to 34s as a clean switch under the click-anchored 35s age guard. Such frames now drop. - Visibility accounting now spans the whole trace: one module-level visibilitychange watcher timestamps transitions, and any transition since the click (not just during the settle wait) drops the trace — a window minimized while the fetch was in flight no longer records its absence as switch time. Replaces the per-settle listener. - begin() now clears the previous switch's settled mark and measure too, so a consumer polling the User Timing buffer mid-switch can never read the prior switch's entries as the current one's. - The goChannel comment no longer claims every channel navigation funnels through it: Pulse startDm navigates to the channel route directly and is untraced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 5 (Fable medium) finding: a longtask trailing switch N is delivered by the PerformanceObserver in a later task and landed in switch N+1's freshly reset array, inflating its longtask lines. Drain takeRecords() and discard before each sample's reset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 6 (Sonnet xhigh) finding: a transient lock on the rotated generation (AV/EDR or an editor, chiefly Windows) failed the whole append — and since the live file stays oversized, every later append re-entered the same failing branch, silently dropping every record until the lock cleared. Rotation is now best-effort: on failure the line appends unrotated and the size cap re-applies once a later rotation succeeds. Unix regression locks the directory and asserts the line survives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 7 (Opus medium) findings: - P2: a trace begun while the channel route was still resolving never mounted ChannelScreen, so no route-exit cleanup existed; leaving for Home and history-backing into the channel within 30s settled the stale trace with the time spent away. Any committed non-channel navigation and any history traversal now drop the active trace at the navigation layer (dropActiveChannelSwitchTrace), which covers traces no component ever owned. Same-channel navigations keep the live trace. - The delayed-chunk spec now asserts no CLEAN measure while the chunk is held (the tracer honestly emits a truncated one if its 5s deadline passes) and guards its own timing budget explicitly so a slow CI box fails with the real reason. - The rotation-degradation test blocks rotation with a non-empty directory instead of chmod, so it also holds when tests run as root (containers), and now runs on Windows too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
…op starved frames
Blind-review round 9 (Opus high) findings:
- P2: the members queryFn attributed unconditionally, so a roster fetch
superseded by a live join/leave invalidation could claim the trace's
one-shot membersFetch slot with a stale count and duration. It now
checks signal.throwIfAborted() before attributing — same rule as the
window fetch's reconcile abort gate.
- P2: a permanently dead sink (unwritable log dir, stale directory at
the log path) was indistinguishable from no traced switches: console
lines kept flowing while every append failed into .catch(() => {}).
The first persistence failure now warns once.
- P3: during a suspension that fires no visibilitychange, the deferred
render-pending marker stays latched, so the settle wait recorded a
truncated measure inflated by the whole stall (bounded only by the
35s age guard). A single inter-frame gap beyond 3s — beyond any real
main-thread stall — now drops the sample.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
… sends (#6572) Lands the build-now items from the desktop latency plan (#ui-performance-deep-dive) as one change. Every perceived-latency hot path a user hits on launch, channel open, thread open, and reply send drops one or more round trips. **A1 — persisted channel heads (the big one).** Native WAL SQLite cache (`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey, relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap, schema-version reset, corrupt-row tolerance, checkpointed on shutdown. Three blocking-pool commands: `channel_head_cache_load` / `_store` / `_clear`. On the renderer side, `CommunityQueryProvider` kicks off hydration of up to 12 heads when it constructs the query client — the app, splash and relay preconnect mount immediately; only `useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then consumes a one-shot hydrated gate so a hydrated channel pays **zero** `get_channel_window` calls on mount and exactly **one** on the post-subscription refresh, whose response replaces page zero wholesale. That refresh fires whether live-subscription setup succeeds or fails, and is sequenced behind hydration so it is always a distinct authoritative fetch (see Review follow-ups). Bounds-only persisted heads (zero rows) are not hydrated and take the cold loading path. The timeline loading latch recognizes native-hydrated rows as restart-safe so they paint immediately instead of holding a skeleton. The cache is a paint accelerator only — the relay response is always authoritative. Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401 lines). Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or `localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is cleared on community removal and scoped per identity, so a replaced signer never sees the previous identity's rows. **B1 — thread aux in one response.** Relay thread filters accept `include_aux`; the bridge appends the same authorized two-hop reactions/edits/deletions closure a channel window gets (`build_aux_query` shared with the window path). Renderer `useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is computed from reply-kind rows only since aux rows are unpaged. Documented in `docs/bridge-channel-window.md`. Thread queries keep `staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s, which CI's `thread-unread.spec.ts` caught — once the user leaves a channel, the live subscription stops feeding that thread's cache, so a reopen must always take the (now single) authoritative read. **B2 — cached root on reply send.** `send_channel_message` gains `root_event_id`; when the renderer already holds the parent (channel or thread cache) it passes the NIP-10 root, and native signs without the relay round trip that `resolve_thread_ref` used to make. Strict hex parse; `root_event_id` requires `parent_event_id`; absent root falls back to the existing relay resolution. The renderer never sends a guessed root. **B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5** relay preconnect fires as soon as identity is ready instead of waiting for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts` "service restart close resets accumulated backoff") had been relying on the idle-callback batching to skip past its own seeded dial failures before the channel list painted; `8133d70bb` makes it wait for the connected state instead (test-only, still fails with the 1012 backoff reset disabled). **B6** profile freshness 60s→10 min (both the in-memory entry check and the query `staleTime`). Tradeoff: another user's display-name/avatar edit can take up to 10 min to propagate to a client that already holds their profile (relay reconnect refetches `users-batch` but resolves from the still-fresh per-pubkey entry); your own edits still evict the entry immediately (`evictUsersBatchEntries` in `useUpdateProfileMutation`). Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the measurement instrument and is intentionally not folded in. No duplicate PR found. Addressing Carl's reviews [5001114109](#6572 (review)) and [5002596542](#6572 (review)), each pushed as new commits (no rebase): - `4f06b7770` fix(desktop): mount app while channel heads hydrate; always revalidate — provider no longer gates children on the cache load; `refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads skipped at seed; seed merges into an existing window store. +3 tests. - `35834cb31` fix(relay): drain aux closure hops across the page clamp — `query_all_pages` walks the `(created_at, id)` keyset via `until`/`before_id` until a short page (`AUX_PAGE_LIMIT` = `DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so one-shot `limit: 1000` newest-first no longer drops the oldest edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated. - `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no overlap). - `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind channel head hydration — `refreshChannelWindowMessages` awaits `channelHeadHydration()` and, for a hydration-seeded query (`data !== undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before invalidating. Without this, a subscription that settles before the SQLite load invalidated a data-less in-flight query; TanStack dedupes that onto the existing fetch (`query-core` `fetch()` only cancels when `state.data` exists), which returned the seeded snapshot — 0 authoritative fetches. Regression test reproduces Carl's exact ordering (fails at `35834cb31` with 0 calls), plus a cold-channel guard that the fix does not double-fetch. - `b129231c8` fix(desktop): let concurrent post-hydration refreshes share one window fetch — found independently by Max and Wren reviewing `5a5566c0f`: subscribe settlement + reconnect both wake on the same snapshot promise and both invalidate; the second (default `cancelRefetch: true`) cancelled and replaced the first authoritative fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits the relay). The seeded branch now invalidates with `cancelRefetch: false` so a second waker joins the in-flight fetch; cold/warm keep the default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window` relies on it). Concurrent regression test fails at `5a5566c0f` with 3. At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD` = `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib` 910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same specs minus affordance); GitHub CI green on every job except Smoke (3) (unrelated project-review row-count + messaging timing flake, per Carl) and Unit Tests (sherpa cache skeleton, below). Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70 + a comments-only commit correcting two `profile/hooks.ts` freshness comments from 60s to 10 min; pre-push desktop check/typecheck/test 5,387/0 re-ran at 0c49236) in one shell; `origin/main` = `040b203f7` at PR open, since moved to `4baccd539` (#6558, mobile only — zero file overlap, `git merge-tree` clean): - `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount then 1 on invalidate with wholesale replacement) - Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` + `channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at `7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec persists a head, reloads into a fresh mock relay with the head fetch held 5s, asserts the persisted row paints within 2s, exactly one `get_channel_window` after open, and the stale row is removed when the authoritative page lands. - `pnpm typecheck`, `pnpm check` — clean At `7acbf951b` (everything except the two-line `useThreadReplies.ts` staleTime revert and the test-only `relay-reconnect.spec.ts` change), also green in one shell: - `just desktop-tauri-test` — 2,859 passed / 0 failed across the workspace (channel_head_cache: wire shape, LRU+caps, schema reset, corrupt-row skip) - `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p buzz-relay --lib` — 908 passed / 0 failed - `just check` components: fmt-check, clippy, desktop-check, desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy, web-check, mobile-check, file-size-check — all green - `just desktop-build`, `web-build`, `desktop-tauri-check`, `mobile-test` (1,661 passed) — all green CI note: the "Unit Tests" job goes red on this PR and on `main` whenever it hits a poisoned `rust-cache` entry (an empty-directory skeleton of `target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts), surfacing as `could not find native static library sherpa-onnx-c-api` in `buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry and rerunning turned the job green at `0c492366d` (28/28); it re-poisons on the next `main` push until the workflow clears that directory after cache restore. Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and line-by-line by me before opening; the staleTime fix re-verified by Wren and me independently; the relay-reconnect test fix bisected and verified by me. --------- Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
…le specs Blind-review round 10 (Fable high) finding: the specs' settle polls accepted a legitimately truncated measure — the tracer honestly hitting its 5s render-wait deadline on a slow box — and then failed the painted- rows assertion with a message blaming the tracer. Both tests now poll for clean measures only and fail truncated-only runs with an explicit harness-timing message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
… sends (#6572) Lands the build-now items from the desktop latency plan (#ui-performance-deep-dive) as one change. Every perceived-latency hot path a user hits on launch, channel open, thread open, and reply send drops one or more round trips. **A1 — persisted channel heads (the big one).** Native WAL SQLite cache (`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey, relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap, schema-version reset, corrupt-row tolerance, checkpointed on shutdown. Three blocking-pool commands: `channel_head_cache_load` / `_store` / `_clear`. On the renderer side, `CommunityQueryProvider` kicks off hydration of up to 12 heads when it constructs the query client — the app, splash and relay preconnect mount immediately; only `useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then consumes a one-shot hydrated gate so a hydrated channel pays **zero** `get_channel_window` calls on mount and exactly **one** on the post-subscription refresh, whose response replaces page zero wholesale. That refresh fires whether live-subscription setup succeeds or fails, and is sequenced behind hydration so it is always a distinct authoritative fetch (see Review follow-ups). Bounds-only persisted heads (zero rows) are not hydrated and take the cold loading path. The timeline loading latch recognizes native-hydrated rows as restart-safe so they paint immediately instead of holding a skeleton. The cache is a paint accelerator only — the relay response is always authoritative. Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401 lines). Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or `localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is cleared on community removal and scoped per identity, so a replaced signer never sees the previous identity's rows. **B1 — thread aux in one response.** Relay thread filters accept `include_aux`; the bridge appends the same authorized two-hop reactions/edits/deletions closure a channel window gets (`build_aux_query` shared with the window path). Renderer `useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is computed from reply-kind rows only since aux rows are unpaged. Documented in `docs/bridge-channel-window.md`. Thread queries keep `staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s, which CI's `thread-unread.spec.ts` caught — once the user leaves a channel, the live subscription stops feeding that thread's cache, so a reopen must always take the (now single) authoritative read. **B2 — cached root on reply send.** `send_channel_message` gains `root_event_id`; when the renderer already holds the parent (channel or thread cache) it passes the NIP-10 root, and native signs without the relay round trip that `resolve_thread_ref` used to make. Strict hex parse; `root_event_id` requires `parent_event_id`; absent root falls back to the existing relay resolution. The renderer never sends a guessed root. **B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5** relay preconnect fires as soon as identity is ready instead of waiting for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts` "service restart close resets accumulated backoff") had been relying on the idle-callback batching to skip past its own seeded dial failures before the channel list painted; `8133d70bb` makes it wait for the connected state instead (test-only, still fails with the 1012 backoff reset disabled). **B6** profile freshness 60s→10 min (both the in-memory entry check and the query `staleTime`). Tradeoff: another user's display-name/avatar edit can take up to 10 min to propagate to a client that already holds their profile (relay reconnect refetches `users-batch` but resolves from the still-fresh per-pubkey entry); your own edits still evict the entry immediately (`evictUsersBatchEntries` in `useUpdateProfileMutation`). Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the measurement instrument and is intentionally not folded in. No duplicate PR found. Addressing Carl's reviews [5001114109](#6572 (review)) and [5002596542](#6572 (review)), each pushed as new commits (no rebase): - `4f06b7770` fix(desktop): mount app while channel heads hydrate; always revalidate — provider no longer gates children on the cache load; `refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads skipped at seed; seed merges into an existing window store. +3 tests. - `35834cb31` fix(relay): drain aux closure hops across the page clamp — `query_all_pages` walks the `(created_at, id)` keyset via `until`/`before_id` until a short page (`AUX_PAGE_LIMIT` = `DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so one-shot `limit: 1000` newest-first no longer drops the oldest edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated. - `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no overlap). - `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind channel head hydration — `refreshChannelWindowMessages` awaits `channelHeadHydration()` and, for a hydration-seeded query (`data !== undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before invalidating. Without this, a subscription that settles before the SQLite load invalidated a data-less in-flight query; TanStack dedupes that onto the existing fetch (`query-core` `fetch()` only cancels when `state.data` exists), which returned the seeded snapshot — 0 authoritative fetches. Regression test reproduces Carl's exact ordering (fails at `35834cb31` with 0 calls), plus a cold-channel guard that the fix does not double-fetch. - `b129231c8` fix(desktop): let concurrent post-hydration refreshes share one window fetch — found independently by Max and Wren reviewing `5a5566c0f`: subscribe settlement + reconnect both wake on the same snapshot promise and both invalidate; the second (default `cancelRefetch: true`) cancelled and replaced the first authoritative fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits the relay). The seeded branch now invalidates with `cancelRefetch: false` so a second waker joins the in-flight fetch; cold/warm keep the default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window` relies on it). Concurrent regression test fails at `5a5566c0f` with 3. At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD` = `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0, Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` + `relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh `build:e2e`, pre-push hooks green. At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib` 910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same specs minus affordance); GitHub CI green on every job except Smoke (3) (unrelated project-review row-count + messaging timing flake, per Carl) and Unit Tests (sherpa cache skeleton, below). Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70 + a comments-only commit correcting two `profile/hooks.ts` freshness comments from 60s to 10 min; pre-push desktop check/typecheck/test 5,387/0 re-ran at 0c49236) in one shell; `origin/main` = `040b203f7` at PR open, since moved to `4baccd539` (#6558, mobile only — zero file overlap, `git merge-tree` clean): - `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount then 1 on invalidate with wholesale replacement) - Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` + `channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at `7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec persists a head, reloads into a fresh mock relay with the head fetch held 5s, asserts the persisted row paints within 2s, exactly one `get_channel_window` after open, and the stale row is removed when the authoritative page lands. - `pnpm typecheck`, `pnpm check` — clean At `7acbf951b` (everything except the two-line `useThreadReplies.ts` staleTime revert and the test-only `relay-reconnect.spec.ts` change), also green in one shell: - `just desktop-tauri-test` — 2,859 passed / 0 failed across the workspace (channel_head_cache: wire shape, LRU+caps, schema reset, corrupt-row skip) - `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p buzz-relay --lib` — 908 passed / 0 failed - `just check` components: fmt-check, clippy, desktop-check, desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy, web-check, mobile-check, file-size-check — all green - `just desktop-build`, `web-build`, `desktop-tauri-check`, `mobile-test` (1,661 passed) — all green CI note: the "Unit Tests" job goes red on this PR and on `main` whenever it hits a poisoned `rust-cache` entry (an empty-directory skeleton of `target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts), surfacing as `could not find native static library sherpa-onnx-c-api` in `buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry and rerunning turned the job green at `0c492366d` (28/28); it re-poisons on the next `main` push until the workflow clears that directory after cache restore. Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and line-by-line by me before opening; the staleTime fix re-verified by Wren and me independently; the relay-reconnect test fix bisected and verified by me. --------- Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
… guard
Blind-review round 12 (Opus xhigh) findings, both reproduced:
- P1: the round-9 abort gate destructured { signal } in the members
queryFn — merely reading that getter sets React Query's
abortSignalConsumed, switching the roster query to cancel-and-revert
when its last observer unsubscribes mid-fetch. Interrupted switches
discarded rosters that previously landed in cache (the Tauri call
cannot be cancelled, so the work was paid and thrown away), and
A->B->A warm switches repaid a full roster fetch. Replaced with a
per-channel supersession token (openChannelMembersFetch) checked by
traceChannelMembersFetch — stale fetches stay out of the one-shot
slot without touching the signal. Sequences reset with community
state.
- P2: lastFrameAt started null, so the settle-entry -> first-frame
window skipped the starvation guard: a no-visibilitychange suspension
there recorded a truncated measure inflated by the whole stall
(bounded only by the 35s age guard). The gap clock is now seeded at
settle entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 13 (Sonnet xhigh) hardening: leavesChannelSurface used a /channels/ prefix check, so sibling routes (forum posts) counted as staying on the channel surface and kept a live trace alive. No currently reachable path turns that into a wrong record — every re-entry overwrites or drops the trace — but the invariant from 9b46232 should hold structurally, not incidentally. Anything other than the exact message-view route now drops the active trace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
…ent test Blind-review round 14 (Opus medium) P3s: - settleChannelSwitchTrace guarded window but then read document (visibilityState, querySelector) unguarded — the document guard in ensureVisibilityWatcher was dead protection. The settle entry gate now covers both globals. - The concurrent boundary test folded the rotated generation in behind if-let, so a regression where rotation never fires under contention would pass green with all 32 lines in the live file. The read is now unconditional, and the boundary comment's arithmetic is corrected (rotation triggers before the 23rd append). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 15 (Fable xhigh) finding: a clean measure recorded behind the held chunk — the exact regression under test — clears the start mark and nulls elapsedSinceClick, so the timing-budget assertion failed first with a 'rerun, not a tracer bug' message stating the opposite of the truth. The contract assertion now runs first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
…budget guard Blind-review round 16 (Opus high) P3s: - A same-task unmount-then-renavigate to the same channel left the exit cleanup's scheduled abandon pending, and its microtask killed the freshly opened trace (silent lost sample). beginChannelSwitchTrace now revokes any pending abandon for its channel. - The delayed-chunk spec's timing-budget guard hit expect(null) with a raw matcher error when a truncated record had already cleared the start mark — the one case its message exists for. Boolean form now covers the null. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
… dispatch Blind-review round 17 (Fable high) finding: startedAt sampled performance.now() at click-handler dispatch, silently excluding input delay — a click queued behind a long task under-reported by the whole queueing time, unbounded, in exactly the contention regime the tracer exists to expose. begin() now anchors at the dispatching event's timeStamp when one is present (window.event is set only during synchronous dispatch, so stale timestamps cannot leak in from async continuations; min() guards skewed clocks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 18 (Opus xhigh) finding: 26ee898 moved the measure's origin to the input event's timeStamp but left the start mark at handler-dispatch time, so the Performance panel showed the measure beginning before its own start mark by the input delay — two different switch durations from one instrument. The mark now carries startTime: startedAt, pinned by asserting mark and measure share the anchor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
|
🤖 Comment from Maxwell's AI agent. Since the last review response, this branch went through a multi-round adversarial review loop: 23 completed fresh-eyes reviews rotating across three models and three reasoning-effort levels, each blind to authorship, each prompted to refute, looping until three consecutive reviews found nothing. The last three rounds returned zero findings. Every fix below is its own commit with a red-first test where practical; head is 843e667, CI green. Measurement-honesty fixes (tracer): frame-starvation guards on every phase of the settle wait (not-pending arrivals past the deadline, per-frame gap ceiling seeded at settle entry, 35s age bound); visibility accounting spanning the whole trace via one module-level Attribution fixes: the members roster gate is a per-channel supersession token rather than the query Sink fixes (Rust): rotation is best-effort (a transiently locked rotated generation degrades to an unrotated append instead of dropping every record, with a stderr line); a permanently dead sink warns once in the console instead of being indistinguishable from "no switches traced"; the rotate-once contract is asserted unconditionally in the concurrency test; the degradation test blocks rotation with a non-empty directory so it holds under root and on Windows. Harness fixes: stale longtask deliveries drained at sample start; settle specs assert the clean-measure contract before their own timing-budget guard and fail truncated-only runs with an explicit harness-timing message (null-safe). Deferred by design (accepted, not missed — full 32-item ledger travels with the review harness): drop-not-fabricate biases (dropped traces emit no drop record; >3s single-frame renders drop rather than record), population scoping (deep-link and search-hit navigations anchor at Validation on head: desktop unit suite 5,471/5,471; Rust perf_log 11/11 (fmt + clippy clean); settle + navigation smoke specs green; member-heavy perf baseline green with per-direction medians; file-size/px/whitespace gates clean; CI 24 checks passing. |
Channel switching on large communities felt 1–2s slow with no way to attribute the time. This PR adds the measurement layer used to diagnose and verify the fixes in the follow-up PRs; it changes no product behavior.
Tracing (
channelSwitchPerf.ts): every channel navigation records click → route commit → settled paint, with the two relay fetches that can sit on that path (message window, member roster) attributed only when they complete inside the switch window:Each switch also emits User Timing marks (
buzz:channel-switch:*) and appends a JSONL record via a new Tauri command to{app_log_dir}/switch-perf.jsonl, stamped with the build's git revision (baked by build.rs) and an optionalBUZZ_PERF_LOG_LABELrun label — so before/after sessions are attributable offline. Settle waits (bounded) for the timeline's deferred commit so render-heavy switches aren't underreported; forum surfaces abandon the trace rather than underreport; a settle for a different channel never clobbers a newer switch's trace; the active trace resets on community switch.Harness: an
inflateChannelMembersmock-bridge knob (channel name → member count, synthetic members appended on first read) andmember-heavy-switch.perf.ts, measuring warm-switch wall time + longtasks for channel↔channel and channel↔Projects at baseline / 2k / 10k members with fixed message volume.Baselines this instrument established (pre-fix)
The follow-up PRs cut the live median to 201ms (−35%) and warm switches to 148ms (−43%), verified with this same instrument.