Files
tidb/planner/core/plan_cache_lru_test.go

398 lines
13 KiB
Go

// Package core Copyright 2022 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 core
import (
"math/rand"
"strconv"
"testing"
"time"
"github.com/pingcap/tidb/parser/mysql"
"github.com/pingcap/tidb/types"
"github.com/pingcap/tidb/util/hack"
"github.com/pingcap/tidb/util/kvcache"
"github.com/stretchr/testify/require"
)
func randomPlanCacheKey() *planCacheKey {
random := rand.New(rand.NewSource(time.Now().UnixNano()))
return &planCacheKey{
database: strconv.FormatInt(int64(random.Int()), 10),
schemaVersion: time.Now().UnixNano(),
}
}
func randomPlanCacheValue(types []*types.FieldType) *PlanCacheValue {
plans := []Plan{&Insert{}, &Update{}, &Delete{}, &PhysicalTableScan{}, &PhysicalTableDual{}, &PhysicalTableReader{},
&PhysicalTableScan{}, &PhysicalIndexJoin{}, &PhysicalIndexHashJoin{}, &PhysicalIndexMergeJoin{}, &PhysicalIndexMergeReader{},
&PhysicalIndexLookUpReader{}, &PhysicalApply{}, &PhysicalApply{}, &PhysicalLimit{}}
random := rand.New(rand.NewSource(time.Now().UnixNano()))
return &PlanCacheValue{
Plan: plans[random.Int()%len(plans)],
matchOpts: planCacheMatchOpts{paramTypes: types},
}
}
func TestLRUPCPut(t *testing.T) {
// test initialize
mockCtx := MockContext()
mockCtx.GetSessionVars().EnablePlanCacheForParamLimit = true
lruA := NewLRUPlanCache(0, 0, 0, mockCtx)
require.Equal(t, lruA.capacity, uint(100))
maxMemDroppedKv := make(map[kvcache.Key]kvcache.Value)
lru := NewLRUPlanCache(3, 0, 0, mockCtx)
lru.onEvict = func(key kvcache.Key, value kvcache.Value) {
maxMemDroppedKv[key] = value
}
require.Equal(t, uint(3), lru.capacity)
keys := make([]*planCacheKey, 5)
vals := make([]*PlanCacheValue, 5)
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeLong)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeInt24)},
}
limitParams := [][]uint64{
{1}, {2}, {3}, {4}, {5},
}
// one key corresponding to multi values
for i := 0; i < 5; i++ {
keys[i] = &planCacheKey{database: strconv.FormatInt(int64(1), 10)}
vals[i] = &PlanCacheValue{
matchOpts: planCacheMatchOpts{
paramTypes: pTypes[i],
limitOffsetAndCount: limitParams[i],
},
}
lru.Put(keys[i], vals[i], pTypes[i], limitParams[i])
}
require.Equal(t, lru.size, lru.capacity)
require.Equal(t, uint(3), lru.size)
// test for non-existent elements
require.Len(t, maxMemDroppedKv, 2)
for i := 0; i < 2; i++ {
bucket, exist := lru.buckets[string(hack.String(keys[i].Hash()))]
require.True(t, exist)
for element := range bucket {
require.NotEqual(t, vals[i], element.Value.(*planCacheEntry).PlanValue)
}
require.Equal(t, vals[i], maxMemDroppedKv[keys[i]])
}
// test for existent elements
root := lru.lruList.Front()
require.NotNil(t, root)
for i := 4; i >= 2; i-- {
entry, ok := root.Value.(*planCacheEntry)
require.True(t, ok)
require.NotNil(t, entry)
// test key
key := entry.PlanKey
require.NotNil(t, key)
require.Equal(t, keys[i], key)
bucket, exist := lru.buckets[string(hack.String(keys[i].Hash()))]
require.True(t, exist)
matchOpts := &planCacheMatchOpts{
paramTypes: pTypes[i],
limitOffsetAndCount: limitParams[i],
}
element, exist := lru.pickFromBucket(bucket, matchOpts)
require.NotNil(t, element)
require.True(t, exist)
require.Equal(t, root, element)
// test value
value, ok := entry.PlanValue.(*PlanCacheValue)
require.True(t, ok)
require.Equal(t, vals[i], value)
root = root.Next()
}
// test for end of double-linked list
require.Nil(t, root)
}
func TestLRUPCGet(t *testing.T) {
mockCtx := MockContext()
mockCtx.GetSessionVars().EnablePlanCacheForParamLimit = true
lru := NewLRUPlanCache(3, 0, 0, mockCtx)
keys := make([]*planCacheKey, 5)
vals := make([]*PlanCacheValue, 5)
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeLong)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeInt24)},
}
limitParams := [][]uint64{
{1}, {2}, {3}, {4}, {5},
}
// 5 bucket
for i := 0; i < 5; i++ {
keys[i] = &planCacheKey{database: strconv.FormatInt(int64(i%4), 10)}
vals[i] = &PlanCacheValue{
matchOpts: planCacheMatchOpts{
paramTypes: pTypes[i],
limitOffsetAndCount: limitParams[i],
},
}
lru.Put(keys[i], vals[i], pTypes[i], limitParams[i])
}
// test for non-existent elements
for i := 0; i < 2; i++ {
value, exists := lru.Get(keys[i], pTypes[i], limitParams[i])
require.False(t, exists)
require.Nil(t, value)
}
for i := 2; i < 5; i++ {
value, exists := lru.Get(keys[i], pTypes[i], limitParams[i])
require.True(t, exists)
require.NotNil(t, value)
require.Equal(t, vals[i], value)
require.Equal(t, uint(3), lru.size)
require.Equal(t, uint(3), lru.capacity)
root := lru.lruList.Front()
require.NotNil(t, root)
entry, ok := root.Value.(*planCacheEntry)
require.True(t, ok)
require.Equal(t, keys[i], entry.PlanKey)
value, ok = entry.PlanValue.(*PlanCacheValue)
require.True(t, ok)
require.Equal(t, vals[i], value)
}
}
func TestLRUPCDelete(t *testing.T) {
mockCtx := MockContext()
mockCtx.GetSessionVars().EnablePlanCacheForParamLimit = true
lru := NewLRUPlanCache(3, 0, 0, mockCtx)
keys := make([]*planCacheKey, 3)
vals := make([]*PlanCacheValue, 3)
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
}
limitParams := [][]uint64{
{1}, {2}, {3},
}
for i := 0; i < 3; i++ {
keys[i] = &planCacheKey{database: strconv.FormatInt(int64(i), 10)}
vals[i] = &PlanCacheValue{
matchOpts: planCacheMatchOpts{
paramTypes: pTypes[i],
limitOffsetAndCount: limitParams[i],
},
}
lru.Put(keys[i], vals[i], pTypes[i], []uint64{})
}
require.Equal(t, 3, int(lru.size))
lru.Delete(keys[1])
value, exists := lru.Get(keys[1], pTypes[1], limitParams[1])
require.False(t, exists)
require.Nil(t, value)
require.Equal(t, 2, int(lru.size))
_, exists = lru.Get(keys[0], pTypes[0], limitParams[0])
require.True(t, exists)
_, exists = lru.Get(keys[2], pTypes[2], limitParams[2])
require.True(t, exists)
}
func TestLRUPCDeleteAll(t *testing.T) {
lru := NewLRUPlanCache(3, 0, 0, MockContext())
keys := make([]*planCacheKey, 3)
vals := make([]*PlanCacheValue, 3)
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
}
for i := 0; i < 3; i++ {
keys[i] = &planCacheKey{database: strconv.FormatInt(int64(i), 10)}
vals[i] = &PlanCacheValue{
matchOpts: planCacheMatchOpts{
paramTypes: pTypes[i],
},
}
lru.Put(keys[i], vals[i], pTypes[i], []uint64{})
}
require.Equal(t, 3, int(lru.size))
lru.DeleteAll()
for i := 0; i < 3; i++ {
value, exists := lru.Get(keys[i], pTypes[i], []uint64{})
require.False(t, exists)
require.Nil(t, value)
require.Equal(t, 0, int(lru.size))
}
}
func TestLRUPCSetCapacity(t *testing.T) {
maxMemDroppedKv := make(map[kvcache.Key]kvcache.Value)
lru := NewLRUPlanCache(5, 0, 0, MockContext())
lru.onEvict = func(key kvcache.Key, value kvcache.Value) {
maxMemDroppedKv[key] = value
}
require.Equal(t, uint(5), lru.capacity)
keys := make([]*planCacheKey, 5)
vals := make([]*PlanCacheValue, 5)
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeLong)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeInt24)},
}
// one key corresponding to multi values
for i := 0; i < 5; i++ {
keys[i] = &planCacheKey{database: strconv.FormatInt(int64(1), 10)}
vals[i] = &PlanCacheValue{
matchOpts: planCacheMatchOpts{
paramTypes: pTypes[i],
}}
lru.Put(keys[i], vals[i], pTypes[i], []uint64{})
}
require.Equal(t, lru.size, lru.capacity)
require.Equal(t, uint(5), lru.size)
err := lru.SetCapacity(3)
require.NoError(t, err)
// test for non-existent elements
require.Len(t, maxMemDroppedKv, 2)
for i := 0; i < 2; i++ {
bucket, exist := lru.buckets[string(hack.String(keys[i].Hash()))]
require.True(t, exist)
for element := range bucket {
require.NotEqual(t, vals[i], element.Value.(*planCacheEntry).PlanValue)
}
require.Equal(t, vals[i], maxMemDroppedKv[keys[i]])
}
// test for existent elements
root := lru.lruList.Front()
require.NotNil(t, root)
for i := 4; i >= 2; i-- {
entry, ok := root.Value.(*planCacheEntry)
require.True(t, ok)
require.NotNil(t, entry)
// test value
value, ok := entry.PlanValue.(*PlanCacheValue)
require.True(t, ok)
require.Equal(t, vals[i], value)
root = root.Next()
}
// test for end of double-linked list
require.Nil(t, root)
err = lru.SetCapacity(0)
require.Error(t, err, "capacity of LRU cache should be at least 1")
}
func TestIssue37914(t *testing.T) {
lru := NewLRUPlanCache(3, 0.1, 1, MockContext())
pTypes := []*types.FieldType{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)}
key := &planCacheKey{database: strconv.FormatInt(int64(1), 10)}
val := &PlanCacheValue{matchOpts: planCacheMatchOpts{paramTypes: pTypes}}
require.NotPanics(t, func() {
lru.Put(key, val, pTypes, []uint64{})
})
}
func TestIssue38244(t *testing.T) {
lru := NewLRUPlanCache(3, 0, 0, MockContext())
require.Equal(t, uint(3), lru.capacity)
keys := make([]*planCacheKey, 5)
vals := make([]*PlanCacheValue, 5)
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeLong)},
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeInt24)},
}
// one key corresponding to multi values
for i := 0; i < 5; i++ {
keys[i] = &planCacheKey{database: strconv.FormatInt(int64(i), 10)}
vals[i] = &PlanCacheValue{matchOpts: planCacheMatchOpts{paramTypes: pTypes[i]}}
lru.Put(keys[i], vals[i], pTypes[i], []uint64{})
}
require.Equal(t, lru.size, lru.capacity)
require.Equal(t, uint(3), lru.size)
require.Equal(t, len(lru.buckets), 3)
}
func TestLRUPlanCacheMemoryUsage(t *testing.T) {
pTypes := []*types.FieldType{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)}
ctx := MockContext()
ctx.GetSessionVars().EnablePreparedPlanCacheMemoryMonitor = true
lru := NewLRUPlanCache(3, 0, 0, ctx)
evict := make(map[kvcache.Key]kvcache.Value)
lru.onEvict = func(key kvcache.Key, value kvcache.Value) {
evict[key] = value
}
var res int64 = 0
// put
for i := 0; i < 3; i++ {
k := randomPlanCacheKey()
v := randomPlanCacheValue(pTypes)
lru.Put(k, v, pTypes, []uint64{})
res += k.MemoryUsage() + v.MemoryUsage()
require.Equal(t, lru.MemoryUsage(), res)
}
// evict
p := &PhysicalTableScan{}
k := &planCacheKey{database: "3"}
v := &PlanCacheValue{Plan: p}
lru.Put(k, v, pTypes, []uint64{})
res += k.MemoryUsage() + v.MemoryUsage()
for kk, vv := range evict {
res -= kk.(*planCacheKey).MemoryUsage() + vv.(*PlanCacheValue).MemoryUsage()
}
require.Equal(t, lru.MemoryUsage(), res)
// delete
lru.Delete(k)
res -= k.MemoryUsage() + v.MemoryUsage()
require.Equal(t, lru.MemoryUsage(), res)
// delete all
lru.DeleteAll()
require.Equal(t, lru.MemoryUsage(), int64(0))
}