Files
GoToSocial/vendor/codeberg.org/gruf/go-structr/timeline.go

1009 lines
23 KiB
Go

package structr
import (
"cmp"
"reflect"
"slices"
"sync"
"unsafe"
)
// Direction defines a direction
// to iterate entries in a Timeline.
type Direction bool
const (
// Asc = ascending, i.e. bottom-up.
Asc = Direction(true)
// Desc = descending, i.e. top-down.
Desc = Direction(false)
)
// TimelineConfig defines config vars for initializing a Timeline{}.
type TimelineConfig[StructType any, PK cmp.Ordered] struct {
// Copy provides a means of copying
// timelined values, to ensure returned values
// do not share memory with those in timeline.
Copy func(StructType) StructType
// Invalidate is called when timelined
// values are invalidated, either as passed
// to Insert(), or by calls to Invalidate().
Invalidate func(StructType)
// PKey defines the generic parameter StructType's
// field to use as the primary key for this cache.
// It must be ordered so that the timeline can
// maintain correct sorting of inserted values.
//
// Field selection logic follows the same path as
// with IndexConfig{}.Fields. Noting that in this
// case only a single field is permitted, though
// it may be nested, and as described above the
// type must conform to cmp.Ordered.
PKey string
// Indices defines indices to create
// in the Timeline for the receiving
// generic struct type parameter.
Indices []IndexConfig
}
// Timeline provides an ordered-list like cache of structures,
// with automated indexing and invalidation by any initialization
// defined combination of fields. The list order is maintained
// according to the configured struct primary key.
type Timeline[StructType any, PK cmp.Ordered] struct {
// hook functions.
invalid func(StructType)
copy func(StructType) StructType
// main underlying
// timeline list.
//
// where:
// - head = top = largest
// - tail = btm = smallest
list list
// contains struct field information of
// the field used as the primary key for
// this timeline. it can also be found
// under indices[0]
pkey pkey_field
// indices used in storing passed struct
// types by user defined sets of fields.
indices []Index
// protective mutex, guards:
// - Timeline{}.*
// - Index{}.data
mutex sync.Mutex
}
// Init initializes the timeline with given configuration
// including struct fields to index, and necessary fns.
func (t *Timeline[T, PK]) Init(config TimelineConfig[T, PK]) {
rt := reflect.TypeOf((*T)(nil)).Elem()
if len(config.Indices) == 0 {
panic("no indices provided")
}
if config.Copy == nil {
panic("copy function must be provided")
}
// Safely copy over
// provided config.
t.mutex.Lock()
defer t.mutex.Unlock()
// The first index is created from PKey.
t.indices = make([]Index, len(config.Indices)+1)
t.indices[0].ptr = unsafe.Pointer(t)
t.indices[0].init(rt, IndexConfig{
Fields: config.PKey,
AllowZero: true,
Multiple: true,
}, 0)
if len(t.indices[0].fields) > 1 {
panic("primary key must contain only 1 field")
}
for i, cfg := range config.Indices {
t.indices[i+1].ptr = unsafe.Pointer(t)
t.indices[i+1].init(rt, cfg, 0)
}
// Before extracting
// first index for pkey.
field := t.indices[0].fields[0]
t.pkey = pkey_field{
rtype: field.rtype,
offsets: field.offsets,
likeptr: field.likeptr,
}
// Copy over remaining.
t.copy = config.Copy
t.invalid = config.Invalidate
}
// Index selects index with given name from timeline, else panics.
func (t *Timeline[T, PK]) Index(name string) *Index {
for i, idx := range t.indices {
if idx.name == name {
return &(t.indices[i])
}
}
panic("unknown index: " + name)
}
// Select allows you to retrieve a slice of values, in order, from the timeline.
// This slice is defined by the minimum and maximum primary key parameters, up to
// a given length in size. The direction in which you select will determine which
// of the min / max primary key values is used as the *cursor* to begin the start
// of the selection, and which is used as the *boundary* to mark the end, if set.
// In either case, the length parameter is always optional.
//
// dir = Asc : cursors up from 'max' (required), with boundary 'min' (optional).
// dir = Desc : cursors down from 'min' (required), with boundary 'max' (optional).
func (t *Timeline[T, PK]) Select(min, max *PK, length *int, dir Direction) (values []T) {
// Acquire lock.
t.mutex.Lock()
// Check init'd.
if t.copy == nil {
t.mutex.Unlock()
panic("not initialized")
}
switch dir {
case Asc:
// Verify args.
if min == nil {
t.mutex.Unlock()
panic("min must be provided when selecting asc")
}
// Select determined values ASCENDING.
values = t.select_asc(*min, max, length)
case Desc:
// Verify args.
if max == nil {
t.mutex.Unlock()
panic("max must be provided when selecting asc")
}
// Select determined values DESCENDING.
values = t.select_desc(min, *max, length)
}
// Done with lock.
t.mutex.Unlock()
return values
}
// Insert will insert the given values into the timeline,
// calling any set invalidate hook on each inserted value.
func (t *Timeline[T, PK]) Insert(values ...T) {
// Acquire lock.
t.mutex.Lock()
// Check init'd.
if t.copy == nil {
t.mutex.Unlock()
panic("not initialized")
}
// Allocate a slice of our value wrapping struct type.
with_keys := make([]value_with_pk[T, PK], len(values))
if len(with_keys) != len(values) {
panic("BCE")
}
// Range the provided values.
for i, value := range values {
// Create our own copy
// of value to work with.
value = t.copy(value)
// Take ptr to the value copy.
vptr := unsafe.Pointer(&value)
// Extract primary key from vptr.
kptr := extract_pkey(vptr, t.pkey)
var pkey PK
if kptr != nil {
// Cast as PK type.
pkey = *(*PK)(kptr)
} else {
// Use zero value pointer.
kptr = unsafe.Pointer(&pkey)
}
// Append wrapped value to slice with
// the acquire pointers and primary key.
with_keys[i] = value_with_pk[T, PK]{
k: pkey,
v: value,
kptr: kptr,
vptr: vptr,
}
}
var last *list_elem
// BEFORE inserting the prepared slice of value copies w/ primary
// keys, sort them by their primary key, ascending. This permits
// us to re-use the 'last' timeline position as next insert cursor.
// Otherwise we would have to iterate from 'head' every single time.
slices.SortFunc(with_keys, func(a, b value_with_pk[T, PK]) int {
const k = +1
switch {
case a.k < b.k:
return +k
case b.k < a.k:
return -k
default:
return 0
}
})
// Store each value in the timeline,
// updating the last used list element
// each time so we don't have to iter
// down from head on every single store.
for _, value := range with_keys {
last = t.store_one(last, value)
}
// Get func ptrs.
invalid := t.invalid
// Done with lock.
t.mutex.Unlock()
if invalid != nil {
// Pass all invalidated values
// to given user hook (if set).
for _, value := range values {
invalid(value)
}
}
}
// Invalidate invalidates all entries stored in index under given keys.
// Note that if set, this will call the invalidate hook on each value.
func (t *Timeline[T, PK]) Invalidate(index *Index, keys ...Key) {
if index == nil {
panic("no index given")
} else if index.ptr != unsafe.Pointer(t) {
panic("invalid index for timeline")
}
// Acquire lock.
t.mutex.Lock()
// Preallocate expected ret slice.
values := make([]T, 0, len(keys))
for i := range keys {
// Delete all items under key from index, collecting
// value items and dropping them from all their indices.
index.delete(keys[i].key, func(item *indexed_item) {
// Cast to *actual* timeline item.
t_item := to_timeline_item(item)
if value, ok := item.data.(T); ok {
// No need to copy, as item
// being deleted from cache.
values = append(values, value)
}
// Delete item.
t.delete(t_item)
})
}
// Get func ptrs.
invalid := t.invalid
// Done with lock.
t.mutex.Unlock()
if invalid != nil {
// Pass all invalidated values
// to given user hook (if set).
for _, value := range values {
invalid(value)
}
}
}
// Range will range over all values in the timeline in given direction.
// dir = Asc : ranges from the bottom-up.
// dir = Desc : ranges from the top-down.
//
// Please note that the entire Timeline{} will be locked for the duration of the range
// operation, i.e. from the beginning of the first yield call until the end of the last.
func (t *Timeline[T, PK]) Range(dir Direction) func(yield func(T) bool) {
return func(yield func(T) bool) {
if t.copy == nil {
panic("not initialized")
} else if yield == nil {
panic("nil func")
}
// Acquire lock.
t.mutex.Lock()
defer t.mutex.Unlock()
switch dir {
case Asc:
// Iterate through linked list from bottom (i.e. tail).
for prev := t.list.tail; prev != nil; prev = prev.prev {
// Extract item from list element.
item := (*timeline_item)(prev.data)
// Create copy of item value.
value := t.copy(item.data.(T))
// Pass to given function.
if !yield(value) {
break
}
}
case Desc:
// Iterate through linked list from top (i.e. head).
for next := t.list.head; next != nil; next = next.next {
// Extract item from list element.
item := (*timeline_item)(next.data)
// Create copy of item value.
value := t.copy(item.data.(T))
// Pass to given function.
if !yield(value) {
break
}
}
}
}
}
// RangeKeys will iterate over all values for given keys in the given index.
//
// Please note that the entire Timeline{} will be locked for the duration of the range
// operation, i.e. from the beginning of the first yield call until the end of the last.
func (t *Timeline[T, PK]) RangeKeys(index *Index, keys ...Key) func(yield func(T) bool) {
return func(yield func(T) bool) {
if t.copy == nil {
panic("not initialized")
} else if index == nil {
panic("no index given")
} else if index.ptr != unsafe.Pointer(t) {
panic("invalid index for timeline")
} else if yield == nil {
panic("nil func")
}
// Acquire lock.
t.mutex.Lock()
defer t.mutex.Unlock()
for _, key := range keys {
var done bool
// Iterate over values in index under key.
index.get(key.key, func(i *indexed_item) {
// Cast to timeline_item type.
item := to_timeline_item(i)
// Create copy of item value.
value := t.copy(item.data.(T))
// Pass val to yield function.
done = done || !yield(value)
})
if done {
break
}
}
}
}
// Trim will remove entries from the timeline in given
// direction, ensuring timeline is no larger than 'max'.
// If 'max' >= t.Len(), this function is a no-op.
// dir = Asc : trims from the bottom-up.
// dir = Desc : trims from the top-down.
func (t *Timeline[T, PK]) Trim(max int, dir Direction) {
// Acquire lock.
t.mutex.Lock()
// Calculate number to drop.
diff := t.list.len - int(max)
if diff <= 0 {
// Trim not needed.
t.mutex.Unlock()
return
}
switch dir {
case Asc:
// Iterate over 'diff' items
// from bottom of timeline list.
for range diff {
// Get bottom list elem.
bottom := t.list.tail
if bottom == nil {
// reached
// end.
break
}
// Drop bottom-most item from timeline.
item := (*timeline_item)(bottom.data)
t.delete(item)
}
case Desc:
// Iterate over 'diff' items
// from top of timeline list.
for range diff {
// Get top list elem.
top := t.list.head
if top == nil {
// reached
// end.
break
}
// Drop top-most item from timeline.
item := (*timeline_item)(top.data)
t.delete(item)
}
}
// Compact index data stores.
for _, idx := range t.indices {
(&idx).data.Compact()
}
// Done with lock.
t.mutex.Unlock()
}
// Clear empties the timeline by calling .TrimBottom(0, Down).
func (t *Timeline[T, PK]) Clear() { t.Trim(0, Desc) }
// Len returns the current length of cache.
func (t *Timeline[T, PK]) Len() int {
t.mutex.Lock()
l := t.list.len
t.mutex.Unlock()
return l
}
// Debug returns debug stats about cache.
func (t *Timeline[T, PK]) Debug() map[string]any {
m := make(map[string]any, 2)
t.mutex.Lock()
m["list"] = t.list.len
indices := make(map[string]any, len(t.indices))
m["indices"] = indices
for _, idx := range t.indices {
var n uint64
for _, l := range idx.data.m {
n += uint64(l.len)
}
indices[idx.name] = n
}
t.mutex.Unlock()
return m
}
func (t *Timeline[T, PK]) select_asc(min PK, max *PK, length *int) (values []T) {
// Iterate through linked list
// from bottom (i.e. tail), asc.
prev := t.list.tail
// Iterate from 'prev' up, skipping all
// entries with pkey below cursor 'min'.
for ; prev != nil; prev = prev.prev {
item := (*timeline_item)(prev.data)
pkey := *(*PK)(item.pk)
// Check below min.
if pkey < min {
continue
}
// Reached
// cursor.
break
}
if prev == nil {
// No values
// remaining.
return
}
// Optimized switch case to handle
// each set of argument combinations
// separately, in order to minimize
// number of checks during loops.
switch {
case length != nil && max != nil:
// Deref arguments.
length := *length
max := *max
// Optimistic preallocate slice.
values = make([]T, 0, length)
// Both a length and maximum were given,
// select from cursor until either reached.
for ; prev != nil; prev = prev.prev {
item := (*timeline_item)(prev.data)
pkey := *(*PK)(item.pk)
// Check above max.
if pkey >= max {
break
}
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
// Check if length reached.
if len(values) >= length {
break
}
}
case length != nil:
// Deref length.
length := *length
// Optimistic preallocate slice.
values = make([]T, 0, length)
// Only a length was given, select
// from cursor until length reached.
for ; prev != nil; prev = prev.prev {
item := (*timeline_item)(prev.data)
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
// Check if length reached.
if len(values) >= length {
break
}
}
case max != nil:
// Deref min.
max := *max
// Only a maximum was given, select
// from cursor until max is reached.
for ; prev != nil; prev = prev.prev {
item := (*timeline_item)(prev.data)
pkey := *(*PK)(item.pk)
// Check above max.
if pkey >= max {
break
}
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
}
default:
// No maximum or length were given,
// ALL from cursor need selecting.
for ; prev != nil; prev = prev.prev {
item := (*timeline_item)(prev.data)
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
}
}
return
}
func (t *Timeline[T, PK]) select_desc(min *PK, max PK, length *int) (values []T) {
// Iterate through linked list
// from top (i.e. head), desc.
next := t.list.head
// Iterate from 'next' down, skipping
// all entries with pkey above cursor 'max'.
for ; next != nil; next = next.next {
item := (*timeline_item)(next.data)
pkey := *(*PK)(item.pk)
// Check above max.
if pkey > max {
continue
}
// Reached
// cursor.
break
}
if next == nil {
// No values
// remaining.
return
}
// Optimized switch case to handle
// each set of argument combinations
// separately, in order to minimize
// number of checks during loops.
switch {
case length != nil && min != nil:
// Deref arguments.
length := *length
min := *min
// Optimistic preallocate slice.
values = make([]T, 0, length)
// Both a length and minimum were given,
// select from cursor until either reached.
for ; next != nil; next = next.next {
item := (*timeline_item)(next.data)
pkey := *(*PK)(item.pk)
// Check below min.
if pkey <= min {
break
}
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
// Check if length reached.
if len(values) >= length {
break
}
}
case length != nil:
// Deref length.
length := *length
// Optimistic preallocate slice.
values = make([]T, 0, length)
// Only a length was given, select
// from cursor until length reached.
for ; next != nil; next = next.next {
item := (*timeline_item)(next.data)
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
// Check if length reached.
if len(values) >= length {
break
}
}
case min != nil:
// Deref min.
min := *min
// Only a minimum was given, select
// from cursor until minimum reached.
for ; next != nil; next = next.next {
item := (*timeline_item)(next.data)
pkey := *(*PK)(item.pk)
// Check below min.
if pkey <= min {
break
}
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
}
default:
// No minimum or length were given,
// ALL from cursor need selecting.
for ; next != nil; next = next.next {
item := (*timeline_item)(next.data)
// Append value copy.
value := item.data.(T)
value = t.copy(value)
values = append(values, value)
}
}
return
}
// value_with_pk wraps an incoming value type, with
// its extracted primary key, and pointers to both.
// this encompasses all arguments related to a value
// required by store_one(), simplifying some logic.
//
// with all the primary keys extracted, it also
// makes it much easier to sort input before insert.
type value_with_pk[T any, PK comparable] struct {
k PK // primary key value
v T // value copy
kptr unsafe.Pointer // primary key ptr
vptr unsafe.Pointer // value copy ptr
}
func (t *Timeline[T, PK]) store_one(last *list_elem, value value_with_pk[T, PK]) *list_elem {
// NOTE: the value passed here should
// already be a copy of the original.
// Alloc new index item.
t_item := new_timeline_item()
if cap(t_item.indexed) < len(t.indices) {
// Preallocate item indices slice to prevent Go auto
// allocating overlying large slices we don't need.
t_item.indexed = make([]*index_entry, 0, len(t.indices))
}
// Set item value data.
t_item.data = value.v
t_item.pk = value.kptr
// Acquire key buf.
buf := new_buffer()
// Convert to indexed_item ptr.
i_item := from_timeline_item(t_item)
// Append already-extracted
// primary key to 0th index.
idx := (&t.indices[0])
partptrs := []unsafe.Pointer{value.kptr}
key := idx.key(buf, partptrs)
evicted := idx.append(key, i_item)
if evicted != nil {
// This item is no longer
// indexed, remove from list.
t.list.remove(&evicted.elem)
// Now convert from index_item ptr
// and release it to global mem pool.
evicted := to_timeline_item(evicted)
free_timeline_item(evicted)
}
for i := 1; i < len(t.indices); i++ {
// Get current index ptr.
idx := (&t.indices[i])
// Extract fields comprising index key from value.
parts := extract_fields(value.vptr, idx.fields)
// Calculate this index key.
key := idx.key(buf, parts)
if key == "" {
continue
}
// Append this item to index.
evicted := idx.append(key, i_item)
if evicted != nil {
// This item is no longer
// indexed, remove from list.
t.list.remove(&evicted.elem)
// Now convert from index_item ptr
// and release it to global mem pool.
evicted := to_timeline_item(evicted)
free_timeline_item(evicted)
}
}
// Done with buf.
free_buffer(buf)
if last == nil {
// No previous element was provided, this is
// first insert, we need to work from head.
// Check for emtpy head.
if t.list.head == nil {
// The easiest case, this will
// be the first item in list.
t.list.push_front(&t_item.elem)
return t.list.head
}
// Extract head item and its primary key.
headItem := (*timeline_item)(t.list.head.data)
headPK := *(*PK)(headItem.pk)
if value.k >= headPK {
// Another easier case, this also
// will be the first item in list.
t.list.push_front(&t_item.elem)
return t.list.head
}
// Set last=head
// to work from.
last = t.list.head
}
// Iterate through linked list
// from head to find location.
for next := last.next; //
next != nil; next = next.next {
// Extract item and it's primary key.
nextItem := (*timeline_item)(next.data)
nextPK := *(*PK)(nextItem.pk)
// If pkey smaller than
// cursor's, keep going.
if value.k < nextPK {
continue
}
// New pkey is larger than cursor,
// insert into list just before it.
t.list.insert(&t_item.elem, next.prev)
return next
}
// We reached the end of the
// list, insert at tail pos.
t.list.push_back(&t_item.elem)
return t.list.tail
}
func (t *Timeline[T, PK]) delete(i *timeline_item) {
for len(i.indexed) != 0 {
// Pop last indexed entry from list.
entry := i.indexed[len(i.indexed)-1]
i.indexed[len(i.indexed)-1] = nil
i.indexed = i.indexed[:len(i.indexed)-1]
// Get entry's index.
index := entry.index
// Drop this index_entry.
index.delete_entry(entry)
}
// Drop from main list.
t.list.remove(&i.elem)
// Free unused item.
free_timeline_item(i)
}
type timeline_item struct {
indexed_item
// retains fast ptr access
// to primary key value of
// above indexed_item{}.data
pk unsafe.Pointer
// check bits always all set
// to 1. used to ensure cast
// from indexed_item to this
// type was originally a
// timeline_item to begin with.
ck uint
}
func init() {
// ensure the embedded indexed_item struct is ALWAYS at zero offset.
// we rely on this to allow a ptr to one to be a ptr to either of them.
const off = unsafe.Offsetof(timeline_item{}.indexed_item)
if off != 0 {
panic("invalid timeline_item{}.indexed_item offset")
}
}
// from_timeline_item converts a timeline_item ptr to indexed_item, given the above init() guarantee.
func from_timeline_item(item *timeline_item) *indexed_item {
return (*indexed_item)(unsafe.Pointer(item))
}
// to_timeline_item converts an indexed_item ptr to timeline_item, given the above init() guarantee.
// NOTE THIS MUST BE AN indexed_item THAT WAS INITIALLY CONVERTED WITH from_timeline_item().
func to_timeline_item(item *indexed_item) *timeline_item {
to := (*timeline_item)(unsafe.Pointer(item))
if to.ck != ^uint(0) {
// ensure check bits are
// set indicating it was a
// timeline_item originally.
should_not_reach(true)
}
return to
}
var timeline_item_pool sync.Pool
// new_timeline_item returns a new prepared timeline_item.
func new_timeline_item() *timeline_item {
v := timeline_item_pool.Get()
if v == nil {
i := new(timeline_item)
i.elem.data = unsafe.Pointer(i)
i.ck = ^uint(0)
v = i
}
item := v.(*timeline_item)
return item
}
// free_timeline_item releases the timeline_item.
func free_timeline_item(item *timeline_item) {
if len(item.indexed) > 0 ||
item.elem.next != nil ||
item.elem.prev != nil {
should_not_reach(false)
return
}
item.data = nil
item.pk = nil
timeline_item_pool.Put(item)
}