From 379ee54f5c72216d429addb1ce1915101c974ba5 Mon Sep 17 00:00:00 2001 From: m4ushold <160091510+m4ushold@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:02:17 +0900 Subject: [PATCH] Improve performance for creating crdt.TreeNode (#939) The time complexity of creating crdt.TreeNode is O(N^2), potentially causing performance bottlenecks. It's optimized to O(n). While this may not be a significant issue currently, there is a risk that as the number of tree nodes in the protobuf increases, operations will scale quadratically, potentially causing performance bottlenecks. --------- Co-authored-by: JiHwan Yim Co-authored-by: Youngteac Hong --- api/converter/from_pb.go | 11 ++--- test/bench/tree_editing_bench_test.go | 68 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 test/bench/tree_editing_bench_test.go diff --git a/api/converter/from_pb.go b/api/converter/from_pb.go index 7bb740049..450403a42 100644 --- a/api/converter/from_pb.go +++ b/api/converter/from_pb.go @@ -583,18 +583,15 @@ func FromTreeNodes(pbNodes []*api.TreeNode) (*crdt.TreeNode, error) { } root := nodes[len(nodes)-1] + depthTable := make(map[int32]*crdt.TreeNode) + depthTable[pbNodes[len(nodes)-1].Depth] = nodes[len(nodes)-1] for i := len(nodes) - 2; i >= 0; i-- { - var parent *crdt.TreeNode - for j := i + 1; j < len(nodes); j++ { - if pbNodes[i].Depth-1 == pbNodes[j].Depth { - parent = nodes[j] - break - } - } + var parent *crdt.TreeNode = depthTable[pbNodes[i].Depth-1] if err := parent.Prepend(nodes[i]); err != nil { return nil, err } + depthTable[pbNodes[i].Depth] = nodes[i] } root.Index.UpdateDescendantsSize() diff --git a/test/bench/tree_editing_bench_test.go b/test/bench/tree_editing_bench_test.go new file mode 100644 index 000000000..4b41d7bbc --- /dev/null +++ b/test/bench/tree_editing_bench_test.go @@ -0,0 +1,68 @@ +//go:build bench + +/* + * Copyright 2024 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 bench + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/yorkie-team/yorkie/api/converter" + "github.com/yorkie-team/yorkie/pkg/document/crdt" + "github.com/yorkie-team/yorkie/pkg/document/json" + "github.com/yorkie-team/yorkie/test/helper" +) +func BenchmarkTree(b *testing.B) { + verticesCounts := []int{10000, 20000, 30000} + + for _, cnt := range verticesCounts { + root := buildTree(cnt) + b.ResetTimer() + + b.Run(fmt.Sprintf("%d vertices to protobuf", cnt), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = converter.ToTreeNodes(root) + } + }) + + b.Run(fmt.Sprintf("%d vertices from protobuf", cnt), func(b *testing.B) { + for i := 0; i < b.N; i++ { + pbNodes := converter.ToTreeNodes(root) + _, err := converter.FromTreeNodes(pbNodes) + assert.NoError(b, err) + } + }) + } +} + +// buildTree creates a tree with the given number of vertices. +func buildTree(vertexCnt int) *crdt.TreeNode { + children := make([]json.TreeNode, vertexCnt) + for i := 0; i < vertexCnt; i++ { + children[i] = json.TreeNode{ + Type: "p", Children: []json.TreeNode{{Type: "text", Value: "a"}}, + } + } + + return helper.BuildTreeNode(&json.TreeNode{ + Type: "r", + Children: children, + }) +}