Skip to content
quarry Artifact [caa7f1cf10]
Login

Artifact [caa7f1cf10]

Artifact caa7f1cf1038613a99362ba3145bacf838ec15132bcc52765c9bfbaf6d1fc58d:


package zitcli

import (
	"container/list"
	"sync"
)

// lru is a byte-bounded, least-recently-used memo over zit reads.
//
// Bounded by BYTES, not entries, because the values are store content:
// an entry-count cap is fine for listings and meaningless for anything
// whose entries vary by orders of magnitude — one Cat can be a 30MB
// blob, and sixty-four of them is two gigabytes of resident memory. A
// diff has the same shape: a one-line fix and a thousand-file merge are
// both one entry and nothing like one cost.
//
// Per repository rather than shared across the fleet. A shared budget
// uses memory better, but it lets one busy store evict another's
// entries, so a reader on a quiet repository pays for traffic they have
// nothing to do with. Predictable beats optimal here.
//
// EVERY key must be derived from an immutable version. That is not a
// property this type can check, so it is the caller's obligation and the
// reason ops resolve names to hashes before reading — see
// Deps.resolveVersion. A moving name cached here would serve stale
// content forever.
type lru[V any] struct {
	mu     sync.Mutex
	max    int64
	used   int64
	ll     *list.List               // front = most recently used
	items  map[string]*list.Element // key -> element holding *entry[V]
	sizeOf func(V) int64
}

type entry[V any] struct {
	key  string
	val  V
	size int64
}

func newLRU[V any](max int64, sizeOf func(V) int64) *lru[V] {
	return &lru[V]{max: max, ll: list.New(), items: map[string]*list.Element{}, sizeOf: sizeOf}
}

// get returns a cached value and marks it recently used. The value is
// the cached one, so callers must not mutate it — every caller hands it
// straight to a reader or a parser.
func (c *lru[V]) get(key string) (V, bool) {
	var zero V
	if c == nil {
		return zero, false
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	el, ok := c.items[key]
	if !ok {
		return zero, false
	}
	c.ll.MoveToFront(el)
	return el.Value.(*entry[V]).val, true
}

// put stores a value, evicting least-recently-used entries until it
// fits. A single value larger than the whole budget is NOT cached and
// not an error: admitting it would evict everything else to hold one
// item, which is worse than missing.
func (c *lru[V]) put(key string, val V) {
	if c == nil {
		return
	}
	size := c.sizeOf(val)
	if size > c.max {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if el, ok := c.items[key]; ok {
		e := el.Value.(*entry[V])
		c.used -= e.size
		e.val, e.size = val, size
		c.used += e.size
		c.ll.MoveToFront(el)
		return
	}
	e := &entry[V]{key: key, val: val, size: size}
	c.items[key] = c.ll.PushFront(e)
	c.used += e.size
	for c.used > c.max {
		back := c.ll.Back()
		if back == nil {
			return
		}
		old := c.ll.Remove(back).(*entry[V])
		delete(c.items, old.key)
		c.used -= old.size
	}
}

// stats reports entries and bytes held, for tests and diagnostics.
func (c *lru[V]) stats() (entries int, bytes int64) {
	if c == nil {
		return 0, 0
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	return len(c.items), c.used
}

// blobCache is the LRU over file contents.
type blobCache = lru[[]byte]

func newBlobCache(max int64) *blobCache {
	return newLRU[[]byte](max, func(v []byte) int64 { return int64(len(v)) })
}

// diffCache is the LRU over check-in diffs.
//
// The pair of hashes is immutable, so a hit is permanently correct and
// this memo never needs invalidating — the reason a diff is worth
// caching at all. What it does need is a bound that reflects what a
// diff can weigh, which is why it is the same byte-budgeted structure
// as the blob cache rather than a count of entries.
type diffCache = lru[diffResult]

func newDiffCache(max int64) *diffCache {
	return newLRU[diffResult](max, func(v diffResult) int64 {
		// The unified text dominates; the entry list is names and
		// statuses. Counted rather than ignored because a merge across
		// a thousand files carries a thousand of them.
		n := int64(len(v.unified))
		for _, e := range v.entries {
			n += int64(len(e.Name) + len(e.Status))
		}
		return n
	})
}