393 lines
8.0 KiB
Go
393 lines
8.0 KiB
Go
package structr
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"github.com/zeebo/xxh3"
|
|
)
|
|
|
|
// IndexConfig defines config variables
|
|
// for initializing a struct index.
|
|
type IndexConfig struct {
|
|
|
|
// Fields should contain a comma-separated
|
|
// list of struct fields used when generating
|
|
// keys for this index. Nested fields should
|
|
// be specified using periods. An example:
|
|
// "Username,Favorites.Color"
|
|
//
|
|
// Field types supported include:
|
|
// - ~int
|
|
// - ~int8
|
|
// - ~int16
|
|
// - ~int32
|
|
// - ~int64
|
|
// - ~float32
|
|
// - ~float64
|
|
// - ~string
|
|
// - slices of above
|
|
// - ptrs of above
|
|
Fields string
|
|
|
|
// Multiple indicates whether to accept multiple
|
|
// possible values for any single index key. The
|
|
// default behaviour is to only accept one value
|
|
// and overwrite existing on any write operation.
|
|
Multiple bool
|
|
|
|
// AllowZero indicates whether to accept zero
|
|
// value fields in index keys. i.e. whether to
|
|
// index structs for this set of field values
|
|
// IF any one of those field values is the zero
|
|
// value for that type. The default behaviour
|
|
// is to skip indexing structs for this lookup
|
|
// when any of the indexing fields are zero.
|
|
AllowZero bool
|
|
}
|
|
|
|
// Index is an exposed Cache internal model, used to
|
|
// extract struct keys, generate hash checksums for them
|
|
// and store struct results by the init defined config.
|
|
// This model is exposed to provide faster lookups in the
|
|
// case that you would like to manually provide the used
|
|
// index via the Cache.___By() series of functions, or
|
|
// access the underlying index key generator.
|
|
type Index[StructType any] struct {
|
|
|
|
// name is the actual name of this
|
|
// index, which is the unparsed
|
|
// string value of contained fields.
|
|
name string
|
|
|
|
// backing data store of the index, containing
|
|
// the cached results contained within wrapping
|
|
// index_entry{} which also contains the exact
|
|
// key each result is stored under. the hash map
|
|
// only keys by the xxh3 hash checksum for speed.
|
|
data map[Hash]*list //[*index_entry[StructType]]
|
|
|
|
// struct fields encompassed by
|
|
// keys (+ hashes) of this index.
|
|
fields []structfield
|
|
|
|
// index flags:
|
|
// - 1 << 0 = unique
|
|
// - 1 << 1 = allow zero
|
|
flags uint8
|
|
}
|
|
|
|
// Key returns the configured fields as key, and hash sum of key.
|
|
func (i *Index[T]) Key(value T) ([]any, Hash, bool) {
|
|
h := get_hasher()
|
|
key, sum, ok := index_key(i, h, value)
|
|
hash_pool.Put(h)
|
|
return key, sum, ok
|
|
}
|
|
|
|
func is_unique(f uint8) bool {
|
|
const mask = uint8(1) << 0
|
|
return f&mask != 0
|
|
}
|
|
|
|
func set_is_unique(f *uint8) {
|
|
const mask = uint8(1) << 0
|
|
(*f) |= mask
|
|
}
|
|
|
|
func allow_zero(f uint8) bool {
|
|
const mask = uint8(1) << 1
|
|
return f&mask != 0
|
|
}
|
|
|
|
func set_allow_zero(f *uint8) {
|
|
const mask = uint8(1) << 1
|
|
(*f) |= mask
|
|
}
|
|
|
|
func init_index[T any](i *Index[T], config IndexConfig, max int) {
|
|
// Set name from the raw
|
|
// struct fields string.
|
|
i.name = config.Fields
|
|
|
|
// Set struct flags.
|
|
if config.AllowZero {
|
|
set_allow_zero(&i.flags)
|
|
}
|
|
if !config.Multiple {
|
|
set_is_unique(&i.flags)
|
|
}
|
|
|
|
// Split to get the containing struct fields.
|
|
fields := strings.Split(config.Fields, ",")
|
|
|
|
// Preallocate expected struct field slice.
|
|
i.fields = make([]structfield, len(fields))
|
|
|
|
// Get the reflected struct ptr type.
|
|
t := reflect.TypeOf((*T)(nil)).Elem()
|
|
|
|
for x, fieldName := range fields {
|
|
// Split name to account for nesting.
|
|
names := strings.Split(fieldName, ".")
|
|
|
|
// Look for usable struct field.
|
|
i.fields[x] = find_field(t, names)
|
|
}
|
|
|
|
// Initialize index_entry list store.
|
|
i.data = make(map[Hash]*list, max+1)
|
|
}
|
|
|
|
func index_key[T any](i *Index[T], h *xxh3.Hasher, value T) ([]any, Hash, bool) {
|
|
key := extract_fields(value, i.fields)
|
|
sum, zero := hash_sum(i.fields, h, key)
|
|
if zero && !allow_zero(i.flags) {
|
|
var zero Hash
|
|
return nil, zero, false
|
|
}
|
|
return key, sum, true
|
|
}
|
|
|
|
func index_hash[T any](i *Index[T], h *xxh3.Hasher, key []any) (Hash, bool) {
|
|
sum, zero := hash_sum(i.fields, h, key)
|
|
if zero && !allow_zero(i.flags) {
|
|
var zero Hash
|
|
return zero, false
|
|
}
|
|
return sum, true
|
|
}
|
|
|
|
func index_get[T any](i *Index[T], hash Hash, key []any) *list {
|
|
l := i.data[hash]
|
|
if l == nil {
|
|
return nil
|
|
}
|
|
entry := (*index_entry)(l.head.data)
|
|
if !is_equal(entry.key, key) {
|
|
return l
|
|
}
|
|
return l
|
|
}
|
|
|
|
func index_append[T any](c *Cache[T], i *Index[T], hash Hash, key []any, res *result) {
|
|
// Get list at key.
|
|
l := i.data[hash]
|
|
|
|
if l == nil {
|
|
|
|
// Allocate new list.
|
|
l = list_acquire()
|
|
i.data[hash] = l
|
|
|
|
} else if entry := (*index_entry)(l.head.data); //nocollapse
|
|
!is_equal(entry.key, key) {
|
|
|
|
// Collision! Drop all.
|
|
delete(i.data, hash)
|
|
|
|
// Iterate entries in list.
|
|
for x := 0; x < l.len; x++ {
|
|
|
|
// Pop current head.
|
|
list_remove(l, l.head)
|
|
|
|
// Extract result.
|
|
res := entry.result
|
|
|
|
// Drop index entry from res.
|
|
result_drop_index(res, i)
|
|
if len(res.indexed) == 0 {
|
|
|
|
// Old res now unused,
|
|
// release to mem pool.
|
|
result_release(c, res)
|
|
}
|
|
}
|
|
|
|
return
|
|
|
|
} else if is_unique(i.flags) {
|
|
|
|
// Remove current
|
|
// indexed entry.
|
|
list_remove(l, l.head)
|
|
|
|
// Get ptr to old
|
|
// entry before we
|
|
// release to pool.
|
|
res := entry.result
|
|
|
|
// Drop this index's key from
|
|
// old res now not indexed here.
|
|
result_drop_index(res, i)
|
|
if len(res.indexed) == 0 {
|
|
|
|
// Old res now unused,
|
|
// release to mem pool.
|
|
result_release(c, res)
|
|
}
|
|
}
|
|
|
|
// Acquire + setup index entry.
|
|
entry := index_entry_acquire()
|
|
entry.index = unsafe.Pointer(i)
|
|
entry.result = res
|
|
entry.key = key
|
|
entry.hash = hash
|
|
|
|
// Append to result's indexed entries.
|
|
res.indexed = append(res.indexed, entry)
|
|
|
|
// Add index entry to index list.
|
|
list_push_front(l, &entry.elem)
|
|
}
|
|
|
|
func index_delete[T any](c *Cache[T], i *Index[T], hash Hash, key []any, fn func(*result)) {
|
|
if fn == nil {
|
|
panic("nil fn")
|
|
}
|
|
|
|
// Get list at hash.
|
|
l := i.data[hash]
|
|
if l == nil {
|
|
return
|
|
}
|
|
|
|
entry := (*index_entry)(l.head.data)
|
|
|
|
// Check contains expected key for hash.
|
|
if !is_equal(entry.key, key) {
|
|
return
|
|
}
|
|
|
|
// Delete data at hash.
|
|
delete(i.data, hash)
|
|
|
|
// Iterate entries in list.
|
|
for x := 0; x < l.len; x++ {
|
|
|
|
// Pop current head.
|
|
entry := (*index_entry)(l.head.data)
|
|
list_remove(l, l.head)
|
|
|
|
// Extract result.
|
|
res := entry.result
|
|
|
|
// Call hook.
|
|
fn(res)
|
|
|
|
// Drop index entry from res.
|
|
result_drop_index(res, i)
|
|
}
|
|
|
|
// Release to pool.
|
|
list_release(l)
|
|
}
|
|
|
|
func index_delete_entry[T any](c *Cache[T], entry *index_entry) {
|
|
// Get from entry.
|
|
i := (*Index[T])(entry.index)
|
|
|
|
// Get list at hash sum.
|
|
l := i.data[entry.hash]
|
|
if l == nil {
|
|
return
|
|
}
|
|
|
|
// Remove entry from list.
|
|
list_remove(l, &entry.elem)
|
|
if l.len == 0 {
|
|
|
|
// Remove list from map.
|
|
delete(i.data, entry.hash)
|
|
|
|
// Release to pool.
|
|
list_release(l)
|
|
}
|
|
|
|
// Extract result.
|
|
res := entry.result
|
|
|
|
// Drop index entry from res.
|
|
result_drop_index(res, i)
|
|
}
|
|
|
|
var entry_pool sync.Pool
|
|
|
|
type index_entry struct {
|
|
// elem contains the list element
|
|
// appended to each per-hash list
|
|
// within the Index{} type. the
|
|
// contained value is a self-ref.
|
|
elem list_elem
|
|
|
|
// index is the Index{} this
|
|
// index_entry{} is stored in.
|
|
index unsafe.Pointer
|
|
|
|
// result is the actual
|
|
// underlying result stored
|
|
// within the index. this
|
|
// also contains a ref to
|
|
// this *index_entry in order
|
|
// to track indices each result
|
|
// is currently stored under.
|
|
result *result
|
|
|
|
// key contains the actual
|
|
// key this item was stored
|
|
// under, used for collision
|
|
// check.
|
|
key []any
|
|
|
|
// hash contains computed
|
|
// hash checksum of .key.
|
|
hash Hash
|
|
}
|
|
|
|
func index_entry_acquire() *index_entry {
|
|
// Acquire from pool.
|
|
v := entry_pool.Get()
|
|
if v == nil {
|
|
v = new(index_entry)
|
|
}
|
|
|
|
// Cast index_entry value.
|
|
entry := v.(*index_entry)
|
|
|
|
// Set index list elem entry on itself.
|
|
entry.elem.data = unsafe.Pointer(entry)
|
|
|
|
return entry
|
|
}
|
|
|
|
func index_entry_release(entry *index_entry) {
|
|
var zero Hash
|
|
|
|
// Reset index entry.
|
|
entry.elem.data = nil
|
|
entry.index = nil
|
|
entry.result = nil
|
|
entry.key = nil
|
|
entry.hash = zero
|
|
|
|
// Release to pool.
|
|
entry_pool.Put(entry)
|
|
}
|
|
|
|
// is_equal returns whether 2 key slices are equal.
|
|
func is_equal(k1, k2 []any) bool {
|
|
if len(k1) != len(k2) {
|
|
return false
|
|
}
|
|
for i := range k1 {
|
|
if k1[i] != k2[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|