aboutsummaryrefslogtreecommitdiff
package clock

import (
	"hash/maphash"
	"runtime"

	"lindenii.org/go/lgo/intconv"
)

// maxShards bounds the shard count.
//
// Keep it relatively modest
// so the per-shard budget
// stays large enough to admit sizable values.
const maxShards = 16

// WeightFunc reports one entry's weight, used for eviction budgeting.
type WeightFunc[K comparable, V any] func(key K, value V) uint64

// Cache is a concurrent, weight-bounded cache
// with CLOCK eviction.
//
// Reads are lock-free;
// writes lock only the shard that owns the key.
//
// Labels: MT-Safe.
type Cache[K comparable, V any] struct {
	shards   []*shard[K, V]
	seed     maphash.Seed
	mask     uint64
	weightFn WeightFunc[K, V]
}

// New returns a cache bounded to maxWeight total weight,
// weighing entries with weightFn.
//
// New panics if weightFn is nil.
func New[K comparable, V any](maxWeight uint64, weightFn WeightFunc[K, V]) *Cache[K, V] {
	if weightFn == nil {
		panic("internal/clock: nil weight function")
	}

	count, mask := shardLayout(maxWeight)
	perShard := maxWeight / (mask + 1)

	shards := make([]*shard[K, V], count)
	for i := range shards {
		shards[i] = newShard[K, V](perShard)
	}

	return &Cache[K, V]{
		shards:   shards,
		seed:     maphash.MakeSeed(),
		mask:     mask,
		weightFn: weightFn,
	}
}

// shardLayout picks a power-of-two shard count and its address mask.
//
// Tracks GOMAXPROCS, capped at maxShards,
// and is shrunk so the per-shard budget
// stays at least one while maxWeight is nonzero.
func shardLayout(maxWeight uint64) (int, uint64) {
	count := 1
	for count < runtime.GOMAXPROCS(0) && count < maxShards {
		count *= 2
	}

	countU, err := intconv.IntToUint64(count)
	if err != nil {
		return 1, 0
	}

	for countU > maxWeight && countU > 1 {
		count /= 2
		countU /= 2
	}

	return count, countU - 1
}

// shardFor returns the shard that owns key.
func (cache *Cache[K, V]) shardFor(key K) *shard[K, V] {
	return cache.shards[maphash.Comparable(cache.seed, key)&cache.mask]
}