diff --git a/cloud/metainfo/http.go b/cloud/metainfo/http.go index 8b10dc8e..a724cfdb 100644 --- a/cloud/metainfo/http.go +++ b/cloud/metainfo/http.go @@ -104,7 +104,9 @@ func FromHTTPHeader(ctx context.Context, h HTTPHeaderCarrier) context.Context { // TODO: remove this? return ctx } - return withNode(ctx, m.toNode()) + ctx = withNode(ctx, m.toNode()) + m.recycle() + return ctx } // ToHTTPHeader writes all metainfo into the given HTTP header. diff --git a/cloud/metainfo/kv.go b/cloud/metainfo/kv.go index 07abd66d..b508bf17 100644 --- a/cloud/metainfo/kv.go +++ b/cloud/metainfo/kv.go @@ -14,7 +14,10 @@ package metainfo -import "context" +import ( + "context" + "sync" +) type ctxKeyType struct{} @@ -122,18 +125,31 @@ func (n *node) delPersistent(k string) (r *node) { return n } +var mapViewPool sync.Pool + type mapView struct { - persistent map[string]string - transient map[string]string - stale map[string]string + persistent kvstore + transient kvstore + stale kvstore } func newMapView() *mapView { - return &mapView{ - persistent: make(map[string]string), - transient: make(map[string]string), - stale: make(map[string]string), + mv := mapViewPool.Get() + if mv == nil { + return &mapView{ + persistent: newKVStore(), + transient: newKVStore(), + stale: newKVStore(), + } } + return mv.(*mapView) +} + +func (m *mapView) recycle() { + m.persistent.recycle() + m.transient.recycle() + m.stale.recycle() + mapViewPool.Put(m) } func (m *mapView) size() int { @@ -149,11 +165,11 @@ func (m *mapView) toNode() *node { } func (n *node) mapView() *mapView { - return &mapView{ - persistent: sliceToMap(n.persistent), - transient: sliceToMap(n.transient), - stale: sliceToMap(n.stale), - } + mv := newMapView() + sliceToMap(n.persistent, mv.persistent) + sliceToMap(n.transient, mv.transient) + sliceToMap(n.stale, mv.stale) + return mv } func search(kvs []kv, key string) (idx int, ok bool) { diff --git a/cloud/metainfo/kvstore.go b/cloud/metainfo/kvstore.go new file mode 100644 index 00000000..af8f73ac --- /dev/null +++ b/cloud/metainfo/kvstore.go @@ -0,0 +1,51 @@ +// Copyright 2023 ByteDance Inc. +// +// 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 metainfo + +import "sync" + +type kvstore map[string]string + +var kvpool sync.Pool + +func newKVStore(size ...int) kvstore { + kvs := kvpool.Get() + if kvs == nil { + if len(size) > 0 { + return make(kvstore, size[0]) + } + return make(kvstore) + } + return kvs.(kvstore) +} + +func (store kvstore) recycle() { + /* + for k := range m { + delete(m, k) + } + ==> + LEAQ type.map[string]int(SB), AX + MOVQ AX, (SP) + MOVQ "".m(SB), AX + MOVQ AX, 8(SP) + PCDATA $1, $0 + CALL runtime.mapclear(SB) + */ + for key := range store { + delete(store, key) + } + kvpool.Put(store) +} diff --git a/cloud/metainfo/kvstore_test.go b/cloud/metainfo/kvstore_test.go new file mode 100644 index 00000000..40edb448 --- /dev/null +++ b/cloud/metainfo/kvstore_test.go @@ -0,0 +1,66 @@ +// Copyright 2023 ByteDance Inc. +// +// 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 metainfo + +import ( + "fmt" + "testing" +) + +func TestKVStore(t *testing.T) { + store := newKVStore() + store["a"] = "a" + store["a"] = "b" + if store["a"] != "b" { + t.Fatal() + } + store.recycle() + if store["a"] == "b" { + t.Fatal() + } + store = newKVStore() + if store["a"] == "b" { + t.Fatal() + } +} + +func BenchmarkMap(b *testing.B) { + for keys := 1; keys <= 1000; keys *= 10 { + b.Run(fmt.Sprintf("keys=%d", keys), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + m := make(map[string]string) + for idx := 0; idx < 1000; idx++ { + m[fmt.Sprintf("key-%d", idx)] = string('a' + byte(idx%26)) + } + } + }) + } +} + +func BenchmarkKVStore(b *testing.B) { + for keys := 1; keys <= 1000; keys *= 10 { + b.Run(fmt.Sprintf("keys=%d", keys), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + m := newKVStore() + for idx := 0; idx < 1000; idx++ { + m[fmt.Sprintf("key-%d", idx)] = string('a' + byte(idx%26)) + } + m.recycle() + } + }) + } +} diff --git a/cloud/metainfo/utils.go b/cloud/metainfo/utils.go index 7f2cfc56..42b73613 100644 --- a/cloud/metainfo/utils.go +++ b/cloud/metainfo/utils.go @@ -69,7 +69,9 @@ func SetMetaInfoFromMap(ctx context.Context, m map[string]string) context.Contex return ctx } - return withNode(ctx, mv.toNode()) + ctx = withNode(ctx, mv.toNode()) + mv.recycle() + return ctx } // SaveMetaInfoToMap set key-value pairs from ctx to m while filtering out transient-upstream data. @@ -78,29 +80,28 @@ func SaveMetaInfoToMap(ctx context.Context, m map[string]string) { return } ctx = TransferForward(ctx) - for k, v := range GetAllValues(ctx) { - m[PrefixTransient+k] = v - } - for k, v := range GetAllPersistentValues(ctx) { - m[PrefixPersistent+k] = v + if n := getNode(ctx); n != nil { + for _, kv := range n.persistent { + m[PrefixPersistent+kv.key] = kv.val + } + for _, kv := range n.transient { + m[PrefixTransient+kv.key] = kv.val + } + for _, kv := range n.stale { + m[PrefixTransient+kv.key] = kv.val + } } } -// sliceToMap converts a kv slice to map. If the slice is empty, an empty map will be returned instead of nil. -func sliceToMap(slice []kv) (m map[string]string) { - if size := len(slice); size == 0 { - m = make(map[string]string) - } else { - m = make(map[string]string, size) - } +// sliceToMap converts a kv slice to map. +func sliceToMap(slice []kv, kvs kvstore) { for _, kv := range slice { - m[kv.key] = kv.val + kvs[kv.key] = kv.val } - return } // mapToSlice converts a map to a kv slice. If the map is empty, the return value will be nil. -func mapToSlice(kvs map[string]string) (slice []kv) { +func mapToSlice(kvs kvstore) (slice []kv) { size := len(kvs) if size == 0 { return diff --git a/cloud/metainfo/utils_test.go b/cloud/metainfo/utils_test.go index 053ed268..1982d6c1 100644 --- a/cloud/metainfo/utils_test.go +++ b/cloud/metainfo/utils_test.go @@ -16,6 +16,7 @@ package metainfo_test import ( "context" + "fmt" "testing" "github.com/bytedance/gopkg/cloud/metainfo" @@ -146,3 +147,16 @@ func TestSaveMetaInfoToMap(t *testing.T) { assert(t, m[metainfo.PrefixTransient+"b"] == "b2") assert(t, m[metainfo.PrefixPersistent+"b"] == "b3") } + +func BenchmarkSetMetaInfoFromMap(b *testing.B) { + ctx := metainfo.WithPersistentValue(context.Background(), "key", "val") + m := map[string]string{} + for i := 0; i < 32; i++ { + m[fmt.Sprintf("key-%d", i)] = fmt.Sprintf("val-%d", i) + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = metainfo.SetMetaInfoFromMap(ctx, m) + } +}