Cache store: SSD wear protection -- compaction policy, daily write cap, drive-type behaviour, write stats #709

Closed
opened 2026-10-12 11:17:22 +00:00 by sky_line · 2 comments
Owner

Part of #715 (cache rewrite epic).

Compaction adds writes; keep SSD wear at or below today's file-per-item cache.

Scale: a heavy explorer might write ~12 GB/day of new cache data: ~0.7%/year of a 600 TBW 1 TB TLC drive, ~3%/year of a 150 TBW 500 GB QLC drive. Small, but bounded anyway:

  1. No compaction while the budget has room. Dead records stay until space is needed.
  2. Reclaim only mostly-dead segments (<= 25% live), rescuing only records with a real reuse count; everything else is evicted with the segment. Worst-case write amplification 1.33x.
  3. Daily compaction cap (e.g. 10% of the day's new data). Past the cap, whole segments are evicted without rescue -- extra misses, never extra wear.
  4. No writes on reads: access times live in the index and are saved with its snapshot (today both caches rewrite file timestamps on access).
  5. Drive-type behaviour: detect rotational vs. solid-state (Linux /sys/block/*/queue/rotational, Windows storage seek-penalty query). SSD: minimal compaction only, no locality rewrites. HDD: locality rewrites allowed (see the HDD locality ticket).
  6. Stats: bytes written per day (new / compaction), shown in the cache stats and logged.

Large sequential appends and whole-segment deletes (one TRIM each) are also the SSD-friendliest write pattern, lowering the drive's own internal write amplification compared with small-file churn.

Part of #715 (cache rewrite epic). Compaction adds writes; keep SSD wear at or below today's file-per-item cache. **Scale:** a heavy explorer might write ~12 GB/day of new cache data: ~0.7%/year of a 600 TBW 1 TB TLC drive, ~3%/year of a 150 TBW 500 GB QLC drive. Small, but bounded anyway: 1. **No compaction while the budget has room.** Dead records stay until space is needed. 2. **Reclaim only mostly-dead segments** (<= 25% live), rescuing only records with a real reuse count; everything else is evicted with the segment. Worst-case write amplification 1.33x. 3. **Daily compaction cap** (e.g. 10% of the day's new data). Past the cap, whole segments are evicted without rescue -- extra misses, never extra wear. 4. **No writes on reads:** access times live in the index and are saved with its snapshot (today both caches rewrite file timestamps on access). 5. **Drive-type behaviour:** detect rotational vs. solid-state (Linux `/sys/block/*/queue/rotational`, Windows storage seek-penalty query). SSD: minimal compaction only, no locality rewrites. HDD: locality rewrites allowed (see the HDD locality ticket). 6. **Stats:** bytes written per day (new / compaction), shown in the cache stats and logged. Large sequential appends and whole-segment deletes (one TRIM each) are also the SSD-friendliest write pattern, lowering the drive's own internal write amplification compared with small-file churn.
sky_line added this to the Slipstream project 2026-10-12 11:17:22 +00:00
Author
Owner

Interface audit (step 1 of #715): wear-relevant writes today and this ticket's slice of the store.

Writes today that the store removes or bounds

Writer Code What it writes
Asset cache access time llfilesystem.cpp:96-104, :380 file mtime on READ open, at most hourly per file
GPU-ready access time llgputexcache.cpp:492 touch mtime, hourly per file
Sound / cluster access time llsoundcache.cpp:322, llcleanclustercache.cpp:420 mtime, hourly per file
J2C access time lltexturecache.cpp:1387 + writeUpdatedEntries every 300 s (:908) 28-byte entries rewritten in place
J2C refinement lltexturecache.cpp:546 doWrite the whole body file again on every discard step
Asset purge lldiskcache.cpp:92, every 15 min deletes; stats 265k files per run

Slice of the interface

struct WearPolicy {
    bool ssd;                        // drive detection (Linux /sys/block/*/queue/rotational, Windows seek penalty)
    F32  reclaim_live_max = 0.25f;   // reclaim only segments <= 25% live
    U64  daily_compaction_cap_bytes; // 0 = 10% of the day's new bytes
    bool locality_rewrites;          // HDD only (#712)
};
Store::setWearPolicy(const WearPolicy&);
Store::touch(key);   // RAM only: last_access, prev_session_access, reuse_count, saved with the index snapshot
Stats: bytes_written_today_new, bytes_written_today_compaction, dead_bytes, segments
  • touch() never writes; access data rides the index snapshot (one sequential file).
  • Rescue during compaction is limited to records whose reuse_count > 0; everything else is evicted with the segment.
  • J2C upgrade appends count as new bytes (they replace today's body rewrites one for one). The writer queue's SUPERSEDE_PENDING flag drops an upgrade that is replaced before it reaches disk, which saves bytes compared with today.
  • Past the daily cap, whole segments are evicted without rescue.

Drop-in plan

Lands with the store core: in phase 1 only "no compaction while under budget" + whole-segment eviction + stats. Rescue, the daily cap and drive detection follow once eviction runs on real data. Stats show in the existing cache stats line and the log.

**Interface audit (step 1 of #715): wear-relevant writes today and this ticket's slice of the store.** ### Writes today that the store removes or bounds | Writer | Code | What it writes | |---|---|---| | Asset cache access time | llfilesystem.cpp:96-104, :380 | file mtime on READ open, at most hourly per file | | GPU-ready access time | llgputexcache.cpp:492 `touch` | mtime, hourly per file | | Sound / cluster access time | llsoundcache.cpp:322, llcleanclustercache.cpp:420 | mtime, hourly per file | | J2C access time | lltexturecache.cpp:1387 + `writeUpdatedEntries` every 300 s (:908) | 28-byte entries rewritten in place | | J2C refinement | lltexturecache.cpp:546 `doWrite` | the whole body file again on every discard step | | Asset purge | lldiskcache.cpp:92, every 15 min | deletes; stats 265k files per run | ### Slice of the interface ```cpp struct WearPolicy { bool ssd; // drive detection (Linux /sys/block/*/queue/rotational, Windows seek penalty) F32 reclaim_live_max = 0.25f; // reclaim only segments <= 25% live U64 daily_compaction_cap_bytes; // 0 = 10% of the day's new bytes bool locality_rewrites; // HDD only (#712) }; Store::setWearPolicy(const WearPolicy&); Store::touch(key); // RAM only: last_access, prev_session_access, reuse_count, saved with the index snapshot Stats: bytes_written_today_new, bytes_written_today_compaction, dead_bytes, segments ``` - `touch()` never writes; access data rides the index snapshot (one sequential file). - Rescue during compaction is limited to records whose `reuse_count` > 0; everything else is evicted with the segment. - J2C upgrade appends count as **new** bytes (they replace today's body rewrites one for one). The writer queue's SUPERSEDE_PENDING flag drops an upgrade that is replaced before it reaches disk, which saves bytes compared with today. - Past the daily cap, whole segments are evicted without rescue. ### Drop-in plan Lands with the store core: in phase 1 only "no compaction while under budget" + whole-segment eviction + stats. Rescue, the daily cap and drive detection follow once eviction runs on real data. Stats show in the existing cache stats line and the log.
Author
Owner

Fixed in 1.4.1 (4c26bd0138). Reopen if you still see it.

Fixed in 1.4.1 (4c26bd0138). Reopen if you still see it.
Sign in to join this conversation.
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
sky_line/Slipstream#709
No description provided.