LRU chunk eviction (bounded cache, default 1024 chunks / ~200MB)

The in-memory chunk+frame cache no longer grows unbounded as players
explore. An LRU policy (doubly-linked list + index map, O(1) per op)
evicts least-recently-used chunks when the cache exceeds MaxCachedChunks,
dropping both the chunk and its cached frame. Dirty chunks are skipped
until the autosave flushes them, so no edit is ever lost to eviction.

- world/cache.go: maxChunks field + order/index LRU bookkeeping; touch
  (move-to-front) on every chunkAt/Frame/SetBlock hit; evictIfNeeded on
  miss; NewCacheWithLimit constructor (0 = unbounded, backward-compat).
  Dirty chunks are bumped to MRU and left in place rather than doing
  region I/O under the cache mutex.
- server/server.go: Config.MaxCachedChunks (default 1024); New wires it
  into NewCacheWithLimit when a world dir is set.
- cmd/regionio/main.go: -maxcache flag.
- Tests: limit cap, LRU ordering (touched chunk survives), both-maps
  drop, dirty-keep, reload-on-access, and edits-survive-eviction+reload
  (end-to-end via the store).
This commit is contained in:
Master290 2026-06-25 13:56:06 +03:00
parent d7c80a8858
commit 65a7445a78
4 changed files with 269 additions and 10 deletions

View file

@ -24,6 +24,9 @@ type Config struct {
// read from and written to <WorldDir>/region/*.mca and player edits
// survive restarts. Empty disables persistence (in-memory only).
WorldDir string
// MaxCachedChunks bounds the in-memory chunk+frame cache (LRU). 0 means
// unbounded (use only for tests/flat worlds). At ~200KiB/chunk, 1024 ≈ 200MB.
MaxCachedChunks int
}
// DefaultConfig returns sensible defaults matching vanilla expectations.
@ -40,6 +43,9 @@ func DefaultConfig() Config {
// WorldDir defaults to "world" so the world persists by default;
// set to "" for a throwaway in-memory world.
WorldDir: "world",
// MaxCachedChunks keeps the live cache near 200MB at the default; the
// streamer's pre-gen ring and player view distance comfortably fit.
MaxCachedChunks: 1024,
}
}
@ -56,6 +62,8 @@ type Server struct {
func New(cfg Config) (*Server, error) {
gen := world.NewVanillaGenerator(cfg.WorldSeed)
if cfg.WorldDir == "" {
// No persistence; keep eviction off too (flat/test worlds expect full
// presence). Real servers set WorldDir and MaxCachedChunks together.
return &Server{cfg: cfg, chunks: world.NewCache(int32(cfg.CompressionThreshold), gen)}, nil
}
store, err := world.NewStore(cfg.WorldDir)
@ -64,7 +72,7 @@ func New(cfg Config) (*Server, error) {
}
return &Server{
cfg: cfg,
chunks: world.NewCacheWithStore(int32(cfg.CompressionThreshold), gen, store),
chunks: world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks),
store: store,
}, nil
}