snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
112
balancer/autosharding/autosharding.go
Normal file
112
balancer/autosharding/autosharding.go
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 gRPC authors.
|
||||
*
|
||||
* 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 autosharding implements the autosharding load balancing policy.
|
||||
package autosharding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/endpointsharding"
|
||||
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
// Name is the name of the autosharding balancer.
|
||||
const Name = "autosharding_experimental"
|
||||
|
||||
func init() {
|
||||
balancer.Register(bb{})
|
||||
}
|
||||
|
||||
// lbConfig is the balancer config for the autosharding balancer.
|
||||
type lbConfig struct {
|
||||
serviceconfig.LoadBalancingConfig `json:"-"`
|
||||
|
||||
ChannelFactoryKey string `json:"channelFactoryKey,omitempty"`
|
||||
AutoShardingTarget string `json:"autoshardingTarget,omitempty"`
|
||||
KeyHeaderName string `json:"keyHeaderName,omitempty"`
|
||||
EnableFallback bool `json:"enableFallback,omitempty"`
|
||||
InitialAssignmentTimeout iserviceconfig.Duration `json:"initialAssignmentTimeout,omitempty"`
|
||||
}
|
||||
|
||||
type bb struct{}
|
||||
|
||||
func (bb) Name() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (bb) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
lbConfig := &lbConfig{InitialAssignmentTimeout: iserviceconfig.Duration(60 * time.Second)}
|
||||
if err := json.Unmarshal(s, lbConfig); err != nil {
|
||||
return nil, fmt.Errorf("autosharding: unable to unmarshal LBConfig: %v", err)
|
||||
}
|
||||
if lbConfig.ChannelFactoryKey == "" {
|
||||
return nil, fmt.Errorf("autosharding: channelFactoryKey field is required")
|
||||
}
|
||||
if lbConfig.AutoShardingTarget == "" {
|
||||
return nil, fmt.Errorf("autosharding: autoshardingTarget field is required")
|
||||
}
|
||||
if lbConfig.KeyHeaderName == "" {
|
||||
return nil, fmt.Errorf("autosharding: keyHeaderName field is required")
|
||||
}
|
||||
return lbConfig, nil
|
||||
}
|
||||
|
||||
func (bb) Build(balancer.ClientConn, balancer.BuildOptions) balancer.Balancer {
|
||||
return &autoshardingBalancer{}
|
||||
}
|
||||
|
||||
// slice represents a key range and its assigned endpoints.
|
||||
//
|
||||
//lint:ignore U1000 Struct fields planned for future implementation
|
||||
type slice struct {
|
||||
startKey []byte // Inclusive start key of the key-range
|
||||
endKey []byte // Exclusive, nil for sentinel/infinity
|
||||
endpoints []int // Indices into assignment.endpointNames
|
||||
}
|
||||
|
||||
// assignment represents a complete snapshot of sharding assignments, and is
|
||||
// expected to cover the entire key range.
|
||||
type assignment struct {
|
||||
slices []slice // Sorted by startKey
|
||||
endpointNames []string // Complete list of endpoint names
|
||||
generation int64
|
||||
}
|
||||
|
||||
// endpointState represents the state associated with an endpoint in the LB policy.
|
||||
//
|
||||
//lint:ignore U1000 Struct fields planned for future implementation
|
||||
type endpointState struct {
|
||||
index int // Index of the endpoint within the NR update
|
||||
endpoint resolver.Endpoint // The actual endpoint returned by the NR
|
||||
childState endpointsharding.ChildState // State as reported by the child policy
|
||||
}
|
||||
|
||||
// endpointMap maps from endpoint hostname to endpoint state.
|
||||
type endpointMap struct {
|
||||
m map[string]*endpointState
|
||||
}
|
||||
|
||||
type autoshardingBalancer struct {
|
||||
balancer.Balancer
|
||||
}
|
||||
144
balancer/autosharding/autosharding_test.go
Normal file
144
balancer/autosharding/autosharding_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 gRPC authors.
|
||||
*
|
||||
* 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 autosharding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestParseConfig_Success(t *testing.T) {
|
||||
parser := bb{}
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantCfg serviceconfig.LoadBalancingConfig
|
||||
}{
|
||||
{
|
||||
name: "all-fields",
|
||||
input: `{
|
||||
"channelFactoryKey": "factory-key",
|
||||
"autoshardingTarget": "target",
|
||||
"keyHeaderName": "header",
|
||||
"enableFallback": true,
|
||||
"initialAssignmentTimeout": "30s"
|
||||
}`,
|
||||
wantCfg: &lbConfig{
|
||||
ChannelFactoryKey: "factory-key",
|
||||
AutoShardingTarget: "target",
|
||||
KeyHeaderName: "header",
|
||||
EnableFallback: true,
|
||||
InitialAssignmentTimeout: iserviceconfig.Duration(30 * time.Second),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default-timeout",
|
||||
input: `{
|
||||
"channelFactoryKey": "factory-key",
|
||||
"autoshardingTarget": "target",
|
||||
"keyHeaderName": "header"
|
||||
}`,
|
||||
wantCfg: &lbConfig{
|
||||
ChannelFactoryKey: "factory-key",
|
||||
AutoShardingTarget: "target",
|
||||
KeyHeaderName: "header",
|
||||
InitialAssignmentTimeout: iserviceconfig.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
gotCfg, err := parser.ParseConfig(json.RawMessage(test.input))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConfig() error = %v, want nil", err)
|
||||
}
|
||||
if diff := cmp.Diff(test.wantCfg, gotCfg); diff != "" {
|
||||
t.Errorf("ParseConfig() config diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestParseConfig_Failure(t *testing.T) {
|
||||
parser := bb{}
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "invalid-json",
|
||||
input: "{{invalidjson{{",
|
||||
},
|
||||
{
|
||||
name: "invalid-duration",
|
||||
input: `{
|
||||
"channelFactoryKey": "factory-key",
|
||||
"autoshardingTarget": "target",
|
||||
"keyHeaderName": "header",
|
||||
"initialAssignmentTimeout": "invalid"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "missing-channel-factory-key",
|
||||
input: `{
|
||||
"autoshardingTarget": "target",
|
||||
"keyHeaderName": "header"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "missing-autosharding-target",
|
||||
input: `{
|
||||
"channelFactoryKey": "factory-key",
|
||||
"keyHeaderName": "header"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "missing-key-header-name",
|
||||
input: `{
|
||||
"channelFactoryKey": "factory-key",
|
||||
"autoshardingTarget": "target"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "empty-config",
|
||||
input: `{}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if _, err := parser.ParseConfig(json.RawMessage(test.input)); err == nil {
|
||||
t.Fatalf("ParseConfig() succeeded, want error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
120
balancer/autosharding/slice_map.go
Normal file
120
balancer/autosharding/slice_map.go
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 gRPC authors.
|
||||
*
|
||||
* 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 autosharding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// sliceMapEntry represents an entry for a key-range in the sliceMap.
|
||||
type sliceMapEntry struct {
|
||||
startKey []byte // Inclusive start key of the key-range
|
||||
endpoints []int // Indices into list[PickerEndpoint] in the Picker
|
||||
}
|
||||
|
||||
// sliceMap is an immutable data structure used by the Picker to map request
|
||||
// keys to their assigned key-range. Because it is immutable, it allows the
|
||||
// Picker to access it without any explicit synchronization with the LB policy.
|
||||
//
|
||||
// The sliceMap is meant to be used by the Picker in conjunction with a list of
|
||||
// pickerEndpoints such that the list can be swapped out, as long as the number
|
||||
// and order of endpoints don't change.
|
||||
type sliceMap struct {
|
||||
slices []sliceMapEntry // Sorted by startKey
|
||||
fallbackPool []int // Indices into list[PickerEndpoint] for all endpoints
|
||||
generation int64 // Snapshot generation number
|
||||
}
|
||||
|
||||
// lookup returns the index of the slice covering the given key.
|
||||
//
|
||||
// Because assignments are pre-validated to have no gaps and cover the full key
|
||||
// range, and since slices is sorted by startKey, lookup boils down to a binary
|
||||
// search, looking for the insertion point.
|
||||
//
|
||||
// If the key matches the startKey of a slice, that slice index is returned.
|
||||
// Otherwise, the index of the slice immediately preceding the insertion point
|
||||
// is returned (i.e., the slice covering the range [startKey, nextStartKey)).
|
||||
//
|
||||
// Returns -1 when the sliceMap is empty, which is the case before the first
|
||||
// assignment is received.
|
||||
func (sm *sliceMap) lookup(key []byte) int {
|
||||
if len(sm.slices) == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
idx, found := slices.BinarySearchFunc(sm.slices, key, func(e sliceMapEntry, k []byte) int {
|
||||
return bytes.Compare(e.startKey, k)
|
||||
})
|
||||
|
||||
if found {
|
||||
return idx
|
||||
}
|
||||
|
||||
// Key falls in range [slices[idx - 1].startKey, slices[idx].startKey).
|
||||
return idx - 1
|
||||
}
|
||||
|
||||
// buildSliceMap is used to generate a new sliceMap from the EndpointMap and
|
||||
// Assignment when either of them change.
|
||||
func buildSliceMap(endpointMap *endpointMap, assignment *assignment) *sliceMap {
|
||||
sm := &sliceMap{}
|
||||
|
||||
// Populate fallbackPool with values [0, 1, 2, ... N-1] where N is the
|
||||
// number of endpoints returned by the name resolver. The only reason to
|
||||
// have this slice is to allow the Picker to share code that picks a random
|
||||
// endpoint from a slice of indices, either from the fallbackPool or from a
|
||||
// sliceMapEntry.
|
||||
sm.fallbackPool = make([]int, len(endpointMap.m))
|
||||
for i := range sm.fallbackPool {
|
||||
sm.fallbackPool[i] = i
|
||||
}
|
||||
|
||||
// If no assignment has been received yet (startup case), return early with
|
||||
// empty slices.
|
||||
if assignment == nil {
|
||||
return sm
|
||||
}
|
||||
|
||||
sm.generation = assignment.generation
|
||||
|
||||
// Build sliceMapEntry for each Slice in the assignment.
|
||||
sm.slices = make([]sliceMapEntry, 0, len(assignment.slices))
|
||||
for _, s := range assignment.slices {
|
||||
entry := sliceMapEntry{
|
||||
startKey: s.startKey,
|
||||
endpoints: make([]int, 0, len(s.endpoints)),
|
||||
}
|
||||
|
||||
// Populate the endpoints for this slice by looking up the endpoint
|
||||
// names in the EndpointMap. If an endpoint name is not found in the
|
||||
// EndpointMap, it is skipped. This ensures that the sliceMap only
|
||||
// contains valid endpoints that are currently available.
|
||||
for _, idx := range s.endpoints {
|
||||
hostname := assignment.endpointNames[idx]
|
||||
if es, ok := endpointMap.m[hostname]; ok {
|
||||
entry.endpoints = append(entry.endpoints, es.index)
|
||||
}
|
||||
}
|
||||
|
||||
sm.slices = append(sm.slices, entry)
|
||||
}
|
||||
|
||||
return sm
|
||||
}
|
||||
216
balancer/autosharding/slice_map_test.go
Normal file
216
balancer/autosharding/slice_map_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 gRPC authors.
|
||||
*
|
||||
* 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 autosharding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func (s) TestSliceMap_Lookup(t *testing.T) {
|
||||
sm := &sliceMap{
|
||||
slices: []sliceMapEntry{
|
||||
{startKey: []byte("a"), endpoints: []int{1}},
|
||||
{startKey: []byte("d"), endpoints: []int{2}},
|
||||
{startKey: []byte("f"), endpoints: []int{3}},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key []byte
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "exact-match-first",
|
||||
key: []byte("a"),
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "exact-match-middle",
|
||||
key: []byte("d"),
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "exact-match-last",
|
||||
key: []byte("f"),
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "between-first-and-middle",
|
||||
key: []byte("c"),
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "between-middle-and-last",
|
||||
key: []byte("e"),
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "after-last",
|
||||
key: []byte("g"),
|
||||
want: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := sm.lookup(tc.key); got != tc.want {
|
||||
t.Errorf("lookup(%q) = %d, want %d", tc.key, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestSliceMap_Lookup_Empty(t *testing.T) {
|
||||
sm := &sliceMap{}
|
||||
if got := sm.lookup([]byte("a")); got != -1 {
|
||||
t.Errorf("lookup() on empty map = %d, want -1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestBuildSliceMap(t *testing.T) {
|
||||
// Setup endpointMap with unsorted order in map to verify deterministic
|
||||
// sorting of fallbackPool.
|
||||
epMap := &endpointMap{
|
||||
m: map[string]*endpointState{
|
||||
"hostC": {index: 2},
|
||||
"hostA": {index: 0},
|
||||
"hostB": {index: 1},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
assignment *assignment
|
||||
want *sliceMap
|
||||
}{
|
||||
{
|
||||
name: "nil-assignment-startup",
|
||||
assignment: nil,
|
||||
want: &sliceMap{fallbackPool: []int{0, 1, 2}},
|
||||
},
|
||||
{
|
||||
name: "valid-assignment",
|
||||
assignment: &assignment{
|
||||
endpointNames: []string{"hostA", "hostB", "hostC"},
|
||||
slices: []slice{
|
||||
{startKey: []byte("a"), endpoints: []int{0, 1}}, // hostA, hostB -> indices 0, 1
|
||||
{startKey: []byte("m"), endpoints: []int{1, 2}}, // hostB, hostC -> indices 1, 2
|
||||
},
|
||||
generation: 42,
|
||||
},
|
||||
want: &sliceMap{
|
||||
slices: []sliceMapEntry{
|
||||
{startKey: []byte("a"), endpoints: []int{0, 1}},
|
||||
{startKey: []byte("m"), endpoints: []int{1, 2}},
|
||||
},
|
||||
fallbackPool: []int{0, 1, 2},
|
||||
generation: 42,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assignment-with-unknown-host",
|
||||
assignment: &assignment{
|
||||
endpointNames: []string{"hostA", "hostUnknown", "hostC"},
|
||||
slices: []slice{
|
||||
{startKey: []byte("a"), endpoints: []int{0, 1}}, // hostUnknown is skipped
|
||||
},
|
||||
generation: 43,
|
||||
},
|
||||
want: &sliceMap{
|
||||
slices: []sliceMapEntry{
|
||||
{startKey: []byte("a"), endpoints: []int{0}}, // Only hostA (index 0)
|
||||
},
|
||||
fallbackPool: []int{0, 1, 2},
|
||||
generation: 43,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := buildSliceMap(epMap, tc.assignment)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("buildSliceMap() diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (e sliceMapEntry) Equal(b sliceMapEntry) bool {
|
||||
return bytes.Equal(e.startKey, b.startKey) && slices.Equal(e.endpoints, b.endpoints)
|
||||
}
|
||||
|
||||
func (sm *sliceMap) Equal(b *sliceMap) bool {
|
||||
if sm == nil || b == nil {
|
||||
return sm == b
|
||||
}
|
||||
fallbackPoolEqual := slices.Equal(sm.fallbackPool, b.fallbackPool)
|
||||
slicesEqual := slices.EqualFunc(sm.slices, b.slices, func(x, y sliceMapEntry) bool { return x.Equal(y) })
|
||||
return sm.generation == b.generation && fallbackPoolEqual && slicesEqual
|
||||
}
|
||||
|
||||
func BenchmarkSliceMap_Lookup(b *testing.B) {
|
||||
for _, numSlices := range []int{1, 10, 100, 1000, 10000} {
|
||||
for _, keySize := range []int{16, 32, 64, 128, 256, 512} {
|
||||
b.Run(fmt.Sprintf("slices_%d_keySize_%d", numSlices, keySize), func(b *testing.B) {
|
||||
sm := &sliceMap{
|
||||
slices: make([]sliceMapEntry, numSlices),
|
||||
}
|
||||
for i := 0; i < numSlices; i++ {
|
||||
// Generate lexicographically sorted keys to serve as slice
|
||||
// boundaries. We multiply by 1000 to create ranges (gaps) between
|
||||
// successive slices (e.g., Slice 0 covers [0, 1000), Slice 1 covers
|
||||
// [1000, 2000)). This allows us to test lookups that fall
|
||||
// *mid-slice*, rather than just exact matches.
|
||||
//
|
||||
// We use zero-padding on the left via "%0*d" (where * is the keySize
|
||||
// width). This fills the key to its full length (e.g., 512 bytes)
|
||||
// with leading zeros. Scanning long identical prefixes forces
|
||||
// bytes.Compare to traverse the full length, simulating a
|
||||
// conservative, worst-case latency scenario.
|
||||
key := fmt.Appendf(nil, "%0*d", keySize, i*1000)
|
||||
sm.slices[i] = sliceMapEntry{startKey: key, endpoints: []int{i}}
|
||||
}
|
||||
|
||||
// Pre-generate a pool of lookup keys. This helps avoid
|
||||
// measuring string formatting overhead inside the timer loop.
|
||||
lookupKeys := make([][]byte, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
// Distribute the 10,000 lookup keys proportionally across the entire
|
||||
// synthetic keyspace [0, numSlices * 1000). This ensures a good mix
|
||||
// of boundary hits and interior hits.
|
||||
val := i * (numSlices * 1000) / 10000
|
||||
lookupKeys[i] = fmt.Appendf(nil, "%0*d", keySize, val)
|
||||
}
|
||||
|
||||
var i int
|
||||
for b.Loop() {
|
||||
sm.lookup(lookupKeys[i%10000])
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user