diff --git a/client/cmd/ycsb/ycsb.go b/client/cmd/ycsb/ycsb.go index c5ee0b6d..35e0399c 100644 --- a/client/cmd/ycsb/ycsb.go +++ b/client/cmd/ycsb/ycsb.go @@ -10,6 +10,7 @@ import ( "io" "math/rand" "slices" + "strconv" "strings" "sync" "time" @@ -135,7 +136,9 @@ func hashKey(keyNum int) string { h *= prime64 n >>= 8 } - return fmt.Sprintf("user%x", h) + // "user" plus at most 16 hexadecimal digits fits in this stack buffer. + key := [20]byte{'u', 's', 'e', 'r'} + return string(strconv.AppendUint(key[:4], h, 16)) } // casKey names the CAS-guarded twin of key number keyNum. CAS keys live in diff --git a/client/cmd/ycsb/ycsb_test.go b/client/cmd/ycsb/ycsb_test.go index 77e98385..fd284207 100644 --- a/client/cmd/ycsb/ycsb_test.go +++ b/client/cmd/ycsb/ycsb_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "errors" + "fmt" "math/rand" "net" "strings" @@ -23,6 +24,43 @@ import ( "google.golang.org/grpc/status" ) +func TestHashKeyMatchesFormatter(t *testing.T) { + maxInt := int(^uint(0) >> 1) + tests := []struct { + name string + keyNum int + }{ + {name: "zero", keyNum: 0}, + {name: "positive", keyNum: 42}, + {name: "negative", keyNum: -42}, + {name: "max int", keyNum: maxInt}, + {name: "min int", keyNum: -maxInt - 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, want := hashKey(tt.keyNum), formatterHashKey(tt.keyNum); got != want { + t.Errorf("hashKey(%d) = %q, want %q", tt.keyNum, got, want) + } + }) + } +} + +func formatterHashKey(keyNum int) string { + const ( + offset64 = 14695981039346656037 + prime64 = 1099511628211 + ) + h := uint64(offset64) + n := uint64(keyNum) + for range 8 { + h ^= n & 0xff + h *= prime64 + n >>= 8 + } + return fmt.Sprintf("user%x", h) +} + const ( ycsbReadWorkers = 4 ycsbReadKeys = 1