From fb64d4d840e038f72522d9e4792f208e2f36b928 Mon Sep 17 00:00:00 2001 From: mcrakhman Date: Sat, 20 Aug 2022 23:22:01 +0200 Subject: [PATCH] Add tree tests and benchmarks --- pkg/acl/list/list.go | 9 +- pkg/acl/tree/tree.go | 4 +- pkg/acl/tree/tree_test.go | 214 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 pkg/acl/tree/tree_test.go diff --git a/pkg/acl/list/list.go b/pkg/acl/list/list.go index ee4a570b..54863bee 100644 --- a/pkg/acl/list/list.go +++ b/pkg/acl/list/list.go @@ -6,14 +6,19 @@ import ( "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/account" "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/aclchanges/aclpb" "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/storage" - "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/tree" "sync" ) type IterFunc = func(record *Record) (IsContinue bool) +type RWLocker interface { + sync.Locker + RLock() + RUnlock() +} + type ACLList interface { - tree.RWLocker + RWLocker ID() string Header() *aclpb.Header ACLState() *ACLState diff --git a/pkg/acl/tree/tree.go b/pkg/acl/tree/tree.go index a6c6d8ac..1836719c 100644 --- a/pkg/acl/tree/tree.go +++ b/pkg/acl/tree/tree.go @@ -15,7 +15,6 @@ const ( Nothing ) -// TODO: consider abstracting into separate package with iterator, remove type Tree struct { root *Change headIds []string @@ -155,7 +154,6 @@ func (t *Tree) add(c *Change) (attached bool) { } // attaching only if all prev ids are attached attached = true - // the logic below is the following for _, pid := range c.PreviousIds { if _, ok := t.attached[pid]; ok { continue @@ -210,7 +208,7 @@ func (t *Tree) attach(c *Change, newEl bool) { break } } - prev.Next = append(prev.Next[:insertIdx+1], prev.Next[:insertIdx]...) + prev.Next = append(prev.Next[:insertIdx+1], prev.Next[insertIdx:]...) prev.Next[insertIdx] = c } } diff --git a/pkg/acl/tree/tree_test.go b/pkg/acl/tree/tree_test.go new file mode 100644 index 00000000..cc2c82bc --- /dev/null +++ b/pkg/acl/tree/tree_test.go @@ -0,0 +1,214 @@ +package tree + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "math/rand" + "testing" + "time" +) + +func newChange(id string, snapshotId string, prevIds ...string) *Change { + return &Change{ + PreviousIds: prevIds, + Id: id, + SnapshotId: snapshotId, + IsSnapshot: false, + } +} + +func newSnapshot(id string, snapshotId string, prevIds ...string) *Change { + return &Change{ + PreviousIds: prevIds, + Id: id, + SnapshotId: snapshotId, + IsSnapshot: true, + } +} + +func TestTree_Add(t *testing.T) { + t.Run("add first el", func(t *testing.T) { + tr := new(Tree) + assert.Equal(t, Rebuild, tr.Add(newSnapshot("root", ""))) + assert.Equal(t, tr.root.Id, "root") + assert.Equal(t, []string{"root"}, tr.Heads()) + }) + t.Run("linear add", func(t *testing.T) { + tr := new(Tree) + assert.Equal(t, Rebuild, tr.Add( + newSnapshot("root", ""), + newChange("one", "root", "root"), + newChange("two", "root", "one"), + )) + assert.Equal(t, []string{"two"}, tr.Heads()) + assert.Equal(t, Append, tr.Add(newChange("three", "root", "two"))) + el := tr.root + var ids []string + for el != nil { + ids = append(ids, el.Id) + if len(el.Next) > 0 { + el = el.Next[0] + } else { + el = nil + } + } + assert.Equal(t, []string{"root", "one", "two", "three"}, ids) + assert.Equal(t, []string{"three"}, tr.Heads()) + }) + t.Run("branch", func(t *testing.T) { + tr := new(Tree) + assert.Equal(t, Rebuild, tr.Add( + newSnapshot("root", ""), + newChange("1", "root", "root"), + newChange("2", "root", "1"), + )) + assert.Equal(t, []string{"2"}, tr.Heads()) + assert.Equal(t, Rebuild, tr.Add( + newChange("1.2", "root", "1.1"), + newChange("1.3", "root", "1.2"), + newChange("1.1", "root", "1"), + )) + assert.Len(t, tr.attached["1"].Next, 2) + assert.Len(t, tr.unAttached, 0) + assert.Len(t, tr.attached, 6) + assert.Equal(t, []string{"1.3", "2"}, tr.Heads()) + }) + t.Run("branch union", func(t *testing.T) { + tr := new(Tree) + assert.Equal(t, Rebuild, tr.Add( + newSnapshot("root", ""), + newChange("1", "root", "root"), + newChange("2", "root", "1"), + newChange("1.2", "root", "1.1"), + newChange("1.3", "root", "1.2"), + newChange("1.1", "root", "1"), + newChange("3", "root", "2", "1.3"), + newChange("4", "root", "3"), + )) + assert.Len(t, tr.unAttached, 0) + assert.Len(t, tr.attached, 8) + assert.Equal(t, []string{"4"}, tr.Heads()) + }) + t.Run("big set", func(t *testing.T) { + tr := new(Tree) + tr.Add(newSnapshot("root", "")) + var changes []*Change + for i := 0; i < 10000; i++ { + if i == 0 { + changes = append(changes, newChange(fmt.Sprint(i), "root", "root")) + } else { + changes = append(changes, newChange(fmt.Sprint(i), "root", fmt.Sprint(i-1))) + } + } + st := time.Now() + tr.AddFast(changes...) + t.Log(time.Since(st)) + assert.Equal(t, []string{"9999"}, tr.Heads()) + }) +} + +func TestTree_Hash(t *testing.T) { + tr := new(Tree) + tr.Add(newSnapshot("root", "")) + hash1 := tr.Hash() + assert.Equal(t, tr.Hash(), hash1) + tr.Add(newChange("1", "root", "root")) + assert.NotEqual(t, tr.Hash(), hash1) + assert.Equal(t, tr.Hash(), tr.Hash()) +} + +func TestTree_AddFuzzy(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + getChanges := func() []*Change { + changes := []*Change{ + newChange("1", "root", "root"), + newChange("2", "root", "1"), + newChange("1.2", "root", "1.1"), + newChange("1.3", "root", "1.2"), + newChange("1.1", "root", "1"), + newChange("3", "root", "2", "1.3"), + } + rand.Shuffle(len(changes), func(i, j int) { + changes[i], changes[j] = changes[j], changes[i] + }) + return changes + } + var phash string + for i := 0; i < 100; i++ { + tr := new(Tree) + tr.Add(newSnapshot("root", "")) + tr.Add(getChanges()...) + assert.Len(t, tr.unAttached, 0) + assert.Len(t, tr.attached, 7) + hash := tr.Hash() + if phash != "" { + assert.Equal(t, phash, hash) + } + phash = hash + assert.Equal(t, []string{"3"}, tr.Heads()) + } +} + +func TestTree_Iterate(t *testing.T) { + t.Run("complex tree", func(t *testing.T) { + tr := new(Tree) + tr.Add( + newSnapshot("0", ""), + newChange("1", "0", "0"), + newChange("1.1", "0", "1"), + newChange("1.2", "0", "1"), + newChange("1.3", "0", "1"), + newChange("1.3.1", "0", "1.3"), + newChange("1.2+3", "0", "1.2", "1.3.1"), + newChange("1.2+3.1", "0", "1.2+3"), + newChange("10", "0", "1.2+3.1", "1.1"), + newChange("last", "0", "10"), + ) + var res []string + tr.Iterate("0", func(c *Change) (isContinue bool) { + res = append(res, c.Id) + return true + }) + res = res[:0] + tr.Iterate("0", func(c *Change) (isContinue bool) { + res = append(res, c.Id) + return true + }) + assert.Equal(t, []string{"0", "1", "1.1", "1.2", "1.3", "1.3.1", "1.2+3", "1.2+3.1", "10", "last"}, res) + }) +} + +func BenchmarkTree_Add(b *testing.B) { + getChanges := func() []*Change { + return []*Change{ + newChange("1", "root", "root"), + newChange("2", "root", "1"), + newChange("1.2", "root", "1.1"), + newChange("1.3", "root", "1.2"), + newChange("1.1", "root", "1"), + newChange("3", "root", "2", "1.3"), + } + } + b.Run("by one", func(b *testing.B) { + tr := new(Tree) + tr.Add(newSnapshot("root", "")) + tr.Add(getChanges()...) + for i := 0; i < b.N; i++ { + tr.Add(newChange(fmt.Sprint(i+4), "root", fmt.Sprint(i+3))) + } + }) + b.Run("add", func(b *testing.B) { + for i := 0; i < b.N; i++ { + tr := new(Tree) + tr.Add(newSnapshot("root", "")) + tr.Add(getChanges()...) + } + }) + b.Run("add fast", func(b *testing.B) { + for i := 0; i < b.N; i++ { + tr := new(Tree) + tr.AddFast(newSnapshot("root", "")) + tr.AddFast(getChanges()...) + } + }) +}