diff --git a/planner/core/BUILD.bazel b/planner/core/BUILD.bazel index a27b6c3ed4..ff4120faaf 100644 --- a/planner/core/BUILD.bazel +++ b/planner/core/BUILD.bazel @@ -29,6 +29,7 @@ go_library( "physical_plans.go", "plan.go", "plan_cache.go", + "plan_cache_lru.go", "plan_cache_utils.go", "plan_cost.go", "plan_cost_detail.go", @@ -175,6 +176,7 @@ go_test( "partition_pruning_test.go", "physical_plan_test.go", "physical_plan_trace_test.go", + "plan_cache_lru_test.go", "plan_cache_test.go", "plan_cache_utils_test.go", "plan_cost_detail_test.go", diff --git a/planner/core/plan_cache_lru.go b/planner/core/plan_cache_lru.go new file mode 100644 index 0000000000..739558f06d --- /dev/null +++ b/planner/core/plan_cache_lru.go @@ -0,0 +1,175 @@ +// 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 ( + "container/list" + "sync" + + "github.com/pingcap/errors" + "github.com/pingcap/tidb/types" + "github.com/pingcap/tidb/util/hack" + "github.com/pingcap/tidb/util/kvcache" +) + +// planCacheEntry wraps Key and Value. It's the value of list.Element. +type planCacheEntry struct { + PlanKey kvcache.Key + PlanValue kvcache.Value +} + +// LRUPlanCache is a dedicated least recently used cache, JUST use for plan cache. +type LRUPlanCache struct { + capacity uint + size uint + // buckets replace the map in general LRU + buckets map[string]map[*list.Element]struct{} + lruList *list.List + // lock make cache thread safe + lock sync.Mutex + + // pickFromBucket get one element from bucket. The LRUPlanCache can not work if it is nil + pickFromBucket func(map[*list.Element]struct{}, []*types.FieldType) (*list.Element, bool) + // onEvict will be called if any eviction happened, only for test use now + onEvict func(kvcache.Key, kvcache.Value) +} + +// NewLRUPlanCache creates a PCLRUCache object, whose capacity is "capacity". +// NOTE: "capacity" should be a positive value. +func NewLRUPlanCache(capacity uint, + pickFromBucket func(map[*list.Element]struct{}, []*types.FieldType) (*list.Element, bool)) (*LRUPlanCache, error) { + if capacity < 1 { + return nil, errors.New("capacity of LRU Cache should be at least 1") + } + return &LRUPlanCache{ + capacity: capacity, + size: 0, + buckets: make(map[string]map[*list.Element]struct{}, 1), //Generally one query has one plan + lruList: list.New(), + pickFromBucket: pickFromBucket, + }, nil +} + +// Get tries to find the corresponding value according to the given key. +func (l *LRUPlanCache) Get(key kvcache.Key, paramTypes []*types.FieldType) (value kvcache.Value, ok bool) { + l.lock.Lock() + defer l.lock.Unlock() + + bucket, bucketExist := l.buckets[string(hack.String(key.Hash()))] + if bucketExist { + if element, exist := l.pickFromBucket(bucket, paramTypes); exist { + l.lruList.MoveToFront(element) + return element.Value.(*planCacheEntry).PlanValue, true + } + } + return nil, false +} + +// Put puts the (key, value) pair into the LRU Cache. +func (l *LRUPlanCache) Put(key kvcache.Key, value kvcache.Value, paramTypes []*types.FieldType) { + l.lock.Lock() + defer l.lock.Unlock() + + hash := string(key.Hash()) + bucket, bucketExist := l.buckets[hash] + if bucketExist { + if element, exist := l.pickFromBucket(bucket, paramTypes); exist { + element.Value.(*planCacheEntry).PlanValue = value + l.lruList.MoveToFront(element) + return + } + } else { + l.buckets[hash] = make(map[*list.Element]struct{}, 1) + } + + newCacheEntry := &planCacheEntry{ + PlanKey: key, + PlanValue: value, + } + element := l.lruList.PushFront(newCacheEntry) + l.buckets[hash][element] = struct{}{} + l.size++ + if l.size > l.capacity { + l.removeOldest() + } +} + +// Delete deletes the multi-values from the LRU Cache. +func (l *LRUPlanCache) Delete(key kvcache.Key) { + l.lock.Lock() + defer l.lock.Unlock() + + hash := hack.String(key.Hash()) + bucket, bucketExist := l.buckets[string(hash)] + if bucketExist { + for element := range bucket { + l.lruList.Remove(element) + l.size-- + } + delete(l.buckets, string(hash)) + } +} + +// DeleteAll deletes all elements from the LRU Cache. +func (l *LRUPlanCache) DeleteAll() { + l.lock.Lock() + defer l.lock.Unlock() + + for lru := l.lruList.Back(); lru != nil; lru = l.lruList.Back() { + l.lruList.Remove(lru) + l.size-- + } + l.buckets = make(map[string]map[*list.Element]struct{}, 1) +} + +// Size gets the current cache size. +func (l *LRUPlanCache) Size() int { + l.lock.Lock() + defer l.lock.Unlock() + + return int(l.size) +} + +// SetCapacity sets capacity of the cache. +func (l *LRUPlanCache) SetCapacity(capacity uint) error { + l.lock.Lock() + defer l.lock.Unlock() + + if capacity < 1 { + return errors.New("capacity of lru cache should be at least 1") + } + l.capacity = capacity + for l.size > l.capacity { + l.removeOldest() + } + return nil +} + +// removeOldest removes the oldest element from the cache. +func (l *LRUPlanCache) removeOldest() { + lru := l.lruList.Back() + if l.onEvict != nil { + l.onEvict(lru.Value.(*planCacheEntry).PlanKey, lru.Value.(*planCacheEntry).PlanValue) + } + + l.lruList.Remove(lru) + l.removeFromBucket(lru) + l.size-- +} + +// removeFromBucket remove element from bucket +func (l *LRUPlanCache) removeFromBucket(element *list.Element) { + bucket := l.buckets[string(hack.String(element.Value.(*planCacheEntry).PlanKey.Hash()))] + delete(bucket, element) +} diff --git a/planner/core/plan_cache_lru_test.go b/planner/core/plan_cache_lru_test.go new file mode 100644 index 0000000000..78658756b2 --- /dev/null +++ b/planner/core/plan_cache_lru_test.go @@ -0,0 +1,327 @@ +// 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 ( + "container/list" + "testing" + + "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" +) + +type mockCacheKey struct { + hash []byte + key int64 +} + +func (mk *mockCacheKey) Hash() []byte { + if mk.hash != nil { + return mk.hash + } + mk.hash = make([]byte, 8) + for i := uint(0); i < 8; i++ { + mk.hash[i] = byte((mk.key >> ((i - 1) * 8)) & 0xff) + } + return mk.hash +} + +func newMockHashKey(key int64) *mockCacheKey { + return &mockCacheKey{ + key: key, + } +} + +type fakePlan struct { + plan int64 + tps []*types.FieldType +} + +func pickFromBucket(bucket map[*list.Element]struct{}, ptypes []*types.FieldType) (*list.Element, bool) { + for element := range bucket { + itemsA := element.Value.(*planCacheEntry).PlanValue.(*fakePlan).tps + flag := true + for j := 0; j < len(itemsA); j++ { + if itemsA[j] != ptypes[j] { + flag = false + break + } + } + if flag { + return element, true + } + } + return nil, false +} + +func TestLRUPCPut(t *testing.T) { + // test initialize + lruA, errA := NewLRUPlanCache(0, pickFromBucket) + require.Nil(t, lruA) + require.Error(t, errA, "capacity of LRU Cache should be at least 1") + + maxMemDroppedKv := make(map[kvcache.Key]kvcache.Value) + lru, err := NewLRUPlanCache(3, pickFromBucket) + lru.onEvict = func(key kvcache.Key, value kvcache.Value) { + maxMemDroppedKv[key] = value + } + require.NoError(t, err) + require.Equal(t, uint(3), lru.capacity) + + keys := make([]*mockCacheKey, 5) + vals := make([]*fakePlan, 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] = newMockHashKey(1) + vals[i] = &fakePlan{ + plan: int64(i), + tps: pTypes[i], + } + lru.Put(keys[i], vals[i], pTypes[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) + element, exist := lru.pickFromBucket(bucket, pTypes[i]) + require.NotNil(t, element) + require.True(t, exist) + require.Equal(t, root, element) + + // test value + value, ok := entry.PlanValue.(*fakePlan) + 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) { + lru, err := NewLRUPlanCache(3, pickFromBucket) + require.NoError(t, err) + + keys := make([]*mockCacheKey, 5) + vals := make([]*fakePlan, 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)}, + } + // 5 bucket + for i := 0; i < 5; i++ { + keys[i] = newMockHashKey(int64(i % 4)) + vals[i] = &fakePlan{ + plan: int64(i), + tps: pTypes[i], + } + lru.Put(keys[i], vals[i], pTypes[i]) + } + + // test for non-existent elements + for i := 0; i < 2; i++ { + value, exists := lru.Get(keys[i], pTypes[i]) + require.False(t, exists) + require.Nil(t, value) + } + + for i := 2; i < 5; i++ { + value, exists := lru.Get(keys[i], pTypes[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.(*fakePlan) + require.True(t, ok) + require.Equal(t, vals[i], value) + } +} + +func TestLRUPCDelete(t *testing.T) { + lru, err := NewLRUPlanCache(3, pickFromBucket) + require.NoError(t, err) + + keys := make([]*mockCacheKey, 3) + vals := make([]*fakePlan, 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] = newMockHashKey(int64(i)) + vals[i] = &fakePlan{ + plan: int64(i), + tps: pTypes[i], + } + lru.Put(keys[i], vals[i], pTypes[i]) + } + require.Equal(t, 3, int(lru.size)) + + lru.Delete(keys[1]) + value, exists := lru.Get(keys[1], pTypes[1]) + require.False(t, exists) + require.Nil(t, value) + require.Equal(t, 2, int(lru.size)) + + _, exists = lru.Get(keys[0], pTypes[0]) + require.True(t, exists) + + _, exists = lru.Get(keys[2], pTypes[2]) + require.True(t, exists) +} + +func TestLRUPCDeleteAll(t *testing.T) { + lru, err := NewLRUPlanCache(3, pickFromBucket) + require.NoError(t, err) + + keys := make([]*mockCacheKey, 3) + vals := make([]*fakePlan, 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] = newMockHashKey(int64(i)) + vals[i] = &fakePlan{ + plan: int64(i), + tps: pTypes[i], + } + lru.Put(keys[i], vals[i], pTypes[i]) + } + require.Equal(t, 3, int(lru.size)) + + lru.DeleteAll() + + for i := 0; i < 3; i++ { + value, exists := lru.Get(keys[i], pTypes[i]) + 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, err := NewLRUPlanCache(5, pickFromBucket) + lru.onEvict = func(key kvcache.Key, value kvcache.Value) { + maxMemDroppedKv[key] = value + } + require.NoError(t, err) + require.Equal(t, uint(5), lru.capacity) + + keys := make([]*mockCacheKey, 5) + vals := make([]*fakePlan, 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] = newMockHashKey(1) + vals[i] = &fakePlan{ + plan: int64(i), + tps: pTypes[i], + } + lru.Put(keys[i], vals[i], pTypes[i]) + } + 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.(*fakePlan) + 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") +}