Files
tidb/pkg/lightning/backend/external/misc_bench_test.go

427 lines
9.4 KiB
Go

// Copyright 2024 PingCAP, 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 external
import (
"context"
"fmt"
"math"
"math/rand"
"os"
"runtime"
"runtime/pprof"
"slices"
"sync"
"testing"
"time"
"github.com/felixge/fgprof"
"github.com/pingcap/tidb/br/pkg/storage"
"github.com/pingcap/tidb/pkg/kv"
"github.com/pingcap/tidb/pkg/util/intest"
"github.com/stretchr/testify/require"
)
func openTestingStorage(t *testing.T) storage.ExternalStorage {
if *testingStorageURI == "" {
t.Skip("testingStorageURI is not set")
}
s, err := storage.NewFromURL(context.Background(), *testingStorageURI)
require.NoError(t, err)
t.Cleanup(s.Close)
return s
}
type kvSource interface {
next() (key, value []byte, handle kv.Handle)
outputSize() int
}
type ascendingKeyGenerator struct {
keySize int
keyCommonPrefix []byte
count int
curKey []byte
keyOutCh chan []byte
}
func generateAscendingKey(
count int,
keySize int,
keyCommonPrefix []byte,
) chan []byte {
c := &ascendingKeyGenerator{
keySize: keySize,
count: count,
keyCommonPrefix: keyCommonPrefix,
keyOutCh: make(chan []byte, 100),
}
c.curKey = make([]byte, keySize)
copy(c.curKey, keyCommonPrefix)
c.run()
return c.keyOutCh
}
func (c *ascendingKeyGenerator) run() {
keyCommonPrefixSize := len(c.keyCommonPrefix)
incSuffixLen := int(math.Ceil(math.Log2(float64(c.count)) / 8))
if c.keySize-keyCommonPrefixSize < incSuffixLen {
panic(fmt.Sprintf("key size %d is too small, keyCommonPrefixSize: %d, incSuffixLen: %d",
c.keySize, keyCommonPrefixSize, incSuffixLen))
}
go func() {
defer close(c.keyOutCh)
for i := 0; i < c.count; i++ {
// try to use most left bytes to alternate the key
for j := keyCommonPrefixSize + incSuffixLen - 1; j >= keyCommonPrefixSize; j-- {
c.curKey[j]++
if c.curKey[j] != 0 {
break
}
}
c.keyOutCh <- slices.Clone(c.curKey)
}
}()
}
type ascendingKeySource struct {
valueSize int
keys [][]byte
keysIdx int
totalSize int
}
func newAscendingKeySource(
count int,
keySize int,
valueSize int,
keyCommonPrefix []byte,
) *ascendingKeySource {
keyCh := generateAscendingKey(count, keySize, keyCommonPrefix)
s := &ascendingKeySource{
valueSize: valueSize,
keys: make([][]byte, count),
}
for i := 0; i < count; i++ {
key := <-keyCh
s.keys[i] = key
s.totalSize += len(key) + valueSize
}
return s
}
func (s *ascendingKeySource) next() (key, value []byte, handle kv.Handle) {
if s.keysIdx >= len(s.keys) {
return nil, nil, nil
}
key = s.keys[s.keysIdx]
s.keysIdx++
return key, make([]byte, s.valueSize), nil
}
func (s *ascendingKeySource) outputSize() int {
return s.totalSize
}
type ascendingKeyAsyncSource struct {
valueSize int
keyOutCh chan []byte
totalSize int
}
func newAscendingKeyAsyncSource(
count int,
keySize int,
valueSize int,
keyCommonPrefix []byte,
) *ascendingKeyAsyncSource {
s := &ascendingKeyAsyncSource{
valueSize: valueSize,
keyOutCh: generateAscendingKey(count, keySize, keyCommonPrefix),
}
return s
}
func (s *ascendingKeyAsyncSource) next() (key, value []byte, handle kv.Handle) {
key, ok := <-s.keyOutCh
if !ok {
return nil, nil, nil
}
s.totalSize += len(key) + s.valueSize
return key, make([]byte, s.valueSize), nil
}
func (s *ascendingKeyAsyncSource) outputSize() int {
return s.totalSize
}
type randomKeyGenerator struct {
keySize int
keyCommonPrefix []byte
rnd *rand.Rand
count int
curKey []byte
keyOutCh chan []byte
}
func generateRandomKey(
count int,
keySize int,
keyCommonPrefix []byte,
seed int,
) chan []byte {
c := &randomKeyGenerator{
keySize: keySize,
count: count,
keyCommonPrefix: keyCommonPrefix,
rnd: rand.New(rand.NewSource(int64(seed))),
keyOutCh: make(chan []byte, 100),
}
c.curKey = make([]byte, keySize)
copy(c.curKey, keyCommonPrefix)
c.run()
return c.keyOutCh
}
func (c *randomKeyGenerator) run() {
keyCommonPrefixSize := len(c.keyCommonPrefix)
incSuffixLen := int(math.Ceil(math.Log2(float64(c.count)) / 8))
randomLen := c.keySize - keyCommonPrefixSize - incSuffixLen
if randomLen < 0 {
panic(fmt.Sprintf("key size %d is too small, keyCommonPrefixSize: %d, incSuffixLen: %d",
c.keySize, keyCommonPrefixSize, incSuffixLen))
}
go func() {
defer close(c.keyOutCh)
for i := 0; i < c.count; i++ {
c.rnd.Read(c.curKey[keyCommonPrefixSize : keyCommonPrefixSize+randomLen])
for j := len(c.curKey) - 1; j >= keyCommonPrefixSize+randomLen; j-- {
c.curKey[j]++
if c.curKey[j] != 0 {
break
}
}
c.keyOutCh <- slices.Clone(c.curKey)
}
}()
}
type randomKeySource struct {
valueSize int
keys [][]byte
keysIdx int
totalSize int
}
func newRandomKeySource(
count int,
keySize int,
valueSize int,
keyCommonPrefix []byte,
seed int,
) *randomKeySource {
keyCh := generateRandomKey(count, keySize, keyCommonPrefix, seed)
s := &randomKeySource{
valueSize: valueSize,
keys: make([][]byte, count),
}
for i := 0; i < count; i++ {
key := <-keyCh
s.keys[i] = key
s.totalSize += len(key) + valueSize
}
return s
}
func (s *randomKeySource) next() (key, value []byte, handle kv.Handle) {
if s.keysIdx >= len(s.keys) {
return nil, nil, nil
}
key = s.keys[s.keysIdx]
s.keysIdx++
return key, make([]byte, s.valueSize), nil
}
func (s *randomKeySource) outputSize() int {
return s.totalSize
}
func createEvenlyDistributedFiles(
store storage.ExternalStorage,
fileSize, fileCount int,
subDir string,
) (int, kv.Key, kv.Key) {
ctx := context.Background()
cleanOldFiles(ctx, store, "/"+subDir)
value := make([]byte, 100)
kvCnt := 0
var minKey, maxKey kv.Key
for i := 0; i < fileCount; i++ {
builder := NewWriterBuilder().
SetBlockSize(10 * 1024 * 1024).
SetMemorySizeLimit(uint64(float64(fileSize) * 1.1))
writer := builder.Build(
store,
"/"+subDir,
fmt.Sprintf("%d", i),
)
keyIdx := i
totalSize := 0
for totalSize < fileSize {
key := fmt.Sprintf("key_%09d", keyIdx)
if len(minKey) == 0 && len(maxKey) == 0 {
minKey = []byte(key)
maxKey = []byte(key)
} else {
minKey = BytesMin(minKey, []byte(key))
maxKey = BytesMax(maxKey, []byte(key))
}
err := writer.WriteRow(ctx, []byte(key), value, nil)
intest.AssertNoError(err)
keyIdx += fileCount
totalSize += len(key) + len(value)
kvCnt++
}
err := writer.Close(ctx)
intest.AssertNoError(err)
}
return kvCnt, minKey, maxKey
}
func createAscendingFiles(
store storage.ExternalStorage,
fileSize, fileCount int,
subDir string,
) (int, kv.Key, kv.Key) {
ctx := context.Background()
cleanOldFiles(ctx, store, "/"+subDir)
keyIdx := 0
value := make([]byte, 100)
kvCnt := 0
var minKey, maxKey kv.Key
for i := 0; i < fileCount; i++ {
builder := NewWriterBuilder().
SetMemorySizeLimit(uint64(float64(fileSize) * 1.1))
writer := builder.Build(
store,
"/"+subDir,
fmt.Sprintf("%d", i),
)
totalSize := 0
var key string
for totalSize < fileSize {
key = fmt.Sprintf("key_%09d", keyIdx)
if i == 0 && totalSize == 0 {
minKey = []byte(key)
}
err := writer.WriteRow(ctx, []byte(key), value, nil)
intest.AssertNoError(err)
keyIdx++
totalSize += len(key) + len(value)
kvCnt++
}
if i == fileCount-1 {
maxKey = []byte(key)
}
err := writer.Close(ctx)
intest.AssertNoError(err)
}
return kvCnt, minKey, maxKey
}
type profiler struct {
onAndOffCPUProf bool
peakMemProf bool
caseIdx int
onAndOffCPUProfCloser func() error
heapProfDoneCh chan struct{}
heapProfWg *sync.WaitGroup
}
func newProfiler(onAndOffCPUProf, peakMemProf bool) *profiler {
return &profiler{
onAndOffCPUProf: onAndOffCPUProf,
peakMemProf: peakMemProf,
}
}
func (p *profiler) beforeTest() {
p.caseIdx++
if p.onAndOffCPUProf {
fileCPU, err := os.Create(fmt.Sprintf("on-and-off-cpu-%d.prof", p.caseIdx))
intest.AssertNoError(err)
p.onAndOffCPUProfCloser = fgprof.Start(fileCPU, fgprof.FormatPprof)
}
if p.peakMemProf {
fileHeap := fmt.Sprintf("heap-%d.prof", p.caseIdx)
p.heapProfDoneCh, p.heapProfWg = recordHeapForMaxInUse(fileHeap)
}
}
func (p *profiler) afterTest() {
if p.onAndOffCPUProf {
err := p.onAndOffCPUProfCloser()
intest.AssertNoError(err)
}
if p.peakMemProf {
close(p.heapProfDoneCh)
p.heapProfWg.Wait()
}
}
func recordHeapForMaxInUse(filename string) (chan struct{}, *sync.WaitGroup) {
doneCh := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
maxInUse := uint64(0)
go func() {
defer wg.Done()
var m runtime.MemStats
ticker := time.NewTicker(300 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-doneCh:
return
case <-ticker.C:
runtime.ReadMemStats(&m)
if m.HeapInuse <= maxInUse {
continue
}
maxInUse = m.HeapInuse
file, err := os.Create(filename)
intest.AssertNoError(err)
err = pprof.WriteHeapProfile(file)
intest.AssertNoError(err)
err = file.Close()
intest.AssertNoError(err)
}
}
}()
return doneCh, &wg
}