Memory: an idle Home lets go of its id index (#398, prototype) #460

Closed
kayg wants to merge 5 commits from issue/398-unload into dev
Owner

Part of #398: kayg asked how low idle memory can go. A prototype for review; it is built on #427 and #428, and its diff includes them until they merge.

What

  • An idle Home lets go of its id index. The index is about 10 MB at 100,000 notes, which was most of an idle server's live heap. The Home drops it after 10 minutes with:

    • no lookup;
    • no open note;
    • no scan or job running.

    Changes still queued for ids.jsonl are written first. The next lookup reads the file again, as a server start does.

  • Nothing polls. Each lookup notes the time. One short-lived thread waits for the quiet stretch to end, unloads, trims the heap and exits. Idle wake-ups stay at 0.

  • Plain ASCII names skip Unicode normalising in names::nfc and names::fold, which also speeds up scans and link parsing. Reading the index of 100,000 notes back took 155 to 170 ms; it now takes 45 ms (release build, staging).

  • CALTERNAL_X_UNLOAD_SECS overrides the 10 minutes, for measuring only.

Numbers, perf VM, 100k notes

Before each first request after unloading I dropped the page cache, so the file is read from disk.

SSD (3 runs) rotational disk (3 runs)
idle RSS before unloading MB 34–46 34–51
idle RSS unloaded MB 19–25 23–33
anonymous memory unloaded MB 5–11 9–19
first note opened after unloading ms 125, 169, 265 145, 221, 243
the same note again ms ~1 ~1
search right after ms 31–133 52–284

All first opens are under the 300 ms target. Tests: unloads_and_reloads covers changes made just before an unload, and reload_speed is an ignored timing test. clippy -D warnings and the notes and fsio tests are clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T

Part of #398: kayg asked how low idle memory can go. **A prototype for review; it is built on #427 and #428**, and its diff includes them until they merge. ## What - **An idle Home lets go of its id index.** The index is about 10 MB at 100,000 notes, which was most of an idle server's live heap. The Home drops it after 10 minutes with: - no lookup; - no open note; - no scan or job running. Changes still queued for `ids.jsonl` are written first. The next lookup reads the file again, as a server start does. - **Nothing polls.** Each lookup notes the time. One short-lived thread waits for the quiet stretch to end, unloads, trims the heap and exits. Idle wake-ups stay at 0. - **Plain ASCII names skip Unicode normalising** in `names::nfc` and `names::fold`, which also speeds up scans and link parsing. Reading the index of 100,000 notes back took 155 to 170 ms; it now takes 45 ms (release build, staging). - **`CALTERNAL_X_UNLOAD_SECS` overrides the 10 minutes**, for measuring only. ## Numbers, perf VM, 100k notes Before each first request after unloading I dropped the page cache, so the file is read from disk. | | SSD (3 runs) | rotational disk (3 runs) | |---|---|---| | idle RSS before unloading MB | 34–46 | 34–51 | | **idle RSS unloaded MB** | **19–25** | 23–33 | | anonymous memory unloaded MB | 5–11 | 9–19 | | **first note opened after unloading ms** | **125, 169, 265** | 145, 221, 243 | | the same note again ms | ~1 | ~1 | | search right after ms | 31–133 | 52–284 | All first opens are under the 300 ms target. Tests: `unloads_and_reloads` covers changes made just before an unload, and `reload_speed` is an ignored timing test. `clippy -D warnings` and the notes and fsio tests are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
glibc keeps what a burst of work freed (the startup scan, the list store
catching up, a search index build or catch-up, the whole-Home list)
inside its heaps, a heap per thread, and rarely returns it: at 100,000
notes the server sat at 150 MB or more with about 30 MB in use (#44).

A new small crate, `calternal-memory`, calls glibc's `malloc_trim` two
seconds after the last burst (a burst arms one wait on a thread of its
own: nothing runs on a timer, an idle server never wakes up for it). It
is the one crate allowed an `unsafe` call, for that one call; the
workspace still forbids `unsafe` everywhere else. Hooked after the
startup scan, big list refreshes, search catch-up and updates, and the
whole-Home list.

Local, 100k notes, system allocator: settled 82 -> 60 MB, after searches
152-160 -> 84 MB.

Part of #44

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Memory: no transparent huge pages
Some checks failed
CI / server (pull_request) Successful in 23s
CI / rust (pull_request) Successful in 2m26s
CI / web (pull_request) Failing after 41m1s
d5cebeb198
With transparent huge pages on `always` (Debian's default, and the perf
VM's), the kernel's background thread merges heap regions into whole
2 MB pages even when most of the region is free. A trimmed heap is
mostly free pages, so an idle server at 100,000 notes grew from 126 to
155 MB while doing nothing, 84 MB of it in huge pages that trimming
can't hand back.

The server now asks the kernel at start (`prctl(PR_SET_THP_DISABLE)`)
to keep it on normal pages, in calternal-memory, the one crate allowed
an `unsafe` call. (#44)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
`names::nfc` and `names::fold` normalised every name, though most are
plain ASCII, where NFC changes nothing and lowercasing is ASCII's.
Reading the id index of 100,000 notes back took 155-170 ms, now
45 ms (staging, release build); scans and link parsing fold every
name too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Memory: an idle Home lets go of its id index (#398)
Some checks failed
CI / server (pull_request) Successful in 25s
CI / rust (pull_request) Successful in 2m16s
CI / web (pull_request) Failing after 42m22s
004fe71b41
The id index (10 MB at 100,000 notes) was most of an idle server's
heap. After 10 minutes with no lookup, no open note and no scan or job
running, the Home drops it; the next lookup reads `ids.jsonl` again (45
ms at 100,000 notes), as a start does. Changes queued for the file are
flushed first. Nothing polls: each lookup notes the time, and one
short-lived thread waits for the quiet stretch to end, unloads, trims
the heap and exits.

Prototype: `CALTERNAL_X_UNLOAD_SECS` overrides the 10 minutes for
measuring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Author
Owner

Replaced by #463, rebased onto current dev (train m, #444) as asked. Closing.

Replaced by #463, rebased onto current dev (train m, #444) as asked. Closing.
kayg closed this pull request 2026-10-15 22:40:50 +00:00
Some checks failed
CI / server (pull_request) Successful in 25s
CI / rust (pull_request) Successful in 2m16s
CI / web (pull_request) Failing after 42m22s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!460
No description provided.