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

Add garbage collection for text type #104

Merged
merged 15 commits into from
Dec 21, 2020
Merged
2 changes: 2 additions & 0 deletions api/converter/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/yorkie-team/yorkie/api/converter"
"github.com/yorkie-team/yorkie/pkg/document"
"github.com/yorkie-team/yorkie/pkg/document/proxy"
time2 "github.com/yorkie-team/yorkie/pkg/document/time"
hackerwins marked this conversation as resolved.
Show resolved Hide resolved
)

func TestConverter(t *testing.T) {
Expand Down Expand Up @@ -153,6 +154,7 @@ func TestConverter(t *testing.T) {

pbPack := converter.ToChangePack(d1.CreateChangePack())
pack, err := converter.FromChangePack(pbPack)
pack.MinSyncedTicket = time2.MaxTicket
assert.NoError(t, err)

d2 := document.New("c1", "d1")
Expand Down
93 changes: 85 additions & 8 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ func TestClientAndDocument(t *testing.T) {
assert.Equal(t, d1.Marshal(), d2.Marshal())
})

t.Run("garbage collection test", func(t *testing.T) {
t.Run("garbage collection for container type test", func(t *testing.T) {
ctx := context.Background()

d1 := document.New(testhelper.Collection, t.Name())
Expand Down Expand Up @@ -874,6 +874,79 @@ func TestClientAndDocument(t *testing.T) {
assert.Equal(t, 0, d2.GarbageLen())
})

t.Run("garbage collection for text type test", func(t *testing.T) {
ctx := context.Background()

d1 := document.New(testhelper.Collection, t.Name())
err := c1.Attach(ctx, d1)
assert.NoError(t, err)

d2 := document.New(testhelper.Collection, t.Name())
err = c2.Attach(ctx, d2)
assert.NoError(t, err)

err = d1.Update(func(root *proxy.ObjectProxy) error {
root.SetNewText("text").
Edit(0, 0, "Hello world")
root.SetNewRichText("richText").
Edit(0, 0, "Hello world", nil)
return nil
}, "sets test and richText")
assert.NoError(t, err)
assert.Equal(t, 0, d1.GarbageLen())
assert.Equal(t, 0, d2.GarbageLen())

// (0, 0) -> (1, 0): syncedseqs:(0, 0)
err = c1.Sync(ctx)
assert.NoError(t, err)

// (1, 0) -> (1, 1): syncedseqs:(0, 0)
err = c2.Sync(ctx)
assert.NoError(t, err)

err = d2.Update(func(root *proxy.ObjectProxy) error {
root.GetText("text").
Edit(0, 1, "a").
Edit(1, 2, "b")
root.GetRichText("richText").
Edit(0, 1, "a", map[string]string{"b": "1"})
return nil
}, "edit text type elements")
assert.NoError(t, err)
assert.Equal(t, 0, d1.GarbageLen())
assert.Equal(t, 3, d2.GarbageLen())

// (1, 1) -> (1, 2): syncedseqs:(0, 1)
err = c2.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 0, d1.GarbageLen())
assert.Equal(t, 3, d2.GarbageLen())

// (1, 2) -> (2, 2): syncedseqs:(1, 1)
err = c1.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 3, d1.GarbageLen())
assert.Equal(t, 3, d2.GarbageLen())

// (2, 2) -> (2, 2): syncedseqs:(1, 2)
err = c2.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 3, d1.GarbageLen())
assert.Equal(t, 3, d2.GarbageLen())

// (2, 2) -> (2, 2): syncedseqs:(2, 2): meet GC condition
err = c1.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 0, d1.GarbageLen())
assert.Equal(t, 3, d2.GarbageLen())

// (2, 2) -> (2, 2): syncedseqs:(2, 2): meet GC condition
err = c2.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 0, d1.GarbageLen())
assert.Equal(t, 0, d2.GarbageLen())
})

t.Run("garbage collection with detached document test", func(t *testing.T) {
ctx := context.Background()

Expand All @@ -889,8 +962,10 @@ func TestClientAndDocument(t *testing.T) {
root.SetInteger("1", 1)
root.SetNewArray("2").AddInteger(1, 2, 3)
root.SetInteger("3", 3)
root.SetNewText("4").Edit(0, 0, "Hi")
root.SetNewRichText("5").Edit(0, 0, "Hi", nil)
return nil
}, "sets 1,2,3")
}, "sets 1,2,3,4,5")
assert.NoError(t, err)
assert.Equal(t, 0, d1.GarbageLen())
assert.Equal(t, 0, d2.GarbageLen())
Expand All @@ -905,16 +980,18 @@ func TestClientAndDocument(t *testing.T) {

err = d1.Update(func(root *proxy.ObjectProxy) error {
root.Delete("2")
root.GetText("4").Edit(0, 1, "h")
root.GetRichText("5").Edit(0, 1, "h", map[string]string{"b": "1"})
return nil
}, "removes 2")
}, "removes 2 and edit text type elements")
assert.NoError(t, err)
assert.Equal(t, 4, d1.GarbageLen())
assert.Equal(t, 6, d1.GarbageLen())
assert.Equal(t, 0, d2.GarbageLen())

// (1, 1) -> (2, 1): syncedseqs:(1, 0)
err = c1.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 4, d1.GarbageLen())
assert.Equal(t, 6, d1.GarbageLen())
assert.Equal(t, 0, d2.GarbageLen())

err = c2.Detach(ctx, d2)
Expand All @@ -923,14 +1000,14 @@ func TestClientAndDocument(t *testing.T) {
// (2, 1) -> (2, 2): syncedseqs:(1, x)
err = c2.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 4, d1.GarbageLen())
assert.Equal(t, 4, d2.GarbageLen())
assert.Equal(t, 6, d1.GarbageLen())
assert.Equal(t, 6, d2.GarbageLen())

// (2, 2) -> (2, 2): syncedseqs:(2, x): meet GC condition
err = c1.Sync(ctx)
assert.NoError(t, err)
assert.Equal(t, 0, d1.GarbageLen())
assert.Equal(t, 4, d2.GarbageLen())
assert.Equal(t, 6, d2.GarbageLen())
})
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/document/change/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ func (c *Context) RegisterElement(elem json.Element) {
func (c *Context) RegisterRemovedElementPair(parent json.Container, deleted json.Element) {
c.root.RegisterRemovedElementPair(parent, deleted)
}

func (c *Context) RegisterRemovedNodeTextElement(textType json.TextElement) {
c.root.RegisterRemovedNodeTextElement(textType)
}
2 changes: 1 addition & 1 deletion pkg/document/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import (
// edit the document.
type Document struct {
// doc is the original data of the actual document.
doc *InternalDocument
doc *InternalDocument

// clone is a copy of `doc` to be exposed to the user and is used to
// protect `doc`.
Expand Down
154 changes: 154 additions & 0 deletions pkg/document/document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ package document_test
import (
"errors"
"fmt"
"runtime"
"testing"

"github.com/yorkie-team/yorkie/pkg/document/json"

"github.com/stretchr/testify/assert"

"github.com/yorkie-team/yorkie/api/converter"
hackerwins marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -171,6 +174,157 @@ func TestDocument(t *testing.T) {
testhelper.PrintMemStats()
})

t.Run("garbage collection for text test", func(t *testing.T) {
doc := document.New("c1", "d1")
assert.Equal(t, "{}", doc.Marshal())
assert.False(t, doc.HasLocalChanges())

// check garbage length
expected := `{"k1":"Hello mario"}`
err := doc.Update(func(root *proxy.ObjectProxy) error {
root.SetNewText("k1").
Edit(0, 0, "Hello world").
Edit(6, 11, "mario")
assert.Equal(t, expected, root.Marshal())
return nil
}, "edit text k1")
assert.NoError(t, err)
assert.Equal(t, expected, doc.Marshal())
assert.Equal(t, 1, doc.GarbageLen())

expected = `{"k1":"Hi jane"}`
err = doc.Update(func(root *proxy.ObjectProxy) error {
text := root.GetText("k1")
text.Edit(0, 5, "Hi")
text.Edit(3, 4, "j")
text.Edit(4, 8, "ane")
assert.Equal(t, expected, root.Marshal())
return nil
}, "edit text k1")
assert.NoError(t, err)
assert.Equal(t, expected, doc.Marshal())

expectedGarbageLen := 4
assert.Equal(t, expectedGarbageLen, doc.GarbageLen())
// garbage collect
assert.Equal(t, expectedGarbageLen, doc.GarbageCollect(time.MaxTicket))
})

t.Run("garbage collection for rich text test", func(t *testing.T) {
doc := document.New("c1", "d1")
assert.Equal(t, "{}", doc.Marshal())
assert.False(t, doc.HasLocalChanges())

// check garbage length
expected := `{"k1":[{"attrs":{"b":"1"},"val":"Hello "},{"attrs":{},"val":"mario"}]}`
err := doc.Update(func(root *proxy.ObjectProxy) error {
root.SetNewRichText("k1").
Edit(0, 0, "Hello world", map[string]string{"b": "1"}).
Edit(6, 11, "mario", nil)
assert.Equal(t, expected, root.Marshal())
return nil
}, "edit text k1")
assert.NoError(t, err)
assert.Equal(t, expected, doc.Marshal())
assert.Equal(t, 1, doc.GarbageLen())

expected = `{"k1":[{"attrs":{"b":"1"},"val":"Hi"},{"attrs":{"b":"1"},"val":" "},{"attrs":{},"val":"j"},{"attrs":{"b":"1"},"val":"ane"}]}`
err = doc.Update(func(root *proxy.ObjectProxy) error {
text := root.GetRichText("k1")
text.Edit(0, 5, "Hi", map[string]string{"b": "1"})
text.Edit(3, 4, "j", nil)
text.Edit(4, 8, "ane", map[string]string{"b": "1"})
assert.Equal(t, expected, root.Marshal())
return nil
}, "edit text k1")
assert.NoError(t, err)
assert.Equal(t, expected, doc.Marshal())

expectedGarbageLen := 4
assert.Equal(t, expectedGarbageLen, doc.GarbageLen())
// garbage collect
assert.Equal(t, expectedGarbageLen, doc.GarbageCollect(time.MaxTicket))
})

t.Run("garbage collection for large size of text garbage test", func(t *testing.T) {
doc := document.New("c1", "d1")
assert.Equal(t, "{}", doc.Marshal())
assert.False(t, doc.HasLocalChanges())

printMemStats := func(root *json.Object) {
bytes, err := converter.ObjectToBytes(doc.RootObject())
assert.NoError(t, err)
testhelper.PrintBytesSize(bytes)
testhelper.PrintMemStats()
}

textSize := 1_000
// 01. initial
err := doc.Update(func(root *proxy.ObjectProxy) error {
text := root.SetNewText("k1")
for i := 0; i < textSize; i++ {
text.Edit(i, i, "a")
}
return nil
}, "initial")
assert.NoError(t, err)
fmt.Println("-----initial")
printMemStats(doc.RootObject())

// 02. 1000 nodes modified
err = doc.Update(func(root *proxy.ObjectProxy) error {
text := root.GetText("k1")
for i := 0; i < textSize; i++ {
text.Edit(i, i+1, "b")
}
return nil
}, "1000 nodes modified")
assert.NoError(t, err)
fmt.Println("-----1000 nodes modified")
printMemStats(doc.RootObject())
assert.Equal(t, textSize, doc.GarbageLen())

// 03. GC
assert.Equal(t, textSize, doc.GarbageCollect(time.MaxTicket))
runtime.GC()
fmt.Println("-----Garbage collect")
printMemStats(doc.RootObject())

// 04. long text by one node
err = doc.Update(func(root *proxy.ObjectProxy) error {
text := root.SetNewText("k2")
str := ""
for i := 0; i < textSize; i++ {
str += "a"
}
text.Edit(0, 0, str)
return nil
}, "initial")
fmt.Println("-----long text by one node")
printMemStats(doc.RootObject())

// 05. Modify one node multiple times
err = doc.Update(func(root *proxy.ObjectProxy) error {
text := root.GetText("k2")
for i := 0; i < textSize; i++ {
if i != textSize {
text.Edit(i, i+1, "b")
}
}
return nil
}, "Modify one node multiple times")
assert.NoError(t, err)
fmt.Println("-----Modify one node multiple times")
printMemStats(doc.RootObject())

// 06. GC
assert.Equal(t, textSize, doc.GarbageLen())
assert.Equal(t, textSize, doc.GarbageCollect(time.MaxTicket))
runtime.GC()
fmt.Println("-----Garbage collect")
printMemStats(doc.RootObject())
})

t.Run("object test", func(t *testing.T) {
doc := document.New("c1", "d1")
err := doc.Update(func(root *proxy.ObjectProxy) error {
Expand Down
3 changes: 2 additions & 1 deletion pkg/document/json/array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import (

func TestArray(t *testing.T) {
t.Run("marshal test", func(t *testing.T) {
ctx := testhelper.TextChangeContext()
root := testhelper.TestRoot()
ctx := testhelper.TextChangeContext(root)

a := json.NewArray(json.NewRGATreeList(), ctx.IssueTimeTicket())

Expand Down
7 changes: 7 additions & 0 deletions pkg/document/json/element.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ type Container interface {
Descendants(callback func(elem Element, parent Container) bool)
}

// TextType represents Text or RichText.
type TextElement interface {
Element
removedNodesLen() int
cleanupRemovedNodes(ticket *time.Ticket) int
}

// Element represents JSON element.
type Element interface {
// Marshal returns the JSON encoding of this element.
Expand Down
3 changes: 2 additions & 1 deletion pkg/document/json/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (

func TestObject(t *testing.T) {
t.Run("marshal test", func(t *testing.T) {
ctx := testhelper.TextChangeContext()
root := testhelper.TestRoot()
ctx := testhelper.TextChangeContext(root)

obj := json.NewObject(json.NewRHTPriorityQueueMap(), ctx.IssueTimeTicket())

Expand Down
Loading