[performance] cache library performance enhancements (updates go-structr => v0.2.0) (#2575)

* update go-structr => v0.2.0

* update readme

* whoops, fix the link
This commit is contained in:
kim
2024-01-26 12:14:10 +00:00
committed by GitHub
parent c946d02c1f
commit 07207e71e9
36 changed files with 4880 additions and 1379 deletions

View File

@@ -2,4 +2,74 @@
A performant struct caching library with automated indexing by arbitrary combinations of fields, including support for negative results (errors!). An example use case is in database lookups.
Some example code of how you can use `go-structr` in your application:
```golang
type Cached struct {
Username string
Domain string
URL string
CountryCode int
}
var c structr.Cache[*Cached]
c.Init(structr.Config[*Cached]{
// Fields this cached struct type
// will be indexed and stored under.
Indices: []structr.IndexConfig{
{Fields: "Username,Domain", AllowZero: true},
{Fields: "URL"},
{Fields: "CountryCode", Multiple: true},
},
// Maximum LRU cache size before
// new entries cause evictions.
MaxSize: 1000,
// User provided value copy function to
// reduce need for reflection + ensure
// concurrency safety for returned values.
CopyValue: func(c *Cached) *Cached {
c2 := new(Cached)
*c2 = *c
return c2
},
// User defined invalidation hook.
Invalidate: func(c *Cached) {
log.Println("invalidated:", c)
},
})
var url string
// Load value from cache, with callback function to hydrate
// cache if value cannot be found under index name with key.
// Negative (error) results are also cached, with user definable
// errors to ignore from caching (e.g. context deadline errs).
value, err := c.LoadOne("URL", func() (*Cached, error) {
return dbType.SelectByURL(url)
}, url)
if err != nil {
return nil, err
}
// Store value in cache, only if provided callback
// function returns without error. Passes value through
// invalidation hook regardless of error return value.
//
// On success value will be automatically added to and
// accessible under all initially configured indices.
if err := c.Store(value, func() error {
return dbType.Insert(value)
}); err != nil {
return nil, err
}
// Invalidate all cached results stored under
// provided index name with give field value(s).
c.Invalidate("CountryCode", 42)
```
This is a core underpinning of [GoToSocial](https://github.com/superseriousbusiness/gotosocial)'s performance.

View File

@@ -111,8 +111,8 @@ func (c *Cache[T]) Init(config Config[T]) {
// provided config.
c.mutex.Lock()
c.indices = make([]Index[T], len(config.Indices))
for i, config := range config.Indices {
c.indices[i].init(config)
for i, cfg := range config.Indices {
c.indices[i].init(cfg, config.MaxSize)
}
c.ignore = config.IgnoreErr
c.copy = config.CopyValue
@@ -138,7 +138,7 @@ func (c *Cache[T]) GetOne(index string, keyParts ...any) (T, bool) {
idx := c.Index(index)
// Generate index key from provided parts.
key, ok := idx.keygen.FromParts(keyParts...)
key, ok := idx.hasher.FromParts(keyParts...)
if !ok {
var zero T
return zero, false
@@ -149,7 +149,7 @@ func (c *Cache[T]) GetOne(index string, keyParts ...any) (T, bool) {
}
// GetOneBy fetches value from cache stored under index, using precalculated index key.
func (c *Cache[T]) GetOneBy(index *Index[T], key string) (T, bool) {
func (c *Cache[T]) GetOneBy(index *Index[T], key uint64) (T, bool) {
if index == nil {
panic("no index given")
} else if !index.unique {
@@ -170,37 +170,33 @@ func (c *Cache[T]) Get(index string, keysParts ...[]any) []T {
idx := c.Index(index)
// Preallocate expected keys slice length.
keys := make([]string, 0, len(keysParts))
keys := make([]uint64, 0, len(keysParts))
// Acquire buf.
buf := getBuf()
// Acquire hasher.
h := getHasher()
for _, parts := range keysParts {
// Reset buf.
buf.Reset()
h.Reset()
// Generate key from provided parts into buffer.
if !idx.keygen.AppendFromParts(buf, parts...) {
key, ok := idx.hasher.fromParts(h, parts...)
if !ok {
continue
}
// Get string copy of
// genarated idx key.
key := string(buf.B)
// Append key to keys.
// Append hash sum to keys.
keys = append(keys, key)
}
// Done with buf.
putBuf(buf)
// Done with h.
putHasher(h)
// Continue fetching values.
return c.GetBy(idx, keys...)
}
// GetBy fetches values from the cache stored under index, using precalculated index keys.
func (c *Cache[T]) GetBy(index *Index[T], keys ...string) []T {
func (c *Cache[T]) GetBy(index *Index[T], keys ...uint64) []T {
if index == nil {
panic("no index given")
}
@@ -265,7 +261,7 @@ func (c *Cache[T]) Put(values ...T) {
// Store all the passed values.
for _, value := range values {
c.store(nil, "", value, nil)
c.store(nil, 0, value, nil)
}
// Done with lock.
@@ -288,7 +284,7 @@ func (c *Cache[T]) LoadOne(index string, load func() (T, error), keyParts ...any
idx := c.Index(index)
// Generate cache from from provided parts.
key, _ := idx.keygen.FromParts(keyParts...)
key, _ := idx.hasher.FromParts(keyParts...)
// Continue loading this result.
return c.LoadOneBy(idx, load, key)
@@ -296,7 +292,7 @@ func (c *Cache[T]) LoadOne(index string, load func() (T, error), keyParts ...any
// LoadOneBy fetches one result from the cache stored under index, using precalculated index key.
// In the case that no result is found, provided load callback will be used to hydrate the cache.
func (c *Cache[T]) LoadOneBy(index *Index[T], load func() (T, error), key string) (T, error) {
func (c *Cache[T]) LoadOneBy(index *Index[T], load func() (T, error), key uint64) (T, error) {
if index == nil {
panic("no index given")
} else if !index.unique {
@@ -421,26 +417,21 @@ func (c *Cache[T]) LoadBy(index *Index[T], get func(load func(keyParts ...any) b
}
}()
// Acquire buf.
buf := getBuf()
// Acquire hasher.
h := getHasher()
// Pass cache check to user func.
get(func(keyParts ...any) bool {
// Reset buf.
buf.Reset()
h.Reset()
// Generate index key from provided key parts.
if !index.keygen.AppendFromParts(buf, keyParts...) {
key, ok := index.hasher.fromParts(h, keyParts...)
if !ok {
return false
}
// Get temp generated key str,
// (not needed after return).
keyStr := buf.String()
// Get all indexed results.
list := index.data[keyStr]
list := index.data[key]
if list != nil && list.len > 0 {
// Value length before
@@ -471,8 +462,8 @@ func (c *Cache[T]) LoadBy(index *Index[T], get func(load func(keyParts ...any) b
return false
})
// Done with buf.
putBuf(buf)
// Done with h.
putHasher(h)
// Done with lock.
c.mutex.Unlock()
@@ -528,7 +519,7 @@ func (c *Cache[T]) Invalidate(index string, keyParts ...any) {
idx := c.Index(index)
// Generate cache from from provided parts.
key, ok := idx.keygen.FromParts(keyParts...)
key, ok := idx.hasher.FromParts(keyParts...)
if !ok {
return
}
@@ -538,7 +529,7 @@ func (c *Cache[T]) Invalidate(index string, keyParts ...any) {
}
// InvalidateBy invalidates all results stored under index key.
func (c *Cache[T]) InvalidateBy(index *Index[T], key string) {
func (c *Cache[T]) InvalidateBy(index *Index[T], key uint64) {
if index == nil {
panic("no index given")
}
@@ -639,7 +630,7 @@ func (c *Cache[T]) Cap() int {
// store will store the given value / error result in the cache, storing it under the
// already provided index + key if provided, else generating keys from provided value.
func (c *Cache[T]) store(index *Index[T], key string, value T, err error) {
func (c *Cache[T]) store(index *Index[T], key uint64, value T, err error) {
// Acquire new result.
res := result_acquire(c)
@@ -671,8 +662,8 @@ func (c *Cache[T]) store(index *Index[T], key string, value T, err error) {
// value, used during cache key gen.
rvalue := reflect.ValueOf(value)
// Acquire buf.
buf := getBuf()
// Acquire hasher.
h := getHasher()
for i := range c.indices {
// Get current index ptr.
@@ -684,22 +675,20 @@ func (c *Cache[T]) store(index *Index[T], key string, value T, err error) {
continue
}
// Generate key from reflect value,
// Generate hash from reflect value,
// (this ignores zero value keys).
buf.Reset() // reset buf first
if !idx.keygen.appendFromRValue(buf, rvalue) {
h.Reset() // reset buf first
key, ok := idx.hasher.fromRValue(h, rvalue)
if !ok {
continue
}
// Alloc key copy.
key := string(buf.B)
// Append result to index at key.
index_append(c, idx, key, res)
}
// Done with buf.
putBuf(buf)
// Done with h.
putHasher(h)
}
if c.lruList.len > c.maxSize {

370
vendor/codeberg.org/gruf/go-structr/hash.go generated vendored Normal file
View File

@@ -0,0 +1,370 @@
package structr
import (
"reflect"
"unsafe"
"github.com/zeebo/xxh3"
)
func hasher(t reflect.Type) func(*xxh3.Hasher, any) bool {
switch t.Kind() {
case reflect.Int,
reflect.Uint,
reflect.Uintptr:
switch unsafe.Sizeof(int(0)) {
case 4:
return hash32bit
case 8:
return hash64bit
default:
panic("unexpected platform int size")
}
case reflect.Int8,
reflect.Uint8:
return hash8bit
case reflect.Int16,
reflect.Uint16:
return hash16bit
case reflect.Int32,
reflect.Uint32,
reflect.Float32:
return hash32bit
case reflect.Int64,
reflect.Uint64,
reflect.Float64,
reflect.Complex64:
return hash64bit
case reflect.String:
return hashstring
case reflect.Pointer:
switch t.Elem().Kind() {
case reflect.Int,
reflect.Uint,
reflect.Uintptr:
switch unsafe.Sizeof(int(0)) {
case 4:
return hash32bitptr
case 8:
return hash64bitptr
default:
panic("unexpected platform int size")
}
case reflect.Int8,
reflect.Uint8:
return hash8bitptr
case reflect.Int16,
reflect.Uint16:
return hash16bitptr
case reflect.Int32,
reflect.Uint32,
reflect.Float32:
return hash32bitptr
case reflect.Int64,
reflect.Uint64,
reflect.Float64,
reflect.Complex64:
return hash64bitptr
case reflect.String:
return hashstringptr
}
case reflect.Slice:
switch t.Elem().Kind() {
case reflect.Int,
reflect.Uint,
reflect.Uintptr:
switch unsafe.Sizeof(int(0)) {
case 4:
return hash32bitslice
case 8:
return hash64bitslice
default:
panic("unexpected platform int size")
}
case reflect.Int8,
reflect.Uint8:
return hash8bitslice
case reflect.Int16,
reflect.Uint16:
return hash16bitslice
case reflect.Int32,
reflect.Uint32,
reflect.Float32:
return hash32bitslice
case reflect.Int64,
reflect.Uint64,
reflect.Float64,
reflect.Complex64:
return hash64bitslice
case reflect.String:
return hashstringslice
}
}
switch {
case t.Implements(reflect.TypeOf((*interface{ MarshalBinary() ([]byte, error) })(nil)).Elem()):
return hashbinarymarshaler
case t.Implements(reflect.TypeOf((*interface{ Bytes() []byte })(nil)).Elem()):
return hashbytesmethod
case t.Implements(reflect.TypeOf((*interface{ String() string })(nil)).Elem()):
return hashstringmethod
case t.Implements(reflect.TypeOf((*interface{ MarshalText() ([]byte, error) })(nil)).Elem()):
return hashtextmarshaler
case t.Implements(reflect.TypeOf((*interface{ MarshalJSON() ([]byte, error) })(nil)).Elem()):
return hashjsonmarshaler
}
panic("unhashable type")
}
func hash8bit(h *xxh3.Hasher, a any) bool {
u := *(*uint8)(iface_value(a))
_, _ = h.Write([]byte{u})
return u == 0
}
func hash8bitptr(h *xxh3.Hasher, a any) bool {
u := (*uint8)(iface_value(a))
if u == nil {
_, _ = h.Write([]byte{
0,
})
return true
} else {
_, _ = h.Write([]byte{
1,
byte(*u),
})
return false
}
}
func hash8bitslice(h *xxh3.Hasher, a any) bool {
b := *(*[]byte)(iface_value(a))
_, _ = h.Write(b)
return b == nil
}
func hash16bit(h *xxh3.Hasher, a any) bool {
u := *(*uint16)(iface_value(a))
_, _ = h.Write([]byte{
byte(u),
byte(u >> 8),
})
return u == 0
}
func hash16bitptr(h *xxh3.Hasher, a any) bool {
u := (*uint16)(iface_value(a))
if u == nil {
_, _ = h.Write([]byte{
0,
})
return true
} else {
_, _ = h.Write([]byte{
1,
byte(*u),
byte(*u >> 8),
})
return false
}
}
func hash16bitslice(h *xxh3.Hasher, a any) bool {
u := *(*[]uint16)(iface_value(a))
for i := range u {
_, _ = h.Write([]byte{
byte(u[i]),
byte(u[i] >> 8),
})
}
return u == nil
}
func hash32bit(h *xxh3.Hasher, a any) bool {
u := *(*uint32)(iface_value(a))
_, _ = h.Write([]byte{
byte(u),
byte(u >> 8),
byte(u >> 16),
byte(u >> 24),
})
return u == 0
}
func hash32bitptr(h *xxh3.Hasher, a any) bool {
u := (*uint32)(iface_value(a))
if u == nil {
_, _ = h.Write([]byte{
0,
})
return true
} else {
_, _ = h.Write([]byte{
1,
byte(*u),
byte(*u >> 8),
byte(*u >> 16),
byte(*u >> 24),
})
return false
}
}
func hash32bitslice(h *xxh3.Hasher, a any) bool {
u := *(*[]uint32)(iface_value(a))
for i := range u {
_, _ = h.Write([]byte{
byte(u[i]),
byte(u[i] >> 8),
byte(u[i] >> 16),
byte(u[i] >> 24),
})
}
return u == nil
}
func hash64bit(h *xxh3.Hasher, a any) bool {
u := *(*uint64)(iface_value(a))
_, _ = h.Write([]byte{
byte(u),
byte(u >> 8),
byte(u >> 16),
byte(u >> 24),
byte(u >> 32),
byte(u >> 40),
byte(u >> 48),
byte(u >> 56),
})
return u == 0
}
func hash64bitptr(h *xxh3.Hasher, a any) bool {
u := (*uint64)(iface_value(a))
if u == nil {
_, _ = h.Write([]byte{
0,
})
return true
} else {
_, _ = h.Write([]byte{
1,
byte(*u),
byte(*u >> 8),
byte(*u >> 16),
byte(*u >> 24),
byte(*u >> 32),
byte(*u >> 40),
byte(*u >> 48),
byte(*u >> 56),
})
return false
}
}
func hash64bitslice(h *xxh3.Hasher, a any) bool {
u := *(*[]uint64)(iface_value(a))
for i := range u {
_, _ = h.Write([]byte{
byte(u[i]),
byte(u[i] >> 8),
byte(u[i] >> 16),
byte(u[i] >> 24),
byte(u[i] >> 32),
byte(u[i] >> 40),
byte(u[i] >> 48),
byte(u[i] >> 56),
})
}
return u == nil
}
func hashstring(h *xxh3.Hasher, a any) bool {
s := *(*string)(iface_value(a))
_, _ = h.WriteString(s)
return s == ""
}
func hashstringptr(h *xxh3.Hasher, a any) bool {
s := (*string)(iface_value(a))
if s == nil {
_, _ = h.Write([]byte{
0,
})
return true
} else {
_, _ = h.Write([]byte{
1,
})
_, _ = h.WriteString(*s)
return false
}
}
func hashstringslice(h *xxh3.Hasher, a any) bool {
s := *(*[]string)(iface_value(a))
for i := range s {
_, _ = h.WriteString(s[i])
}
return s == nil
}
func hashbinarymarshaler(h *xxh3.Hasher, a any) bool {
i := a.(interface{ MarshalBinary() ([]byte, error) })
b, _ := i.MarshalBinary()
_, _ = h.Write(b)
return b == nil
}
func hashbytesmethod(h *xxh3.Hasher, a any) bool {
i := a.(interface{ Bytes() []byte })
b := i.Bytes()
_, _ = h.Write(b)
return b == nil
}
func hashstringmethod(h *xxh3.Hasher, a any) bool {
i := a.(interface{ String() string })
s := i.String()
_, _ = h.WriteString(s)
return s == ""
}
func hashtextmarshaler(h *xxh3.Hasher, a any) bool {
i := a.(interface{ MarshalText() ([]byte, error) })
b, _ := i.MarshalText()
_, _ = h.Write(b)
return b == nil
}
func hashjsonmarshaler(h *xxh3.Hasher, a any) bool {
i := a.(interface{ MarshalJSON() ([]byte, error) })
b, _ := i.MarshalJSON()
_, _ = h.Write(b)
return b == nil
}
func iface_value(a any) unsafe.Pointer {
type eface struct{ _, v unsafe.Pointer }
return (*eface)(unsafe.Pointer(&a)).v
}

176
vendor/codeberg.org/gruf/go-structr/hasher.go generated vendored Normal file
View File

@@ -0,0 +1,176 @@
package structr
import (
"reflect"
"strings"
"github.com/zeebo/xxh3"
)
// Hasher provides hash checksumming for a configured
// index, based on an arbitrary combination of generic
// paramter struct type's fields. This provides hashing
// both by input of the fields separately, or passing
// an instance of the generic paramter struct type.
//
// Supported field types by the hasher include:
// - ~int
// - ~int8
// - ~int16
// - ~int32
// - ~int64
// - ~float32
// - ~float64
// - ~string
// - slices / ptrs of the above
type Hasher[StructType any] struct {
// fields contains our representation
// of struct fields contained in the
// creation of sums by this hasher.
fields []structfield
// zero specifies whether zero
// value fields are permitted.
zero bool
}
// NewHasher returns a new initialized Hasher for the receiving generic
// parameter type, comprising of the given field strings, and whether to
// allow zero values to be incldued within generated hash checksum values.
func NewHasher[T any](fields []string, allowZero bool) Hasher[T] {
var h Hasher[T]
// Preallocate expected struct field slice.
h.fields = make([]structfield, len(fields))
// Get the reflected struct ptr type.
t := reflect.TypeOf((*T)(nil)).Elem()
for i, fieldName := range fields {
// Split name to account for nesting.
names := strings.Split(fieldName, ".")
// Look for a usable struct field from type.
sfield, ok := findField(t, names, allowZero)
if !ok {
panicf("failed finding field: %s", fieldName)
}
// Set parsed struct field.
h.fields[i] = sfield
}
// Set config flags.
h.zero = allowZero
return h
}
// FromParts generates hash checksum (used as index key) from individual key parts.
func (h *Hasher[T]) FromParts(parts ...any) (sum uint64, ok bool) {
hh := getHasher()
sum, ok = h.fromParts(hh, parts...)
putHasher(hh)
return
}
func (h *Hasher[T]) fromParts(hh *xxh3.Hasher, parts ...any) (sum uint64, ok bool) {
if len(parts) != len(h.fields) {
// User must provide correct number of parts for key.
panicf("incorrect number key parts: want=%d received=%d",
len(parts),
len(h.fields),
)
}
if h.zero {
// Zero values are permitted,
// mangle all values and ignore
// zero value return booleans.
for i, part := range parts {
// Write mangled part to hasher.
_ = h.fields[i].hasher(hh, part)
}
} else {
// Zero values are NOT permitted.
for i, part := range parts {
// Write mangled field to hasher.
z := h.fields[i].hasher(hh, part)
if z {
// The value was zero for
// this type, return early.
return 0, false
}
}
}
return hh.Sum64(), true
}
// FromValue generates hash checksum (used as index key) from a value, via reflection.
func (h *Hasher[T]) FromValue(value T) (sum uint64, ok bool) {
rvalue := reflect.ValueOf(value)
hh := getHasher()
sum, ok = h.fromRValue(hh, rvalue)
putHasher(hh)
return
}
func (h *Hasher[T]) fromRValue(hh *xxh3.Hasher, rvalue reflect.Value) (uint64, bool) {
// Follow any ptrs leading to value.
for rvalue.Kind() == reflect.Pointer {
rvalue = rvalue.Elem()
}
if h.zero {
// Zero values are permitted,
// mangle all values and ignore
// zero value return booleans.
for i := range h.fields {
// Get the reflect value's field at idx.
fv := rvalue.FieldByIndex(h.fields[i].index)
fi := fv.Interface()
// Write mangled field to hasher.
_ = h.fields[i].hasher(hh, fi)
}
} else {
// Zero values are NOT permitted.
for i := range h.fields {
// Get the reflect value's field at idx.
fv := rvalue.FieldByIndex(h.fields[i].index)
fi := fv.Interface()
// Write mangled field to hasher.
z := h.fields[i].hasher(hh, fi)
if z {
// The value was zero for
// this type, return early.
return 0, false
}
}
}
return hh.Sum64(), true
}
type structfield struct {
// index is the reflected index
// of this field (this takes into
// account struct nesting).
index []int
// hasher is the relevant function
// for hashing value of structfield
// into the supplied hashbuf, where
// return value indicates if zero.
hasher func(*xxh3.Hasher, any) bool
}

View File

@@ -45,12 +45,12 @@ type Index[StructType any] struct {
// string value of contained fields.
name string
// struct field key serializer.
keygen KeyGen[StructType]
// struct field key hasher.
hasher Hasher[StructType]
// backing in-memory data store of
// generated index keys to result lists.
data map[string]*list[*result[StructType]]
data map[uint64]*list[*result[StructType]]
// whether to allow
// multiple results
@@ -59,20 +59,20 @@ type Index[StructType any] struct {
}
// init initializes this index with the given configuration.
func (i *Index[T]) init(config IndexConfig) {
func (i *Index[T]) init(config IndexConfig, max int) {
fields := strings.Split(config.Fields, ",")
i.name = config.Fields
i.keygen = NewKeyGen[T](fields, config.AllowZero)
i.hasher = NewHasher[T](fields, config.AllowZero)
i.unique = !config.Multiple
i.data = make(map[string]*list[*result[T]])
i.data = make(map[uint64]*list[*result[T]], max+1)
}
// KeyGen returns the key generator associated with this index.
func (i *Index[T]) KeyGen() *KeyGen[T] {
return &i.keygen
// Hasher returns the hash checksummer associated with this index.
func (i *Index[T]) Hasher() *Hasher[T] {
return &i.hasher
}
func index_append[T any](c *Cache[T], i *Index[T], key string, res *result[T]) {
func index_append[T any](c *Cache[T], i *Index[T], key uint64, res *result[T]) {
// Acquire + setup indexkey.
ikey := indexkey_acquire(c)
ikey.entry.Value = res
@@ -138,7 +138,7 @@ func index_deleteOne[T any](c *Cache[T], i *Index[T], ikey *indexkey[T]) {
}
}
func index_delete[T any](c *Cache[T], i *Index[T], key string, fn func(*result[T])) {
func index_delete[T any](c *Cache[T], i *Index[T], key uint64, fn func(*result[T])) {
if fn == nil {
panic("nil fn")
}
@@ -180,7 +180,7 @@ type indexkey[T any] struct {
// key is the generated index key
// the related result is indexed
// under, in the below index.
key string
key uint64
// index is the index that the
// related result is indexed in.
@@ -205,7 +205,7 @@ func indexkey_acquire[T any](c *Cache[T]) *indexkey[T] {
func indexkey_release[T any](c *Cache[T], ikey *indexkey[T]) {
// Reset indexkey.
ikey.entry.Value = nil
ikey.key = ""
ikey.key = 0
ikey.index = nil
// Release indexkey to memory pool.

View File

@@ -1,204 +0,0 @@
package structr
import (
"reflect"
"strings"
"codeberg.org/gruf/go-byteutil"
"codeberg.org/gruf/go-mangler"
)
// KeyGen is the underlying index key generator
// used within Index, and therefore Cache itself.
type KeyGen[StructType any] struct {
// fields contains our representation of
// the struct fields contained in the
// creation of keys by this generator.
fields []structfield
// zero specifies whether zero
// value fields are permitted.
zero bool
}
// NewKeyGen returns a new initialized KeyGen for the receiving generic
// parameter type, comprising of the given field strings, and whether to
// allow zero values to be included within generated output strings.
func NewKeyGen[T any](fields []string, allowZero bool) KeyGen[T] {
var kgen KeyGen[T]
// Preallocate expected struct field slice.
kgen.fields = make([]structfield, len(fields))
// Get the reflected struct ptr type.
t := reflect.TypeOf((*T)(nil)).Elem()
for i, fieldName := range fields {
// Split name to account for nesting.
names := strings.Split(fieldName, ".")
// Look for a usable struct field from type.
sfield, ok := findField(t, names, allowZero)
if !ok {
panicf("failed finding field: %s", fieldName)
}
// Set parsed struct field.
kgen.fields[i] = sfield
}
// Set config flags.
kgen.zero = allowZero
return kgen
}
// FromParts generates key string from individual key parts.
func (kgen *KeyGen[T]) FromParts(parts ...any) (key string, ok bool) {
buf := getBuf()
if ok = kgen.AppendFromParts(buf, parts...); ok {
key = string(buf.B)
}
putBuf(buf)
return
}
// FromValue generates key string from a value, via reflection.
func (kgen *KeyGen[T]) FromValue(value T) (key string, ok bool) {
buf := getBuf()
rvalue := reflect.ValueOf(value)
if ok = kgen.appendFromRValue(buf, rvalue); ok {
key = string(buf.B)
}
putBuf(buf)
return
}
// AppendFromParts generates key string into provided buffer, from individual key parts.
func (kgen *KeyGen[T]) AppendFromParts(buf *byteutil.Buffer, parts ...any) bool {
if len(parts) != len(kgen.fields) {
// User must provide correct number of parts for key.
panicf("incorrect number key parts: want=%d received=%d",
len(parts),
len(kgen.fields),
)
}
if kgen.zero {
// Zero values are permitted,
// mangle all values and ignore
// zero value return booleans.
for i, part := range parts {
// Mangle this value into buffer.
_ = kgen.fields[i].Mangle(buf, part)
// Append part separator.
buf.B = append(buf.B, '.')
}
} else {
// Zero values are NOT permitted.
for i, part := range parts {
// Mangle this value into buffer.
z := kgen.fields[i].Mangle(buf, part)
if z {
// The value was zero for
// this type, return early.
return false
}
// Append part separator.
buf.B = append(buf.B, '.')
}
}
// Drop the last separator.
buf.B = buf.B[:len(buf.B)-1]
return true
}
// AppendFromValue generates key string into provided buffer, from a value via reflection.
func (kgen *KeyGen[T]) AppendFromValue(buf *byteutil.Buffer, value T) bool {
return kgen.appendFromRValue(buf, reflect.ValueOf(value))
}
// appendFromRValue is the underlying generator function for the exported ___FromValue() functions,
// accepting a reflected input. We do not expose this as the reflected value is EXPECTED to be right.
func (kgen *KeyGen[T]) appendFromRValue(buf *byteutil.Buffer, rvalue reflect.Value) bool {
// Follow any ptrs leading to value.
for rvalue.Kind() == reflect.Pointer {
rvalue = rvalue.Elem()
}
if kgen.zero {
// Zero values are permitted,
// mangle all values and ignore
// zero value return booleans.
for i := range kgen.fields {
// Get the reflect value's field at idx.
fv := rvalue.FieldByIndex(kgen.fields[i].index)
fi := fv.Interface()
// Mangle this value into buffer.
_ = kgen.fields[i].Mangle(buf, fi)
// Append part separator.
buf.B = append(buf.B, '.')
}
} else {
// Zero values are NOT permitted.
for i := range kgen.fields {
// Get the reflect value's field at idx.
fv := rvalue.FieldByIndex(kgen.fields[i].index)
fi := fv.Interface()
// Mangle this value into buffer.
z := kgen.fields[i].Mangle(buf, fi)
if z {
// The value was zero for
// this type, return early.
return false
}
// Append part separator.
buf.B = append(buf.B, '.')
}
}
// Drop the last separator.
buf.B = buf.B[:len(buf.B)-1]
return true
}
type structfield struct {
// index is the reflected index
// of this field (this takes into
// account struct nesting).
index []int
// zero is the possible mangled
// zero value for this field.
zero string
// mangler is the mangler function for
// serializing values of this field.
mangler mangler.Mangler
}
// Mangle mangles the given value, using the determined type-appropriate
// field's type. The returned boolean indicates whether this is a zero value.
func (f *structfield) Mangle(buf *byteutil.Buffer, value any) (isZero bool) {
s := len(buf.B) // start pos.
buf.B = f.mangler(buf.B, value)
e := len(buf.B) // end pos.
isZero = (f.zero == string(buf.B[s:e]))
return
}

View File

@@ -7,8 +7,7 @@ import (
"unicode"
"unicode/utf8"
"codeberg.org/gruf/go-byteutil"
"codeberg.org/gruf/go-mangler"
"github.com/zeebo/xxh3"
)
// findField will search for a struct field with given set of names, where names is a len > 0 slice of names account for nesting.
@@ -68,22 +67,8 @@ func findField(t reflect.Type, names []string, allowZero bool) (sfield structfie
t = field.Type
}
// Get final type mangler func.
sfield.mangler = mangler.Get(t)
if allowZero {
var buf []byte
// Allocate field instance.
v := reflect.New(field.Type)
v = v.Elem()
// Serialize this zero value into buf.
buf = sfield.mangler(buf, v.Interface())
// Set zero value str.
sfield.zero = string(buf)
}
// Get final type hash func.
sfield.hasher = hasher(t)
return
}
@@ -93,26 +78,21 @@ func panicf(format string, args ...any) {
panic(fmt.Sprintf(format, args...))
}
// bufpool provides a memory pool of byte
// buffers used when encoding key types.
var bufPool sync.Pool
// hashPool provides a memory pool of xxh3
// hasher objects used indexing field vals.
var hashPool sync.Pool
// getBuf fetches buffer from memory pool.
func getBuf() *byteutil.Buffer {
v := bufPool.Get()
// gethashbuf fetches hasher from memory pool.
func getHasher() *xxh3.Hasher {
v := hashPool.Get()
if v == nil {
buf := new(byteutil.Buffer)
buf.B = make([]byte, 0, 512)
v = buf
v = new(xxh3.Hasher)
}
return v.(*byteutil.Buffer)
return v.(*xxh3.Hasher)
}
// putBuf replaces buffer in memory pool.
func putBuf(buf *byteutil.Buffer) {
if buf.Cap() > int(^uint16(0)) {
return // drop large bufs
}
buf.Reset()
bufPool.Put(buf)
// putHasher replaces hasher in memory pool.
func putHasher(h *xxh3.Hasher) {
h.Reset()
hashPool.Put(h)
}