Cache store: append-only segment files (replaces file-per-item for textures and assets) #708
Labels
No labels
amd
avatars
bug
builder
cannot-reproduce
crash
epic
feature
from-viewer
gl-removal
known-limitation
monitoring
parity
perf
platform
priority
critical
priority
high
priority
low
priority
medium
provenance
rendering
rt
status
in-progress
status
testing
status
up-next
triage
ui
upstream
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
sky_line/Slipstream#708
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #715 (cache rewrite epic).
Why: every cache is one file per item today (J2C bodies 42.9k files, GPU-ready 29.6k, assets 265k in one folder). Costs: a seek per item on hard disks, slow file opens on Windows (antivirus scans each open), a 4 KB minimum per file (55% of GPU-ready files are <= 16 KB), slow startup scans and purges.
Lessons from LL's old single-file VFS (removed 2020): any unclean exit wiped the whole cache; one mutex serialized all disk I/O; a pre-sized file with its own 1 KB block allocator (fragmentation, corruption handling by deletion); a second viewer instance hit sharing violations; space was never given back. The design below keeps the filesystem's robustness without the per-file costs.
Design
pread(not mmap -- a truncated mapped file is a SIGBUS). One writer thread appends. No global lock.Interface audit (step 1 of #715): what the segment store replaces and how it drops in.
Replaces
purge(), :235clearCache(), :351LLPurgeDiskCacheThread(every 15 min, stats every file)metaDataToFilepath(sl_cache_<uuid>_0.asset, type ignored)doRead:282 /doWrite:546,texture.entries+texture.cache+ body files,purgeTextures*:1786/:1876,purgeAllTextures:1736run_scan/run_read/run_write:221-300,evict:182These stay files: llaudio/llsoundcache.cpp (the audio back end loads WAVs by path) and newview/llvocache.cpp (gets only the codec, #713).
Store core (this ticket's slice)
<cache>/store/:lock,index.snap(+.prev),seg-<seq>.dat(128-256 MB, append-only).lookup()never does I/O. Snapshot saved periodically and at exit; at startup load the snapshot, then scan only the segment tails written after it.preadfinished segments from any thread. Segments are refcounted so compaction deletes only after the last reader lets go (share-delete on Windows).format_versionreplaces the whole-cache wipes (LocalCacheVersion,DiskCacheVersion, the J2C encoder string, gputexFILE_VERSION).<cache>/storejoins the folders deleted on a cache-location change (llappviewer.cpp:9105-9115). Today the second instance is read-only for textures, gputex, sounds, cluster and objects, but not for assets: it writes freely and runs the 15-minute purge over the shared folder (purge thread started outside the read-only check, llappviewer.cpp:9156).API sketch:
lookup / touch / read / readRange / readSync / readRangeSync / write(WriteSource, meta, flags) / beginStaged -> append, commit(final_key) | abort / remove / removeAll(kind) / stats / forEach. Full sketch in the #715 comment.Drop-in plan
indra/llcachestore+ tests: crash at each write step, corrupt / truncated records, disk full (fault injection), two instances.LLGpuTexCache's current static API (renderer untouched).LLTextureCacheadapter. One record per texture = the whole J2C prefix (the 600-byte split goes).readRangeSync(offset, size)covers the fetcher's offset reads (lltexturefetch.cpp:1256). Writes use SUPERSEDE_PENDING + SKIP_IF_NOT_LARGER.LLFileSystemadapter. READ = pread at record offset + position (seek is free, so the Ogg decoder'sov_callbackskeep working). WRITE/APPEND = a per-UUID staging table that outlives theLLFileSystemobjects: the UDP transfer and xfer code opens a new APPEND object per packet (lltransfertargetvfile.cpp:141, llxfer_vfile.cpp:263/317) and checksgetExistson a half-received temp (llxfer_vfile.cpp:82/337), so READ / exists / size answer from staged bytes;rename(new_id)commits one record (the download temp-UUID flow), WRITE restarts,removeaborts, session end commits. PIN_SESSION so a fresh asset is not evicted before its callback reads it. Phase 1 keeps UUID-only keys with the type in meta. Today the type is ignored, and llfloaterauction.cpp:206/216 relies on that by accident.READ_WRITE+setSizesparse file + preamble flags (llmeshrepository.cpp:4336-4365, 4414+) move to per-part records (header, LOD0-3, skin, physics convex, physics mesh) in phase 1. About 14 sites in one file; them*InCacheflags become "record exists".Placement design (decided 2026-10-18, see #715)
1. Placement hint on every write. It is computed when the bytes arrive, from what is using the asset at that moment:
SHAREDSHAREDSHAREDA wrong hint only costs locality.
2. Per-region runs, still one writer and one append stream. The writer buffers records per hint (up to ~4 MB or ~2 s per region) and appends each batch as one contiguous run. Segments hold runs from many regions, so there are no extra open files and no half-empty per-region files for regions you only passed through. A crash loses only the unflushed buffer, which is re-downloaded.
3. Region manifest (its own record kind, loaded on demand, ~70 KB for a 3,000-texture region):
Manifests are updated on each visit.
4. Promotion without copying. A record in region A's run that is later used from region B is flagged shared in the index. It stays where it is and is no longer tied to A's lifetime.
Tests to add: a hint that disagrees with later use (it must still be served correctly), losing the buffer in a crash, a manifest that references dead records, and a region UUID whose grid handle changes.
Fixed in 1.4.1 (
4c26bd0138). Reopen if you still see it.