Skip to content

Keep the app answering when a command holds the core, and save ink on text - #22

Merged
XrxcGH merged 6 commits into
betafrom
fx-core-hang
Oct 15, 2026
Merged

XrxcGH merged 6 commits into
betafrom
fx-core-hang

Conversation

@XrxcGH

@XrxcGH XrxcGH commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Fixes beta 4's T2-1 (ink on text never saved) and contains T2-3 (the core stopped responding and Close did nothing).

T2-1: ink on text never saves

Every geometry reader (file reads, edits, frames, notebook styles) now rounds the way the writer does (0.01; crops 6 decimals; line height 2), so the save's read-back check passes. A failed save forgets its orphan .onk segment, and the title bar shows "Couldn't save" from core:save-failed instead of "Saved".

T2-3: the core stops responding, Close does nothing

What the T2 profile and log show: the core answered until 14:48:54, then nothing at all was logged, not even the exit handshake's warning that a Close must produce within 3 s. So the webview's IPC was dead, not only the core: every core command took the core's one lock unbounded on an async worker, so once one command never returned, each later one parked a worker and an IPC request until the channel was full. (Tauri 2.12 sends Windows invokes as fetch() on the ipc custom protocol, so Chromium's per-host connection cap plausibly applies; not measured.)

The command that held the core at 14:49 is not identified. Every lock order in the core was checked by hand and is consistent; the only unbounded cross-thread waits under the core lock were the journal opens, which the inline rename and the new page's first edit both go through. This PR is containment plus diagnosis, so the next occurrence names its holder:

  1. Journal opens give up after OPEN_TIMEOUT (10 s) and the page runs without a journal (spec 20.12); the worker detaches a journal nobody waits for.
  2. Every core command runs on a blocking thread (CoreBridge::run), so a wait never parks the async runtime.
  3. A command waits at most COMMAND_WAIT (15 s, longer than OPEN_TIMEOUT so a stuck open gives up before the commands behind it) at a Condvar door and then fails with code coreBusy, naming the holder. The failed request completes, so the IPC channel stays open for Close, the log and everything else.
  4. The exit waits up to the flush time and then exits without the final save; the journals keep the edits.
  5. A watchdog names the command holding the core after 10 s (core:stalled/core:responsive); the title bar shows "Not responding", also as soon as any command fails as coreBusy.
  6. Interface: a coreBusy page_apply is a non-resync error: the text stays unsent with its riders, the "not kept" toast shows, and the next flush sends it.

Known limits

  • Root cause of the 14:49 hold: unidentified (see above). The watchdog and the coreBusy message exist to catch it next time.
  • IPC saturation: there is no frontend cap on pending invokes, so with many pages open (one page_apply in flight each, plus tree and search calls) more than ~6 concurrent core commands can still fill the channel for up to 15 s per round while the core is held. Acceptable for beta.
  • Rust callers outside bridge.run (external.rs notebook_roots, backup.rs) now get a coreBusy error after 15 s instead of blocking forever; both handle it.

Verification

  • cargo test -p opennote --lib core_bridge; cargo test -p opennote-core journal_thread; cargo clippy -p opennote -p opennote-core --all-targets.
  • vitest: textSync, tauri, saveHealth (30 passed), SaveStatus component test (2 passed); app:typecheck, eslint, prettier clean.
  • Branch merged with origin/beta before the final run.

🤖 Generated with Claude Code

XrxcGH and others added 6 commits October 7, 2026 20:04
A page's geometry is written rounded to 0.01 (frames, an anchored ink
block's dx/dy, the view, paper and background, column widths), but the
readers kept the value an edit gave them. The interface measures where
ink sits on its words in screen pixels, so its offset has more decimals
than the file keeps, and every save's read-back check (spec 17.7, S5)
then failed with "blocks differs after reading back": the page never
reached disk, a retry ran every 30 s, each one left another orphan .onk
segment in ink/, and the title bar said Saved (beta 4, T2-1).

Every reader of a geometry value now rounds it the way the writer does
(file reads, edits, and resolved frames alike), so the page in memory is
the page its bytes read back as. A save that fails after its ink segment
was written removes the segment again. The title bar follows the core's
save events: "Couldn't save" while a page's last save failed, "Saved"
only once the core saves it.

The components vitest project takes its port from OPENNOTE_VITEST_PORT,
so parallel runs on one machine don't collide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setting the same style twice is a repeat, not a change, now that
notebook.json's styles are read with the writer's rounding
(T2-1 follow-up).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beta 4's T2-3: once one command never returned, every later core
command parked a runtime worker and an IPC request behind it; the
webview carries only a few IPC requests at once, so soon nothing
reached Rust at all, Close and the log included, and the status said
"Saving" for the rest of the session.

- Every core command runs on a blocking thread, and waits at most
  15 s for the core before failing as `coreBusy`, naming the holder,
  so the IPC channel stays open.
- Journal opens wait at most 20 s for the journal thread; a page whose
  journal doesn't answer runs degraded instead of holding the core.
- The exit polls the core lock up to the flush time and goes on
  without the final save when the core is held.
- A watchdog names the command that has held the core for 10 s, logs
  it, and sends core:stalled / core:responsive; the title bar shows
  "Not responding" (announced) instead of "Saving" forever, also on a
  `coreBusy` error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A command that waits for the core slept 10 ms between try_lock polls, so a
contended command paid up to 10 ms extra and a waiter could be passed by newer
arrivals for as long as it waited. The commands now queue at a door (a flag
and a condition variable): a waiter sleeps until the holder leaves and starts
the moment it does, and still gives up as coreBusy after COMMAND_WAIT.

The journal open timeout drops from 20 s to 10 s, below COMMAND_WAIT (15 s),
so a stuck journal open gives up before the commands queued behind it do, and
the two constants say how they relate. The interface's coreBusy path has
tests: a busy page_apply keeps the text and its riders unsent, shows the "not
kept" toast and sends them with the next flush (textSync.test), and marks the
core stalled before the watchdog's event (tauri.test). One useless Cow
conversion clippy flagged is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's checks on the pull request raised what the lane's own files owed: the
bridge had grown past the 800-line limit, rustfmt wanted eight of the lane's
files reflowed, and seven comments were too long or missed a serial comma.

The constants, the door, the guard, the holder and the watchdog now live in
core_bridge/held.rs, which reads as one thing: how a command takes the core,
how long it waits, and who says when one holds it too long. The bridge keeps
the commands. The lane's files are formatted as CI's rustfmt wants them, and
the comments say the same in shorter sentences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@XrxcGH
XrxcGH merged commit 1ba3551 into beta Oct 15, 2026
8 of 11 checks passed
@XrxcGH
XrxcGH deleted the fx-core-hang branch October 15, 2026 21:12
@XrxcGH

XrxcGH commented Oct 15, 2026

Copy link
Copy Markdown
Owner Author

Merged through beta in the Beta 5 integration (beta head 9e2ba25); GitHub marked this PR merged when beta was pushed.

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