Skip to content

perf(desktop): skip pre-send parent fetch on reply when thread root is cached#1467

Open
tlongwell-block wants to merge 1 commit into
mainfrom
perf/reply-send-cached-root
Open

perf(desktop): skip pre-send parent fetch on reply when thread root is cached#1467
tlongwell-block wants to merge 1 commit into
mainfrom
perf/reply-send-cached-root

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Problem

Every reply pays a full relay round trip before the send even starts: send_channel_message calls resolve_thread_ref, which fetches the parent event from the relay just to walk its NIP-10 e-tags for the thread root. The frontend timeline cache almost always already has that parent — the user just clicked "reply" on it.

This is the reply-RTT fix from the perf trace (best perceived-latency-per-line in the desktop lane): reply send latency drops by one relay RTT in the common case.

Change

  • Rust (commands/messages.rs): send_channel_message grows an optional cached_root_event_id. When present, thread_ref_from_cached_root builds the ThreadRef locally; when absent, the existing resolve_thread_ref relay path runs unchanged. Applies to both the forum-comment and default send arms.
  • TS (features/messages/lib/threading.ts): new resolveCachedReplyRoot(parentEventId, events) mirrors the Rust resolver exactly — last root marker wins, else last reply marker, else the parent is the root; self-referential markers collapse to the parent; s.len() >= 4 marker guard. It also gates on the same parent-kind allowlist resolve_thread_ref queries (9, 40002, 45001, 45003, 48100) — a cached parent of any other kind returns null so behavior can't diverge from the relay path (which would report "parent event not found").
  • Send mutation (features/messages/hooks.ts): resolves the root from the timeline cache it already reads for optimistic tags, and passes it through. Only the send-mutation block is touched — per the lane partition with Wren (Optimize desktop channel live subscriptions #1466 owns the subscription/bridge regions of this file).
  • shared/api/tauri.ts: parameter plumbing (+2 lines).

Correctness

  1. Fallback, not replacement. null on cache miss, non-allowlisted kind, or missing markers ⇒ Rust falls back to the existing relay resolution. The fast path only engages when the answer is byte-identical to what the relay fetch would return.
  2. Relay still validates. The cached root is used only for tag construction. The relay's ingest-side ancestry check (root tag does not match thread ancestry, handlers/ingest.rs) rejects any stale or wrong root — the fast path cannot produce an accepted-but-misthreaded event, only the same rejection a bad reply gets today.
  3. Pinned mirror. A unit suite (threading.resolveCachedReplyRoot.test.mjs) locks the TS mirror to the Rust semantics: marker precedence, last-wins tag walk, self-referential collapse, short/unmarked e-tag rejection, kind allowlist (including the KIND_HUDDLE_STARTED=48100 kind — an earlier draft had 45021 here; the test now makes that drift red), and the trap where resolveReplyRootId's parent-id fallback would silently mislabel a nested reply as a thread root (the cached resolver returns null instead).

Validation

On this head (branch cut from main e42dae3f, which is an ancestor of HEAD — verified merge-base --is-ancestor in the same shell as the push):

  • pnpm --dir desktop test: 1503 passed / 0 failed (includes the 9 new resolver tests)
  • pnpm --dir desktop typecheck: clean
  • pnpm check (biome + file sizes + px-text): exit 0
  • cargo test (desktop src-tauri): all green
  • cargo clippy --all-targets (desktop src-tauri): no new warnings in touched files

File-size overrides for messages.rs (+24) and tauri.ts (+2) bumped with rationale comments, per the existing queued-to-split convention.

…s cached

Every reply paid a full relay round trip before send: resolve_thread_ref
fetched the parent event just to walk its NIP-10 e-tags for the thread
root — data the frontend timeline cache almost always already has.

send_channel_message grows an optional cached_root_event_id. The TS send
mutation resolves the root from the cached timeline via a new
resolveCachedReplyRoot that mirrors the Rust resolver exactly (last root
marker wins, else last reply marker, else parent is root; same parent-kind
allowlist including KIND_HUDDLE_STARTED=48100; self-referential markers
collapse to the parent). Any cache miss or out-of-allowlist kind returns
null and the Rust side falls back to the existing relay resolution —
behavior unchanged.

Correctness: the cached root is used only for tag construction; the relay's
ingest-side ancestry validation (root tag must match thread ancestry)
still rejects any stale/wrong root, so the fast path cannot produce an
accepted-but-misthreaded event. A unit suite pins the TS mirror to the
Rust resolver semantics, including the trap where resolveReplyRootId's
parent-id fallback would mislabel a nested reply as a thread root.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant