restructure project
Some checks failed
Build / build (push) Failing after 5m21s
Publish Image / publish (push) Failing after 32s
Test / build (push) Failing after 5m8s

This commit is contained in:
Chuck Smith
2024-03-28 16:20:09 -04:00
parent 362138ff2e
commit fc6ceee02c
93 changed files with 479 additions and 194 deletions

270
internal/vm/cache.go Normal file
View File

@@ -0,0 +1,270 @@
package vm
import (
"container/list"
"fmt"
"sync"
)
type cachedFrame struct {
Key uint // pointer to key
Value *Frame // pointer to value
Frequency uint // frequency of access
}
type frequencyNode struct {
count uint // frequency count - never decreases
valuesList *list.List // valuesList contains pointer to the head of values linked list
inner *list.Element // actual content of the next element
}
// creates a new frequency list node with the given count
func newFrequencyNode(count uint) *frequencyNode {
return &frequencyNode{
count: count,
valuesList: list.New(),
inner: nil,
}
}
type keyRefNode struct {
inner *list.Element // contains the actual value wrapped by a list element
parentFreqNode *list.Element // contains reference to the frequency node element
keyRef uint // contains pointer to the key
valueRef *Frame // value
}
// creates a new KeyRef node which is used to represent the value in linked list
func newKeyRefNode(keyRef uint, valueRef *Frame, parent *list.Element) *keyRefNode {
return &keyRefNode{
inner: nil,
parentFreqNode: parent,
keyRef: keyRef,
valueRef: valueRef,
}
}
// FrameCache implements all the methods and data-structures required for a LFU cache for caching frames.
type FrameCache struct {
rwLock sync.RWMutex // rwLock is a read-write mutex which provides concurrent reads but exclusive writes
lookupTable map[uint]*keyRefNode // a hash table of <KeyType, *ValueType> for quick reference of values based on keys
frequencies *list.List // internal linked list that contains frequency mapping
maxSize uint // maxSize represents the maximum number of elements that can be in the cache before eviction
}
// MaxSize returns the maximum size of the cache at that point in time
func (lfu *FrameCache) MaxSize() uint {
lfu.rwLock.RLock()
defer lfu.rwLock.RUnlock()
return lfu.maxSize
}
// CurrentSize returns the number of elements in that cache
func (lfu *FrameCache) CurrentSize() uint {
lfu.rwLock.RLock()
defer lfu.rwLock.RUnlock()
return uint(len(lfu.lookupTable))
}
// IsFull checks if the LFU cache is full
func (lfu *FrameCache) IsFull() bool {
lfu.rwLock.RLock()
defer lfu.rwLock.RUnlock()
return uint(len(lfu.lookupTable)) == lfu.maxSize
}
// SetMaxSize updates the max size of the LFU cache
func (lfu *FrameCache) SetMaxSize(size uint) {
lfu.rwLock.Lock()
defer lfu.rwLock.Unlock()
lfu.maxSize = size
}
// evict the least recently used element from the cache, this function is unsafe to be called externally
// because it doesn't provide locking mechanism.
func (lfu *FrameCache) unsafeEvict() error {
// WARNING: This function assumes that a write lock has been held by the caller already
// get the head node of the list
headFreq := lfu.frequencies.Front()
if headFreq == nil {
// list is empty, this is a very unusual condition
return fmt.Errorf("internal error: failed to evict, empty frequency list")
}
headFreqInner := (headFreq.Value).(*frequencyNode)
if headFreqInner.valuesList.Len() == 0 {
// again this is a very unusual condition
return fmt.Errorf("internal error: failed to evict, empty values list")
}
headValuesList := headFreqInner.valuesList
// pop the head of this this values list
headValueNode := headValuesList.Front()
removeResult := headValuesList.Remove(headValueNode).(*keyRefNode)
// update the values list
headFreqInner.valuesList = headValuesList
if headFreqInner.valuesList.Len() == 0 && headFreqInner.count > 1 {
// this node can be removed from the frequency list
freqList := lfu.frequencies
freqList.Remove(headFreq)
lfu.frequencies = freqList
}
// remove the key from lookup table
key := removeResult.keyRef
delete(lfu.lookupTable, key)
return nil
}
// Put method inserts a `<KeyType, ValueType>` to the LFU cache and updates internal
// data structures to keep track of access frequencies, if the cache is full, it evicts the
// least frequently used value from the cache.
func (lfu *FrameCache) Put(key uint, value *Frame) error {
// get write lock
lfu.rwLock.Lock()
defer lfu.rwLock.Unlock()
if _, ok := lfu.lookupTable[key]; ok {
// update the cache value
lfu.lookupTable[key].valueRef = value
return nil
}
if lfu.maxSize == uint(len(lfu.lookupTable)) {
lfu.unsafeEvict()
}
valueNode := newKeyRefNode(key, value, nil)
head := lfu.frequencies.Front()
if head == nil {
// fresh linked list
freqNode := newFrequencyNode(1)
head = lfu.frequencies.PushFront(freqNode)
freqNode.inner = head
} else {
node := head.Value.(*frequencyNode)
if node.count != 1 {
freqNode := newFrequencyNode(1)
head = lfu.frequencies.PushFront(freqNode)
freqNode.inner = head
}
}
valueNode.parentFreqNode = head
node := head.Value.(*frequencyNode)
head = node.valuesList.PushBack(valueNode)
valueNode.inner = head
lfu.lookupTable[key] = valueNode
return nil
}
// Evict can be called to manually perform eviction
func (lfu *FrameCache) Evict() error {
lfu.rwLock.Lock()
defer lfu.rwLock.Unlock()
return lfu.unsafeEvict()
}
func (lfu *FrameCache) unsafeUpdateFrequency(valueNode *keyRefNode) {
parentFreqNode := valueNode.parentFreqNode
currentNode := parentFreqNode.Value.(*frequencyNode)
nextParentFreqNode := parentFreqNode.Next()
var newParent *list.Element = nil
if nextParentFreqNode == nil {
// this is the last node
// create a new node with frequency + 1
newFreqNode := newFrequencyNode(currentNode.count + 1)
lfu.frequencies.PushBack(newFreqNode)
newParent = parentFreqNode.Next()
} else {
nextNode := nextParentFreqNode.Value.(*frequencyNode)
if nextNode.count == (currentNode.count + 1) {
newParent = nextParentFreqNode
} else {
// insert a node in between
newFreqNode := newFrequencyNode(currentNode.count + 1)
lfu.frequencies.InsertAfter(newFreqNode, parentFreqNode)
newParent = parentFreqNode.Next()
}
}
// remove from the existing list
currentNode.valuesList.Remove(valueNode.inner)
newParentNode := newParent.Value.(*frequencyNode)
valueNode.parentFreqNode = newParent
newValueNode := newParentNode.valuesList.PushBack(valueNode)
valueNode.inner = newValueNode
// check if the current node is empty
if currentNode.valuesList.Len() == 0 {
// remove the current node
lfu.frequencies.Remove(parentFreqNode)
}
}
// Get can be called to obtain the value for given key
func (lfu *FrameCache) Get(key uint) (*Frame, bool) {
lfu.rwLock.Lock()
defer lfu.rwLock.Unlock()
// check if data is in the map
valueNode, found := lfu.lookupTable[key]
if !found {
return nil, false
}
lfu.unsafeUpdateFrequency(valueNode)
return valueNode.valueRef, true
}
// Delete removes the specified entry from LFU cache
func (lfu *FrameCache) Delete(key uint) error {
lfu.rwLock.Lock()
defer lfu.rwLock.Unlock()
// check if the key is in the map
valueNode, found := lfu.lookupTable[key]
if !found {
return fmt.Errorf("key %v not found", key)
}
parentFreqNode := valueNode.parentFreqNode
currentNode := (parentFreqNode.Value).(*frequencyNode)
currentNode.valuesList.Remove(valueNode.inner)
if currentNode.valuesList.Len() == 0 {
lfu.frequencies.Remove(parentFreqNode)
}
delete(lfu.lookupTable, key)
return nil
}
// NewFrameCache ...
func NewFrameCache(maxSize uint) *FrameCache {
return &FrameCache{
rwLock: sync.RWMutex{},
lookupTable: make(map[uint]*keyRefNode),
maxSize: maxSize,
frequencies: list.New(),
}
}