Skip to content

Commit

Permalink
Optimize Minimum VersionVector Computation for Performance (#1153)
Browse files Browse the repository at this point in the history
Significantly improves MinVV calculation efficiency, achieving substantial
performance gains across key metrics. Memory usage reduced by 57.6%, 
push/pull operations accelerated by 52.1%, and overall execution time 
decreased by 20.5% in large-scale testing with 1000 clients.

---------

Co-authored-by: Youngteac Hong <[email protected]>
  • Loading branch information
chacha912 and hackerwins authored Feb 17, 2025
1 parent d49a4bb commit f1c5710
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 36 deletions.
108 changes: 108 additions & 0 deletions pkg/document/time/version_vector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2025 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package time_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/yorkie-team/yorkie/pkg/document/time"
"github.com/yorkie-team/yorkie/test/helper"
)

func TestVersionVector(t *testing.T) {
actor1, _ := time.ActorIDFromHex("000000000000000000000001")
actor2, _ := time.ActorIDFromHex("000000000000000000000002")
actor3, _ := time.ActorIDFromHex("000000000000000000000003")

tests := []struct {
name string
v1 time.VersionVector
v2 time.VersionVector
expect time.VersionVector
}{
{
name: "empty vectors",
v1: time.NewVersionVector(),
v2: time.NewVersionVector(),
expect: time.NewVersionVector(),
},
{
name: "v1 has values, v2 is empty",
v1: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
v2: time.NewVersionVector(),
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 0,
actor2: 0,
}),
},
{
name: "v2 has values, v1 is empty",
v1: time.NewVersionVector(),
v2: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 0,
actor2: 0,
}),
},
{
name: "both vectors have same keys with different values",
v1: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
v2: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 3,
actor2: 4,
}),
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 3,
actor2: 3,
}),
},
{
name: "vectors have different keys",
v1: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
v2: helper.VersionVectorOf(map[*time.ActorID]int64{
actor2: 4,
actor3: 6,
}),
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 0,
actor2: 3,
actor3: 0,
}),
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.v1.Min(tc.v2)
assert.Equal(t, tc.expect, result)
})
}
}
140 changes: 140 additions & 0 deletions server/backend/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"github.com/stretchr/testify/assert"

"github.com/yorkie-team/yorkie/api/types"
"github.com/yorkie-team/yorkie/pkg/document/time"
"github.com/yorkie-team/yorkie/server/backend/database"
"github.com/yorkie-team/yorkie/test/helper"
)

func TestID(t *testing.T) {
Expand All @@ -39,3 +42,140 @@ func TestID(t *testing.T) {
assert.Equal(t, bytes, bytesID)
})
}

func TestFindMinVersionVector(t *testing.T) {
actor1, _ := time.ActorIDFromHex("000000000000000000000001")
actor2, _ := time.ActorIDFromHex("000000000000000000000002")
actor3, _ := time.ActorIDFromHex("000000000000000000000003")

tests := []struct {
name string
vvInfos []database.VersionVectorInfo
excludeClientID types.ID
expect time.VersionVector
}{
{
name: "empty version vector infos",
vvInfos: []database.VersionVectorInfo{},
excludeClientID: "",
expect: nil,
},
{
name: "single version vector info",
vvInfos: []database.VersionVectorInfo{
{
ClientID: "client1",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
},
},
excludeClientID: "",
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
},
{
name: "exclude client",
vvInfos: []database.VersionVectorInfo{
{
ClientID: "client1",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
},
{
ClientID: "client2",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 3,
actor2: 4,
}),
},
},
excludeClientID: "client1",
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 3,
actor2: 4,
}),
},
{
name: "exclude all clients",
vvInfos: []database.VersionVectorInfo{
{
ClientID: "client1",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
}),
},
},
excludeClientID: "client1",
expect: nil,
},
{
name: "multiple clients with different actors",
vvInfos: []database.VersionVectorInfo{
{
ClientID: "client1",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
},
{
ClientID: "client2",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor2: 2,
actor3: 4,
}),
},
},
excludeClientID: "",
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 0,
actor2: 2,
actor3: 0,
}),
},
{
name: "all clients have same actors",
vvInfos: []database.VersionVectorInfo{
{
ClientID: "client1",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 5,
actor2: 3,
}),
},
{
ClientID: "client2",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 3,
actor2: 4,
}),
},
{
ClientID: "client3",
VersionVector: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 4,
actor2: 2,
}),
},
},
excludeClientID: "",
expect: helper.VersionVectorOf(map[*time.ActorID]int64{
actor1: 3,
actor2: 2,
}),
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := database.FindMinVersionVector(tc.vvInfos, tc.excludeClientID)
assert.Equal(t, tc.expect, result)
})
}
}
25 changes: 8 additions & 17 deletions server/backend/database/memory/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -1360,30 +1360,21 @@ func (d *DB) UpdateAndFindMinSyncedVersionVector(
return nil, fmt.Errorf("find all version vectors: %w", err)
}

// 02. Compute min version vector.
var minVersionVector time.VersionVector

// 02-1. Compute min version vector of other clients and collect attachedActorIDs.
var versionVectorInfos []database.VersionVectorInfo
for raw := iterator.Next(); raw != nil; raw = iterator.Next() {
vvi := raw.(*database.VersionVectorInfo)
if clientInfo.ID == vvi.ClientID {
continue
}

if minVersionVector == nil {
minVersionVector = vvi.VersionVector
continue
}

minVersionVector = minVersionVector.Min(vvi.VersionVector)
versionVectorInfos = append(versionVectorInfos, *vvi)
}

// 02-1. Compute min version vector of other clients.
minVersionVector := database.FindMinVersionVector(versionVectorInfos, clientInfo.ID)
// 02-2. Compute min version vector with current client's version vector.
if minVersionVector == nil {
minVersionVector = versionVector
} else {
minVersionVector = minVersionVector.Min(versionVector)
}

// 02-2. Compute min version vector with current client's version vector.
minVersionVector = minVersionVector.Min(versionVector)

// 03. Update current client's version vector. If the client is detached, remove it.
// This is only for the current client and does not affect the version vector of other clients.
if err = d.UpdateVersionVector(ctx, clientInfo, docRefKey, versionVector); err != nil {
Expand Down
24 changes: 5 additions & 19 deletions server/backend/database/mongo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1258,29 +1258,15 @@ func (c *Client) UpdateAndFindMinSyncedVersionVector(
return nil, fmt.Errorf("decode version vectors: %w", err)
}

// 02. Compute min version vector.
var minVersionVector time.VersionVector

// 02-1. Compute min version vector of other clients and collect attachedActorIDs.
for _, vvi := range versionVectorInfos {
if clientInfo.ID == vvi.ClientID {
continue
}

if minVersionVector == nil {
minVersionVector = vvi.VersionVector
continue
}

minVersionVector = minVersionVector.Min(vvi.VersionVector)
}
// 02-1. Compute min version vector of other clients.
minVersionVector := database.FindMinVersionVector(versionVectorInfos, clientInfo.ID)
// 02-2. Compute min version vector with current client's version vector.
if minVersionVector == nil {
minVersionVector = versionVector
} else {
minVersionVector = minVersionVector.Min(versionVector)
}

// 02-2. Compute min version vector with current client's version vector.
minVersionVector = minVersionVector.Min(versionVector)

// 03. Update current client's version vector. If the client is detached, remove it.
// This is only for the current client and does not affect the version vector of other clients.
if err = c.UpdateVersionVector(ctx, clientInfo, docRefKey, versionVector); err != nil {
Expand Down
33 changes: 33 additions & 0 deletions server/backend/database/version_vector.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,36 @@ type VersionVectorInfo struct {
ClientID types.ID `bson:"client_id"`
VersionVector time.VersionVector `bson:"version_vector"`
}

// FindMinVersionVector finds the minimum version vector from the given version vector infos.
// It excludes the version vector of the given client ID if specified.
func FindMinVersionVector(vvInfos []VersionVectorInfo, excludeClientID types.ID) time.VersionVector {
var minVV time.VersionVector

for _, vvi := range vvInfos {
if vvi.ClientID == excludeClientID {
continue
}

if minVV == nil {
minVV = vvi.VersionVector.DeepCopy()
continue
}

for actorID, lamport := range vvi.VersionVector {
if currentLamport, exists := minVV[actorID]; !exists {
minVV[actorID] = 0
} else if lamport < currentLamport {
minVV[actorID] = lamport
}
}

for actorID := range minVV {
if _, exists := vvi.VersionVector[actorID]; !exists {
minVV[actorID] = 0
}
}
}

return minVV
}
9 changes: 9 additions & 0 deletions test/helper/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ func MaxVersionVector(actors ...*time.ActorID) time.VersionVector {
return vector
}

// VersionVectorOf creates a new version vector from the given actors.
func VersionVectorOf(actors map[*time.ActorID]int64) time.VersionVector {
vector := time.NewVersionVector()
for actor, lamport := range actors {
vector.Set(actor, lamport)
}
return vector
}

// TokensEqualBetween is a helper function that checks the tokens between the given
// indexes.
func TokensEqualBetween(t assert.TestingT, tree *index.Tree[*crdt.TreeNode], from, to int, expected []string) bool {
Expand Down

0 comments on commit f1c5710

Please sign in to comment.