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

Immutable rest #2

Open
wants to merge 2 commits into
base: immutable_inserts
Choose a base branch
from
Open
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
50 changes: 37 additions & 13 deletions rangetree/immutable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package rangetree

import "github.com/Workiva/go-datastructures/slice"

type immutableRangeTree struct {
type ImmutableRangeTree struct {
number uint64
top orderedNodes
dimensions uint64
Expand All @@ -16,11 +16,11 @@ func newCache(dimensions uint64) []slice.Int64Slice {
return cache
}

func (irt *immutableRangeTree) needNextDimension() bool {
func (irt *ImmutableRangeTree) needNextDimension() bool {
return irt.dimensions > 1
}

func (irt *immutableRangeTree) add(nodes *orderedNodes, cache []slice.Int64Slice, entry Entry, added *uint64) {
func (irt *ImmutableRangeTree) add(nodes *orderedNodes, cache []slice.Int64Slice, entry Entry, added *uint64) {
var node *node
list := nodes

Expand Down Expand Up @@ -59,7 +59,7 @@ func (irt *immutableRangeTree) add(nodes *orderedNodes, cache []slice.Int64Slice

// Add will add the provided entries into the tree and return
// a new tree with those entries added.
func (irt *immutableRangeTree) Add(entries ...Entry) *immutableRangeTree {
func (irt *ImmutableRangeTree) Add(entries ...Entry) *ImmutableRangeTree {
if len(entries) == 0 {
return irt
}
Expand All @@ -83,8 +83,8 @@ func (irt *immutableRangeTree) Add(entries ...Entry) *immutableRangeTree {
// Returned are two lists and the modified tree. The first list is a
// list of entries that were moved. The second is a list entries that
// were deleted. These lists are exclusive.
func (irt *immutableRangeTree) InsertAtDimension(dimension uint64,
index, number int64) (*immutableRangeTree, Entries, Entries) {
func (irt *ImmutableRangeTree) InsertAtDimension(dimension uint64,
index, number int64) (*ImmutableRangeTree, Entries, Entries) {

if dimension > irt.dimensions || number == 0 {
return irt, nil, nil
Expand All @@ -110,7 +110,9 @@ type immutableNodeBundle struct {
newNode *node
}

func (irt *immutableRangeTree) Delete(entries ...Entry) *immutableRangeTree {
// Delete will remove the provided entries from the rangetree if they exist
// and return the modified rangetree.
func (irt *ImmutableRangeTree) Delete(entries ...Entry) *ImmutableRangeTree {
cache := newCache(irt.dimensions)
top := make(orderedNodes, len(irt.top))
copy(top, irt.top)
Expand All @@ -125,7 +127,7 @@ func (irt *immutableRangeTree) Delete(entries ...Entry) *immutableRangeTree {
return tree
}

func (irt *immutableRangeTree) delete(top *orderedNodes,
func (irt *ImmutableRangeTree) delete(top *orderedNodes,
cache []slice.Int64Slice, entry Entry, deleted *uint64) {

path := make([]*immutableNodeBundle, 0, 5)
Expand Down Expand Up @@ -181,7 +183,7 @@ func (irt *immutableRangeTree) delete(top *orderedNodes,
}
}

func (irt *immutableRangeTree) apply(list orderedNodes, interval Interval,
func (irt *ImmutableRangeTree) apply(list orderedNodes, interval Interval,
dimension uint64, fn func(*node) bool) bool {

low, high := interval.LowAtDimension(dimension), interval.HighAtDimension(dimension)
Expand All @@ -205,9 +207,19 @@ func (irt *immutableRangeTree) apply(list orderedNodes, interval Interval,
return true
}

// Apply will call the provided function with each entry that exists
// within the provided range, in order. Return false at any time to
// cancel iteration. Altering the entry in such a way that its location
// changes will result in undefined behavior.
func (irt *ImmutableRangeTree) Apply(interval Interval, fn func(Entry) bool) {
irt.apply(irt.top, interval, 1, func(n *node) bool {
return fn(n.entry)
})
}

// Query will return an ordered list of results in the given
// interval.
func (irt *immutableRangeTree) Query(interval Interval) Entries {
func (irt *ImmutableRangeTree) Query(interval Interval) Entries {
entries := NewEntries()

irt.apply(irt.top, interval, 1, func(n *node) bool {
Expand All @@ -219,12 +231,24 @@ func (irt *immutableRangeTree) Query(interval Interval) Entries {
}

// Len returns the number of items in this tree.
func (irt *immutableRangeTree) Len() uint64 {
func (irt *ImmutableRangeTree) Len() uint64 {
return irt.number
}

func newImmutableRangeTree(dimensions uint64) *immutableRangeTree {
return &immutableRangeTree{
func newImmutableRangeTree(dimensions uint64) *ImmutableRangeTree {
return &ImmutableRangeTree{
dimensions: dimensions,
}
}

// NewImmutable will construct and return an immutable rangetree.
// The immutable range tree is threadsafe without using locks.
// All methods on the rangetree returned this copy with the changes
// applied. In this way, you can always keep the prevoius rangetrees
// in history for querying history. Because this tree is immutable,
// its performance suffers in comparison with its mutable counterparts.
// This is especially true of shift types of operations which require
// a great deal of copying.
func NewImmutable(dimensions uint64) *ImmutableRangeTree {
return newImmutableRangeTree(dimensions)
}
78 changes: 77 additions & 1 deletion rangetree/immutable_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rangetree

import (
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -310,7 +311,7 @@ func TestImmutableMultiDimensionBulkDeletes(t *testing.T) {
assert.Equal(t, 0, tree3.Len())
}

func constructMultiDimensionalImmutableTree(number int64) (*immutableRangeTree, Entries) {
func constructMultiDimensionalImmutableTree(number int64) (*ImmutableRangeTree, Entries) {
tree := newImmutableRangeTree(2)
entries := make(Entries, 0, number)
for i := int64(0); i < number; i++ {
Expand Down Expand Up @@ -521,3 +522,78 @@ func BenchmarkImmutableInsertSecondDimension(b *testing.B) {
tree.InsertAtDimension(2, 0, 1)
}
}

func TestImmutableTreeApply(t *testing.T) {
tree, entries := constructMultiDimensionalImmutableTree(2)

result := make(Entries, 0, len(entries))

tree.Apply(constructMockInterval(dimension{0, 100}, dimension{0, 100}),
func(e Entry) bool {
result = append(result, e)
return true
},
)

assert.Equal(t, entries, result)
}

func TestImmutableApplyWithBail(t *testing.T) {
tree, entries := constructMultiDimensionalImmutableTree(2)

result := make(Entries, 0, 1)

tree.Apply(constructMockInterval(dimension{0, 100}, dimension{0, 100}),
func(e Entry) bool {
result = append(result, e)
return false
},
)

assert.Equal(t, entries[:1], result)
}

func BenchmarkImmutableApply(b *testing.B) {
numItems := 1000

tree, _ := constructMultiDimensionalImmutableTree(int64(numItems))

iv := constructMockInterval(
dimension{0, int64(numItems)}, dimension{0, int64(numItems)},
)
fn := func(Entry) bool { return true }

b.ResetTimer()

for i := 0; i < b.N; i++ {
tree.Apply(iv, fn)
}
}

// TestRaceCondition is designed to be run with the
// race detector on. If immutability is working as expected,
// there should not be any race conditions detected.
func TestRaceCondition(t *testing.T) {
var wg sync.WaitGroup
wg.Add(2)

tree, _ := constructMultiDimensionalImmutableTree(3)
iv := constructMockInterval(dimension{0, 10}, dimension{0, 10})
entry := constructMockEntry(4, 4, 4)

go func() {
for i := 0; i < 1000; i++ {
tree.Query(iv)
}
wg.Done()
}()

go func() {
for i := 0; i < 1000; i++ {
tree.Add(entry)
}
wg.Done()
}()

wg.Wait()
}