When a 2 GiB file is committed via |commit %base, Clay stores the content atom
twice in its Nock-level state:
lat.ran((map lobe page)) — the content-addressed object store, keyed by SHA-512 hash. Stores[%mime [mite [len atom]]]as apage.mim.dom((map path mime)) — the mime cache for unix-mounted desks. Stores[mite [len atom]]as amime, keyed by path.
This is Clay's intentional design: lat.ran is the permanent content-addressable
store; mim.dom is a fast-path cache for unix sync and %ergo delivery. Both hold
the same 2 GiB atom. Since both are bob atoms (tiny 24-byte loom stubs pointing to
.urb/bob/), the loom only dirties ~48 bytes for the stubs themselves. The 4.4 GiB
dirty loom observed in vmmap is not the stubs — it is the atoms getting materialized
during event processing.
The path during |commit %base processing in the serf:
%intoarrives carrying a bob atom for the large file.- Clay calls
page-to-lobe→(shax (jam page))— jamming the page callsu3r_met/u3r_byteson the bob atom → materializes 2 GiB into the loom temporarily, then frees it. - Clay runs Ford (
checkout-changes) to type-check the file → may call retrieve functions on the bob atom again → materializes again. - Clay stores the bob atom in both
lat.ranandmim.domas bob stubs (fine — 24 bytes each). u3m_savesnapshots the loom. At this point only the two tiny bob stubs remain, but the loom pages dirtied by the jam buffer stay dirty until the next epoch snapshot cycle reclaims them via copy-on-write.
The kernel does not un-dirty loom pages just because the loom allocator frees a region — physical pages remain resident until the OS evicts them or the snapshot cycle reclaims them. This is why vmmap shows 4.4 GiB dirty after the commit even though the steady-state logical loom content is tiny.
When u3r_blob_load materializes a bob atom, rewrite the bob atom in-place as a
forward pointer to the materialized normal atom:
- Set
u3a_fwd_flag(bit 30 oflen_w) alongsideu3a_blob_flag - Store the loom offset of the materialized normal atom in
buf_w[0] - All retrieve functions check
u3a_is_fwdfirst, follow the pointer, skip disk I/O _me_lose: when a promoted bob is freed, decrement the target's refcount_ca_take: copy the fwd pointer, increment target refcount
This means the 2 GiB atom is loaded from disk once and reused for all accesses
within the event. The jam in page-to-lobe, Ford's type-check, and the mim.dom
update all share a single materialized copy.
Tradeoff: the materialized atom lives on the loom for the duration of the event, and is only freed when the last bob stub referencing it is freed. For a file in Clay's permanent state, this means the 2 GiB normal atom lives in the loom forever (until the file is deleted from Clay). Steady-state memory would be worse than the current behavior, which keeps only two tiny bob stubs.
Option A is not the right approach for permanent Clay state.
If page-to-lobe could compute its lobe hash by streaming SHA-512 directly over
the blob store file rather than via (jam page), the 2 GiB atom would never need to
be materialized for the lobe computation. This is the correct architectural fix: the
blob store file is already the canonical byte representation, so hashing it directly
(with a u3_blob_hash_fd C function exposed as a jet) is semantically equivalent
and avoids the jam entirely.
Requires a Hoon-level change to Clay's page-to-lobe arm and a new jet.
A C-level hash table (keyed by bob loom address) that stores the materialized normal
atom, initialized at event start and cleared at event end. All u3r_blob_load calls
check this cache first. This reduces materializations from O(calls) to O(1) per bob
atom per event.
Tradeoff: requires threading event lifecycle callbacks through the noun layer.
The cached atoms must be held with incremented refcounts for their duration.
Adds complexity without fixing the fundamental page-to-lobe issue.
Bob atoms in Clay's state are tiny stubs (24 bytes each). Materialization is temporary — freed after each retrieve operation. The steady-state loom after a snapshot is small. The 4.4 GiB dirty reading is a vmmap artifact of dirty-but-freed loom pages, not live data. Peak memory during the commit event is high, but short-lived, and the OS will page out freed loom regions under memory pressure.
The highest-impact fix is Option B (Hoon-level, bypass jam in page-to-lobe
for blob-backed atoms). The lobe is a content hash; the blob store file is the
content, so hashing it directly is both correct and avoids ever materializing the
atom into the loom for hashing purposes.
Option C is a lower-risk incremental improvement that reduces repeated materialization within a single event, at the cost of added complexity.
Option D is defensible for now: the 2× peak is bounded (it occurs only during the commit event, not at steady state), and the OS handles memory pressure via paging. Since the vmmap dirty pages are freed loom regions — not live data — the actual working set after the event is small.
The right sequencing:
- Ship the current implementation (Option D) — it works correctly.
- Implement Option B as a follow-on: jet a
page-to-lobe-blobpath that calls a C function to stream-hash the blob file, bypassingjamfor blob-backed pages.