Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track nodes to deallocate #16

Merged
merged 1 commit into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 115 additions & 25 deletions allocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,72 @@ type Allocator struct {
config AllocatorConfig
data []byte
zeroNode []byte
nextNodeToAllocate uint64
nextNodeToAllocate NodeAddress
}

// Node returns node bytes.
func (a *Allocator) Node(n uint64) []byte {
return a.data[n*a.config.NodeSize : (n+1)*a.config.NodeSize]
func (a *Allocator) Node(nodeAddress NodeAddress) []byte {
return a.data[uint64(nodeAddress)*a.config.NodeSize : uint64(nodeAddress+1)*a.config.NodeSize]
}

// Allocate allocates new node.
func (a *Allocator) Allocate() (uint64, []byte) {
func (a *Allocator) Allocate() (NodeAddress, []byte) {
return a.allocate(a.zeroNode)
}

// Copy allocates new node and copies content from existing one.
func (a *Allocator) Copy(data []byte) (uint64, []byte) {
func (a *Allocator) Copy(data []byte) (NodeAddress, []byte) {
return a.allocate(data)
}

func (a *Allocator) allocate(copyFrom []byte) (uint64, []byte) {
func (a *Allocator) allocate(copyFrom []byte) (NodeAddress, []byte) {
n := a.nextNodeToAllocate
a.nextNodeToAllocate++
node := a.Node(n)
copy(node, copyFrom)
return n, node
}

// NewNodeAllocator creates new node allocator.
func NewNodeAllocator[T comparable](allocator *Allocator) (NodeAllocator[T], error) {
headerSize := uint64(unsafe.Sizeof(NodeHeader{}))
// NewDeallocator returns new deallocator.
func NewDeallocator(
snapshotID SnapshotID,
deallocationLists *Space[SnapshotID, NodeAddress],
listNodeAllocator ListNodeAllocator,
) Deallocator {
return Deallocator{
snapshotID: snapshotID,
deallocationLists: deallocationLists,
listNodeAllocator: listNodeAllocator,
}
}

// Deallocator tracks nodes to deallocate.
type Deallocator struct {
snapshotID SnapshotID
deallocationLists *Space[SnapshotID, NodeAddress]
listNodeAllocator ListNodeAllocator
}

// Deallocate adds node to the deallocation list.
func (d Deallocator) Deallocate(nodeAddress NodeAddress, srcSnapshotID SnapshotID) {
listNodeAddress, _ := d.deallocationLists.Get(srcSnapshotID)
list := NewList(ListConfig{
SnapshotID: d.snapshotID,
Item: listNodeAddress,
NodeAllocator: d.listNodeAllocator,
Deallocator: d,
})
list.Add(nodeAddress)
if list.config.Item != listNodeAddress {
d.deallocationLists.Set(srcSnapshotID, list.config.Item)
}
}

// NewSpaceNodeAllocator creates new space node allocator.
func NewSpaceNodeAllocator[T comparable](allocator *Allocator) (SpaceNodeAllocator[T], error) {
headerSize := uint64(unsafe.Sizeof(SpaceNodeHeader{}))
if headerSize >= allocator.config.NodeSize {
return NodeAllocator[T]{}, errors.New("node size is too small")
return SpaceNodeAllocator[T]{}, errors.New("node size is too small")
}

stateOffset := headerSize + headerSize%uint64Length
Expand All @@ -70,7 +105,7 @@ func NewNodeAllocator[T comparable](allocator *Allocator) (NodeAllocator[T], err

numOfItems := spaceLeft / (itemSize + 1) // 1 is for slot state
if numOfItems == 0 {
return NodeAllocator[T]{}, errors.New("node size is too small")
return SpaceNodeAllocator[T]{}, errors.New("node size is too small")
}
numOfItems, _ = highestPowerOfTwo(numOfItems)
spaceLeft -= numOfItems
Expand All @@ -79,10 +114,10 @@ func NewNodeAllocator[T comparable](allocator *Allocator) (NodeAllocator[T], err
numOfItems = spaceLeft / itemSize
numOfItems, bitsPerHop := highestPowerOfTwo(numOfItems)
if numOfItems == 0 {
return NodeAllocator[T]{}, errors.New("node size is too small")
return SpaceNodeAllocator[T]{}, errors.New("node size is too small")
}

return NodeAllocator[T]{
return SpaceNodeAllocator[T]{
allocator: allocator,
numOfItems: int(numOfItems),
stateOffset: stateOffset,
Expand All @@ -92,8 +127,8 @@ func NewNodeAllocator[T comparable](allocator *Allocator) (NodeAllocator[T], err
}, nil
}

// NodeAllocator converts nodes from bytes to objects.
type NodeAllocator[T comparable] struct {
// SpaceNodeAllocator converts nodes from bytes to space objects.
type SpaceNodeAllocator[T comparable] struct {
allocator *Allocator

numOfItems int
Expand All @@ -104,41 +139,96 @@ type NodeAllocator[T comparable] struct {
}

// Get returns object for node.
func (na NodeAllocator[T]) Get(n uint64) ([]byte, Node[T]) {
node := na.allocator.Node(n)
func (na SpaceNodeAllocator[T]) Get(nodeAddress NodeAddress) ([]byte, SpaceNode[T]) {
node := na.allocator.Node(nodeAddress)
return node, na.project(node)
}

// Allocate allocates new object.
func (na NodeAllocator[T]) Allocate() (uint64, Node[T]) {
func (na SpaceNodeAllocator[T]) Allocate() (NodeAddress, SpaceNode[T]) {
n, node := na.allocator.Allocate()
return n, na.project(node)
}

// Copy allocates copy of existing object.
func (na NodeAllocator[T]) Copy(data []byte) (uint64, Node[T]) {
func (na SpaceNodeAllocator[T]) Copy(data []byte) (NodeAddress, SpaceNode[T]) {
n, node := na.allocator.Copy(data)
return n, na.project(node)
}

// Index returns element index based on hash.
func (na NodeAllocator[T]) Index(hash uint64) uint64 {
return hash & na.indexMask
func (na SpaceNodeAllocator[T]) Index(hash Hash) uint64 {
return uint64(hash) & na.indexMask
}

// Shift shifts bits in hash.
func (na NodeAllocator[T]) Shift(hash uint64) uint64 {
func (na SpaceNodeAllocator[T]) Shift(hash Hash) Hash {
return hash >> na.bitsPerHop
}

func (na NodeAllocator[T]) project(node []byte) Node[T] {
return Node[T]{
Header: photon.FromBytes[NodeHeader](node),
func (na SpaceNodeAllocator[T]) project(node []byte) SpaceNode[T] {
return SpaceNode[T]{
Header: photon.FromBytes[SpaceNodeHeader](node),
States: photon.SliceFromBytes[State](node[na.stateOffset:], na.numOfItems),
Items: photon.SliceFromBytes[T](node[na.itemOffset:], na.numOfItems),
}
}

// NewListNodeAllocator creates new list node allocator.
func NewListNodeAllocator(allocator *Allocator) (ListNodeAllocator, error) {
headerSize := uint64(unsafe.Sizeof(ListNodeHeader{}))
if headerSize >= allocator.config.NodeSize {
return ListNodeAllocator{}, errors.New("node size is too small")
}

stateOffset := headerSize + headerSize%uint64Length
spaceLeft := allocator.config.NodeSize - stateOffset

numOfItems := spaceLeft / uint64Length
if numOfItems < 2 {
return ListNodeAllocator{}, errors.New("node size is too small")
}

return ListNodeAllocator{
allocator: allocator,
numOfItems: int(numOfItems),
itemOffset: allocator.config.NodeSize - spaceLeft,
}, nil
}

// ListNodeAllocator converts nodes from bytes to list objects.
type ListNodeAllocator struct {
allocator *Allocator

numOfItems int
itemOffset uint64
}

// Get returns object for node.
func (na ListNodeAllocator) Get(nodeAddress NodeAddress) ([]byte, ListNode) {
node := na.allocator.Node(nodeAddress)
return node, na.project(node)
}

// Allocate allocates new object.
func (na ListNodeAllocator) Allocate() (NodeAddress, ListNode) {
n, node := na.allocator.Allocate()
return n, na.project(node)
}

// Copy allocates copy of existing object.
func (na ListNodeAllocator) Copy(data []byte) (NodeAddress, ListNode) {
n, node := na.allocator.Copy(data)
return n, na.project(node)
}

func (na ListNodeAllocator) project(node []byte) ListNode {
return ListNode{
Header: photon.FromBytes[ListNodeHeader](node),
Items: photon.SliceFromBytes[NodeAddress](node[na.itemOffset:], na.numOfItems),
}
}

func highestPowerOfTwo(n uint64) (uint64, uint64) {
var m uint64 = 1
var p uint64
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/outofforest/quantum

go 1.22
go 1.23

require (
github.com/cespare/xxhash v1.1.0
Expand Down
118 changes: 118 additions & 0 deletions list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package quantum

// ListConfig stores list configuration.
type ListConfig struct {
SnapshotID SnapshotID
Item NodeAddress
NodeAllocator ListNodeAllocator
Deallocator Deallocator
}

// NewList creates new list.
func NewList(config ListConfig) *List {
return &List{
config: config,
}
}

// List represents the list of node addresses.
type List struct {
config ListConfig
}

// Add adds address to the list.
func (l *List) Add(nodeAddress NodeAddress) {
if l.config.Item == 0 {
newNodeAddress, newNode := l.config.NodeAllocator.Allocate()
newNode.Items[0] = nodeAddress
newNode.Header.SnapshotID = l.config.SnapshotID
newNode.Header.NumOfItems = 1
l.config.Item = newNodeAddress

return
}
listNodeData, listNode := l.config.NodeAllocator.Get(l.config.Item)
if listNode.Header.NumOfItems+listNode.Header.NumOfSideLists < uint64(len(listNode.Items)) {
if listNode.Header.SnapshotID < l.config.SnapshotID {
newNodeAddress, newNode := l.config.NodeAllocator.Copy(listNodeData)
newNode.Header.SnapshotID = l.config.SnapshotID
oldNodeAddress := l.config.Item
l.config.Item = newNodeAddress
l.config.Deallocator.Deallocate(oldNodeAddress, listNode.Header.SnapshotID)
listNode = newNode
}

listNode.Items[listNode.Header.NumOfItems] = nodeAddress
listNode.Header.NumOfItems++

return
}

newNodeAddress, newNode := l.config.NodeAllocator.Allocate()
newNode.Items[0] = nodeAddress
newNode.Items[len(newNode.Items)-1] = l.config.Item
newNode.Header.SnapshotID = l.config.SnapshotID
newNode.Header.NumOfItems = 1
newNode.Header.NumOfSideLists = 1
l.config.Item = newNodeAddress
}

// Attach attaches another list.
func (l *List) Attach(nodeAddress NodeAddress) {
if l.config.Item == 0 {
l.config.Item = nodeAddress
return
}
listNodeData, listNode := l.config.NodeAllocator.Get(l.config.Item)
if listNode.Header.NumOfItems+listNode.Header.NumOfSideLists < uint64(len(listNode.Items)) {
if listNode.Header.SnapshotID < l.config.SnapshotID {
newNodeAddress, newNode := l.config.NodeAllocator.Copy(listNodeData)
newNode.Header.SnapshotID = l.config.SnapshotID
oldNodeAddress := l.config.Item
l.config.Item = newNodeAddress
l.config.Deallocator.Deallocate(oldNodeAddress, listNode.Header.SnapshotID)
listNode = newNode
}

listNode.Items[uint64(len(listNode.Items))-listNode.Header.NumOfSideLists-1] = nodeAddress
listNode.Header.NumOfSideLists++

return
}

newNodeAddress, newNode := l.config.NodeAllocator.Allocate()
newNode.Items[uint64(len(listNode.Items))-1] = l.config.Item
newNode.Items[uint64(len(listNode.Items))-2] = nodeAddress
newNode.Header.SnapshotID = l.config.SnapshotID
newNode.Header.NumOfSideLists = 2
l.config.Item = newNodeAddress
}

// Iterator returns iterator iterating over elements in the list.
func (l *List) Iterator() func(func(NodeAddress) bool) {
return func(yield func(NodeAddress) bool) {
if l.config.Item == 0 {
return
}

// FIXME (wojciech): Optimize heap allocations.
stack := []NodeAddress{l.config.Item}
for {
if len(stack) == 0 {
return
}

n := stack[len(stack)-1]
stack = stack[:len(stack)-1]

_, listNode := l.config.NodeAllocator.Get(n)
for i := range listNode.Header.NumOfItems {
if !yield(listNode.Items[i]) {
return
}
}

stack = append(stack, listNode.Items[uint64(len(listNode.Items))-listNode.Header.NumOfSideLists:]...)
}
}
}
Loading