Some checks failed
Go / build-and-release (push) Has been cancelled
- Add generic LRUCache[K, V] implementation using container/list for O(1) ops - Replace random 50% eviction with proper LRU eviction in SerialCache - Cache now starts empty and grows on demand up to configured limits - Use [32]byte keys instead of string([]byte) to avoid allocation overhead - Single-entry eviction at capacity instead of 50% bulk clearing - Add comprehensive unit tests and benchmarks for LRUCache - Benchmarks show ~32-34 ns/op with 0 allocations for Get/Put Files modified: - pkg/database/lrucache.go: New generic LRU cache implementation - pkg/database/lrucache_test.go: Unit tests and benchmarks - pkg/database/serial_cache.go: Refactored to use LRUCache 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
331 lines
9.2 KiB
Go
331 lines
9.2 KiB
Go
//go:build !(js && wasm)
|
|
|
|
package database
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/dgraph-io/badger/v4"
|
|
"lol.mleku.dev/chk"
|
|
"next.orly.dev/pkg/database/bufpool"
|
|
"next.orly.dev/pkg/database/indexes"
|
|
"next.orly.dev/pkg/database/indexes/types"
|
|
)
|
|
|
|
// SerialCache provides LRU caching for pubkey and event ID serial lookups.
|
|
// This is critical for compact event decoding performance since every event
|
|
// requires looking up the author pubkey and potentially multiple tag references.
|
|
//
|
|
// The cache uses LRU eviction and starts empty, growing on demand up to the
|
|
// configured limits. This provides better memory efficiency than pre-allocation
|
|
// and better hit rates than random eviction.
|
|
type SerialCache struct {
|
|
// Pubkey serial -> full pubkey (for decoding)
|
|
pubkeyBySerial *LRUCache[uint64, []byte]
|
|
|
|
// Pubkey bytes -> serial (for encoding)
|
|
// Uses [32]byte as key since []byte isn't comparable
|
|
serialByPubkey *LRUCache[[32]byte, uint64]
|
|
|
|
// Event serial -> full event ID (for decoding)
|
|
eventIdBySerial *LRUCache[uint64, []byte]
|
|
|
|
// Event ID bytes -> serial (for encoding)
|
|
serialByEventId *LRUCache[[32]byte, uint64]
|
|
|
|
// Limits (for stats reporting)
|
|
maxPubkeys int
|
|
maxEventIds int
|
|
}
|
|
|
|
// NewSerialCache creates a new serial cache with the specified maximum sizes.
|
|
// The cache starts empty and grows on demand up to these limits.
|
|
func NewSerialCache(maxPubkeys, maxEventIds int) *SerialCache {
|
|
if maxPubkeys <= 0 {
|
|
maxPubkeys = 100000 // Default 100k pubkeys
|
|
}
|
|
if maxEventIds <= 0 {
|
|
maxEventIds = 500000 // Default 500k event IDs
|
|
}
|
|
return &SerialCache{
|
|
pubkeyBySerial: NewLRUCache[uint64, []byte](maxPubkeys),
|
|
serialByPubkey: NewLRUCache[[32]byte, uint64](maxPubkeys),
|
|
eventIdBySerial: NewLRUCache[uint64, []byte](maxEventIds),
|
|
serialByEventId: NewLRUCache[[32]byte, uint64](maxEventIds),
|
|
maxPubkeys: maxPubkeys,
|
|
maxEventIds: maxEventIds,
|
|
}
|
|
}
|
|
|
|
// CachePubkey adds a pubkey to the cache in both directions.
|
|
func (c *SerialCache) CachePubkey(serial uint64, pubkey []byte) {
|
|
if len(pubkey) != 32 {
|
|
return
|
|
}
|
|
|
|
// Copy pubkey to avoid referencing external slice
|
|
pk := make([]byte, 32)
|
|
copy(pk, pubkey)
|
|
|
|
// Cache serial -> pubkey (for decoding)
|
|
c.pubkeyBySerial.Put(serial, pk)
|
|
|
|
// Cache pubkey -> serial (for encoding)
|
|
var key [32]byte
|
|
copy(key[:], pubkey)
|
|
c.serialByPubkey.Put(key, serial)
|
|
}
|
|
|
|
// GetPubkeyBySerial returns the pubkey for a serial from cache.
|
|
func (c *SerialCache) GetPubkeyBySerial(serial uint64) (pubkey []byte, found bool) {
|
|
return c.pubkeyBySerial.Get(serial)
|
|
}
|
|
|
|
// GetSerialByPubkey returns the serial for a pubkey from cache.
|
|
func (c *SerialCache) GetSerialByPubkey(pubkey []byte) (serial uint64, found bool) {
|
|
if len(pubkey) != 32 {
|
|
return 0, false
|
|
}
|
|
var key [32]byte
|
|
copy(key[:], pubkey)
|
|
return c.serialByPubkey.Get(key)
|
|
}
|
|
|
|
// CacheEventId adds an event ID to the cache in both directions.
|
|
func (c *SerialCache) CacheEventId(serial uint64, eventId []byte) {
|
|
if len(eventId) != 32 {
|
|
return
|
|
}
|
|
|
|
// Copy event ID to avoid referencing external slice
|
|
eid := make([]byte, 32)
|
|
copy(eid, eventId)
|
|
|
|
// Cache serial -> event ID (for decoding)
|
|
c.eventIdBySerial.Put(serial, eid)
|
|
|
|
// Cache event ID -> serial (for encoding)
|
|
var key [32]byte
|
|
copy(key[:], eventId)
|
|
c.serialByEventId.Put(key, serial)
|
|
}
|
|
|
|
// GetEventIdBySerial returns the event ID for a serial from cache.
|
|
func (c *SerialCache) GetEventIdBySerial(serial uint64) (eventId []byte, found bool) {
|
|
return c.eventIdBySerial.Get(serial)
|
|
}
|
|
|
|
// GetSerialByEventId returns the serial for an event ID from cache.
|
|
func (c *SerialCache) GetSerialByEventId(eventId []byte) (serial uint64, found bool) {
|
|
if len(eventId) != 32 {
|
|
return 0, false
|
|
}
|
|
var key [32]byte
|
|
copy(key[:], eventId)
|
|
return c.serialByEventId.Get(key)
|
|
}
|
|
|
|
// DatabaseSerialResolver implements SerialResolver using the database and cache.
|
|
type DatabaseSerialResolver struct {
|
|
db *D
|
|
cache *SerialCache
|
|
}
|
|
|
|
// NewDatabaseSerialResolver creates a new resolver.
|
|
func NewDatabaseSerialResolver(db *D, cache *SerialCache) *DatabaseSerialResolver {
|
|
return &DatabaseSerialResolver{db: db, cache: cache}
|
|
}
|
|
|
|
// GetOrCreatePubkeySerial implements SerialResolver.
|
|
func (r *DatabaseSerialResolver) GetOrCreatePubkeySerial(pubkey []byte) (serial uint64, err error) {
|
|
if len(pubkey) != 32 {
|
|
return 0, errors.New("pubkey must be 32 bytes")
|
|
}
|
|
|
|
// Check cache first
|
|
if s, found := r.cache.GetSerialByPubkey(pubkey); found {
|
|
return s, nil
|
|
}
|
|
|
|
// Use existing function which handles creation
|
|
ser, err := r.db.GetOrCreatePubkeySerial(pubkey)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
serial = ser.Get()
|
|
|
|
// Cache it
|
|
r.cache.CachePubkey(serial, pubkey)
|
|
|
|
return serial, nil
|
|
}
|
|
|
|
// GetPubkeyBySerial implements SerialResolver.
|
|
func (r *DatabaseSerialResolver) GetPubkeyBySerial(serial uint64) (pubkey []byte, err error) {
|
|
// Check cache first
|
|
if pk, found := r.cache.GetPubkeyBySerial(serial); found {
|
|
return pk, nil
|
|
}
|
|
|
|
// Look up in database
|
|
ser := new(types.Uint40)
|
|
if err = ser.Set(serial); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pubkey, err = r.db.GetPubkeyBySerial(ser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache it
|
|
r.cache.CachePubkey(serial, pubkey)
|
|
|
|
return pubkey, nil
|
|
}
|
|
|
|
// GetEventSerialById implements SerialResolver.
|
|
func (r *DatabaseSerialResolver) GetEventSerialById(eventId []byte) (serial uint64, found bool, err error) {
|
|
if len(eventId) != 32 {
|
|
return 0, false, errors.New("event ID must be 32 bytes")
|
|
}
|
|
|
|
// Check cache first
|
|
if s, ok := r.cache.GetSerialByEventId(eventId); ok {
|
|
return s, true, nil
|
|
}
|
|
|
|
// Look up in database using existing GetSerialById
|
|
ser, err := r.db.GetSerialById(eventId)
|
|
if err != nil {
|
|
// Not found is not an error - just return found=false
|
|
return 0, false, nil
|
|
}
|
|
|
|
serial = ser.Get()
|
|
|
|
// Cache it
|
|
r.cache.CacheEventId(serial, eventId)
|
|
|
|
return serial, true, nil
|
|
}
|
|
|
|
// GetEventIdBySerial implements SerialResolver.
|
|
func (r *DatabaseSerialResolver) GetEventIdBySerial(serial uint64) (eventId []byte, err error) {
|
|
// Check cache first
|
|
if eid, found := r.cache.GetEventIdBySerial(serial); found {
|
|
return eid, nil
|
|
}
|
|
|
|
// Look up in database - use SerialEventId index
|
|
ser := new(types.Uint40)
|
|
if err = ser.Set(serial); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
eventId, err = r.db.GetEventIdBySerial(ser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache it
|
|
r.cache.CacheEventId(serial, eventId)
|
|
|
|
return eventId, nil
|
|
}
|
|
|
|
// GetEventIdBySerial looks up an event ID by its serial number.
|
|
// Uses the SerialEventId index (sei prefix).
|
|
func (d *D) GetEventIdBySerial(ser *types.Uint40) (eventId []byte, err error) {
|
|
keyBuf := bufpool.GetSmall()
|
|
defer bufpool.PutSmall(keyBuf)
|
|
if err = indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
|
|
err = d.View(func(txn *badger.Txn) error {
|
|
item, gerr := txn.Get(keyBuf.Bytes())
|
|
if chk.E(gerr) {
|
|
return gerr
|
|
}
|
|
|
|
return item.Value(func(val []byte) error {
|
|
eventId = make([]byte, len(val))
|
|
copy(eventId, val)
|
|
return nil
|
|
})
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, errors.New("event ID not found for serial")
|
|
}
|
|
|
|
return eventId, nil
|
|
}
|
|
|
|
// StoreEventIdSerial stores the mapping from event serial to full event ID.
|
|
// This is called during event save to enable later reconstruction.
|
|
func (d *D) StoreEventIdSerial(txn *badger.Txn, serial uint64, eventId []byte) error {
|
|
if len(eventId) != 32 {
|
|
return errors.New("event ID must be 32 bytes")
|
|
}
|
|
|
|
ser := new(types.Uint40)
|
|
if err := ser.Set(serial); err != nil {
|
|
return err
|
|
}
|
|
|
|
keyBuf := bufpool.GetSmall()
|
|
defer bufpool.PutSmall(keyBuf)
|
|
if err := indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
|
return err
|
|
}
|
|
|
|
return txn.Set(bufpool.CopyBytes(keyBuf), eventId)
|
|
}
|
|
|
|
// SerialCacheStats holds statistics about the serial cache.
|
|
type SerialCacheStats struct {
|
|
PubkeysCached int // Number of pubkeys currently cached
|
|
PubkeysMaxSize int // Maximum pubkey cache size
|
|
EventIdsCached int // Number of event IDs currently cached
|
|
EventIdsMaxSize int // Maximum event ID cache size
|
|
PubkeyMemoryBytes int // Estimated memory usage for pubkey cache
|
|
EventIdMemoryBytes int // Estimated memory usage for event ID cache
|
|
TotalMemoryBytes int // Total estimated memory usage
|
|
}
|
|
|
|
// Stats returns statistics about the serial cache.
|
|
func (c *SerialCache) Stats() SerialCacheStats {
|
|
pubkeysCached := c.pubkeyBySerial.Len()
|
|
eventIdsCached := c.eventIdBySerial.Len()
|
|
|
|
// Memory estimation:
|
|
// Each entry has: key + value + list.Element overhead + map entry overhead
|
|
// - Pubkey by serial: 8 (key) + 32 (value) + ~80 (list) + ~16 (map) ≈ 136 bytes
|
|
// - Serial by pubkey: 32 (key) + 8 (value) + ~80 (list) + ~16 (map) ≈ 136 bytes
|
|
// Total per pubkey (both directions): ~272 bytes
|
|
// Similarly for event IDs: ~272 bytes per entry (both directions)
|
|
pubkeyMemory := pubkeysCached * 272
|
|
eventIdMemory := eventIdsCached * 272
|
|
|
|
return SerialCacheStats{
|
|
PubkeysCached: pubkeysCached,
|
|
PubkeysMaxSize: c.maxPubkeys,
|
|
EventIdsCached: eventIdsCached,
|
|
EventIdsMaxSize: c.maxEventIds,
|
|
PubkeyMemoryBytes: pubkeyMemory,
|
|
EventIdMemoryBytes: eventIdMemory,
|
|
TotalMemoryBytes: pubkeyMemory + eventIdMemory,
|
|
}
|
|
}
|
|
|
|
// SerialCacheStats returns statistics about the serial cache.
|
|
func (d *D) SerialCacheStats() SerialCacheStats {
|
|
if d.serialCache == nil {
|
|
return SerialCacheStats{}
|
|
}
|
|
return d.serialCache.Stats()
|
|
}
|