Skip to content

Commit be04a21

Browse files
committed
⚡ Improve attribute view search cache coordination #18474
1 parent 6f407c6 commit be04a21

5 files changed

Lines changed: 298 additions & 48 deletions

File tree

kernel/av/av.go

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/siyuan-note/logging"
3636
"github.com/siyuan-note/siyuan/kernel/cache"
3737
"github.com/siyuan-note/siyuan/kernel/util"
38+
"golang.org/x/sync/singleflight"
3839
)
3940

4041
// AttributeView 描述了属性视图的结构。
@@ -592,48 +593,70 @@ func parseAttributeViewSearchInfo(data []byte) (ret *AttributeViewSearchInfo, er
592593
return
593594
}
594595

596+
var attributeViewSearchInfoFlight singleflight.Group
597+
595598
func GetAttributeViewSearchInfoInBox(avID, boxID string) (ret *AttributeViewSearchInfo, err error) {
596599
avJSONPath, avBoxID := FindAttributeViewPathInBox(avID, boxID)
597600
if avJSONPath == "" {
598601
return
599602
}
603+
if cached, ok := cache.GetAVSearchDataInBox[*AttributeViewSearchInfo](avID, avBoxID); ok {
604+
return cached, nil
605+
}
606+
607+
value, err, _ := attributeViewSearchInfoFlight.Do(avBoxID+"\x00"+avID, func() (any, error) {
608+
return loadAttributeViewSearchInfoInBox(avID, avJSONPath, avBoxID)
609+
})
610+
if value != nil {
611+
ret = value.(*AttributeViewSearchInfo)
612+
}
613+
return
614+
}
615+
616+
func loadAttributeViewSearchInfoInBox(avID, avJSONPath, avBoxID string) (ret *AttributeViewSearchInfo, err error) {
600617
release, lockErr := holdAVBoxReadLock(avBoxID)
601618
if lockErr != nil {
602619
return nil, lockErr
603620
}
604621
defer release()
605622

606-
if cached, ok := cache.GetAVSearchDataInBox[*AttributeViewSearchInfo](avID, avBoxID); ok {
607-
return cached, nil
608-
}
609-
610-
var data []byte
611-
var dataVersion uint64
612-
if cached, version, ok := cache.GetAVDataWithVersionInBox(avID, avBoxID); ok {
613-
data = cached
614-
dataVersion = version
615-
} else {
616-
dataVersion = cache.EnsureAVDataVersionInBox(avID, avBoxID)
617-
if data, err = filelock.ReadFile(avJSONPath); err != nil {
618-
logging.LogErrorf("read attribute view [%s] failed: %s", avJSONPath, err)
619-
return
623+
for i := 0; i < 3; i++ {
624+
if cached, ok := cache.GetAVSearchDataInBox[*AttributeViewSearchInfo](avID, avBoxID); ok {
625+
return cached, nil
620626
}
621-
if avBoxID != "" {
622-
if data, err = decryptAVDataLocked(avBoxID, avID, data); err != nil {
623-
logging.LogErrorf("decrypt attribute view [%s] failed: %s", avJSONPath, err)
627+
628+
var data []byte
629+
var dataVersion uint64
630+
if cached, version, ok := cache.GetAVDataWithVersionInBox(avID, avBoxID); ok {
631+
data = cached
632+
dataVersion = version
633+
} else {
634+
dataVersion = cache.EnsureAVDataVersionInBox(avID, avBoxID)
635+
if data, err = filelock.ReadFile(avJSONPath); err != nil {
636+
logging.LogErrorf("read attribute view [%s] failed: %s", avJSONPath, err)
624637
return
625638
}
626-
} else if util.IsCiphertext(data) {
627-
return
639+
if avBoxID != "" {
640+
if data, err = decryptAVDataLocked(avBoxID, avID, data); err != nil {
641+
logging.LogErrorf("decrypt attribute view [%s] failed: %s", avJSONPath, err)
642+
return
643+
}
644+
} else if util.IsCiphertext(data) {
645+
return nil, nil
646+
}
628647
}
629-
}
630648

631-
if ret, err = parseAttributeViewSearchInfo(data); err != nil {
632-
logging.LogErrorf("unmarshal attribute view search info [%s] failed: %s", avID, err)
633-
return nil, err
649+
if ret, err = parseAttributeViewSearchInfo(data); err != nil {
650+
logging.LogErrorf("unmarshal attribute view search info [%s] failed: %s", avID, err)
651+
return nil, err
652+
}
653+
if cache.SetAVSearchDataInBox(avID, avBoxID, dataVersion, ret) {
654+
return ret, nil
655+
}
634656
}
635-
cache.SetAVSearchDataInBox(avID, avBoxID, dataVersion, ret)
636-
return
657+
err = fmt.Errorf("attribute view [%s] changed while loading search info", avID)
658+
logging.LogWarnf("%s", err)
659+
return nil, err
637660
}
638661

639662
func GetAttributeViewContent(avID string) (content string) {

kernel/cache/av.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package cache
1818

1919
import (
2020
"sync"
21+
"sync/atomic"
2122

2223
"github.com/dgraph-io/ristretto"
2324
)
@@ -45,6 +46,7 @@ var avDataVersion uint64
4546
var avDataVersions = map[string]uint64{}
4647
var avSearchDataCache = map[string]*avSearchDataEntry{}
4748
var avSearchDataCacheLock sync.RWMutex
49+
var avCacheGeneration atomic.Uint64
4850

4951
func avCacheKey(avID, boxID string) string {
5052
return boxID + "\x00" + avID
@@ -143,6 +145,7 @@ func RemoveAVData(avID string) {
143145
}
144146

145147
func ClearAVCache() {
148+
avCacheGeneration.Add(1)
146149
avCacheKeysLock.Lock()
147150
avCacheKeys = map[string]map[string]struct{}{}
148151
avCacheKeysLock.Unlock()
@@ -154,6 +157,10 @@ func ClearAVCache() {
154157
avSearchDataCacheLock.Unlock()
155158
}
156159

160+
func GetAVCacheGeneration() uint64 {
161+
return avCacheGeneration.Load()
162+
}
163+
157164
func GetAVSearchDataInBox[T any](avID, boxID string) (ret T, ok bool) {
158165
avSearchDataCacheLock.RLock()
159166
key := avCacheKey(avID, boxID)

kernel/cache/av_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,11 @@ func TestAVSearchDataWithoutRawData(t *testing.T) {
7373
t.Fatal("setting raw data should invalidate standalone search data")
7474
}
7575
}
76+
77+
func TestAVCacheGeneration(t *testing.T) {
78+
generation := GetAVCacheGeneration()
79+
ClearAVCache()
80+
if current := GetAVCacheGeneration(); current != generation+1 {
81+
t.Fatalf("unexpected AV cache generation: %d", current)
82+
}
83+
}

kernel/model/attribute_view.go

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package model
1818

1919
import (
2020
"bytes"
21+
"context"
2122
"encoding/json"
2223
"errors"
2324
"fmt"
@@ -2743,27 +2744,108 @@ type SearchAttributeViewOptions struct {
27432744
IncludeViewMatches bool
27442745
}
27452746

2746-
var attributeViewSearchCacheWarmups sync.Map
2747+
type attributeViewSearchCacheWarmup struct {
2748+
signature uint64
2749+
cancel context.CancelFunc
2750+
running bool
2751+
}
2752+
2753+
var attributeViewSearchCacheWarmups = struct {
2754+
sync.Mutex
2755+
states map[string]*attributeViewSearchCacheWarmup
2756+
}{states: map[string]*attributeViewSearchCacheWarmup{}}
2757+
2758+
var attributeViewSearchCacheWarmupDelay = 500 * time.Millisecond
2759+
var loadAttributeViewSearchInfo = av.GetAttributeViewSearchInfoInBox
27472760

2748-
func warmAttributeViewSearchCache(boxID string, avIDs []string) {
2761+
const attributeViewSearchCacheWarmupHashOffset = uint64(1469598103934665603)
2762+
const attributeViewSearchCacheWarmupHashPrime = uint64(1099511628211)
2763+
2764+
func warmAttributeViewSearchCache(boxID string, avIDs []string, signature uint64) {
27492765
if len(avIDs) == 0 {
27502766
return
27512767
}
2752-
if _, loaded := attributeViewSearchCacheWarmups.LoadOrStore(boxID, struct{}{}); loaded {
2768+
2769+
attributeViewSearchCacheWarmups.Lock()
2770+
if state := attributeViewSearchCacheWarmups.states[boxID]; state != nil && state.signature == signature {
2771+
attributeViewSearchCacheWarmups.Unlock()
27532772
return
27542773
}
2774+
if state := attributeViewSearchCacheWarmups.states[boxID]; state != nil && state.cancel != nil {
2775+
state.cancel()
2776+
}
2777+
ctx, cancel := context.WithCancel(context.Background())
2778+
state := &attributeViewSearchCacheWarmup{signature: signature, cancel: cancel, running: true}
2779+
attributeViewSearchCacheWarmups.states[boxID] = state
2780+
attributeViewSearchCacheWarmups.Unlock()
2781+
27552782
ids := slices.Clone(avIDs)
2783+
delay := attributeViewSearchCacheWarmupDelay
2784+
loader := loadAttributeViewSearchInfo
27562785
go func() {
2757-
defer attributeViewSearchCacheWarmups.Delete(boxID)
2786+
completed := false
2787+
defer func() {
2788+
attributeViewSearchCacheWarmups.Lock()
2789+
defer attributeViewSearchCacheWarmups.Unlock()
2790+
if attributeViewSearchCacheWarmups.states[boxID] != state {
2791+
return
2792+
}
2793+
if completed {
2794+
state.cancel = nil
2795+
state.running = false
2796+
} else {
2797+
delete(attributeViewSearchCacheWarmups.states, boxID)
2798+
}
2799+
}()
2800+
2801+
timer := time.NewTimer(delay)
2802+
defer timer.Stop()
2803+
select {
2804+
case <-ctx.Done():
2805+
return
2806+
case <-timer.C:
2807+
}
2808+
27582809
for _, avID := range ids {
2810+
select {
2811+
case <-ctx.Done():
2812+
return
2813+
default:
2814+
}
27592815
if isSyncingStorages() {
27602816
return
27612817
}
2762-
_, _ = av.GetAttributeViewSearchInfoInBox(avID, boxID)
2818+
_, _ = loader(avID, boxID)
27632819
}
2820+
completed = true
27642821
}()
27652822
}
27662823

2824+
func stopAttributeViewSearchCacheWarmup(boxID string) {
2825+
attributeViewSearchCacheWarmups.Lock()
2826+
defer attributeViewSearchCacheWarmups.Unlock()
2827+
state := attributeViewSearchCacheWarmups.states[boxID]
2828+
if state == nil || !state.running {
2829+
return
2830+
}
2831+
state.cancel()
2832+
delete(attributeViewSearchCacheWarmups.states, boxID)
2833+
}
2834+
2835+
func updateAttributeViewSearchCacheWarmupSignature(signature uint64, avID string, info os.FileInfo) uint64 {
2836+
for i := 0; i < len(avID); i++ {
2837+
signature ^= uint64(avID[i])
2838+
signature *= attributeViewSearchCacheWarmupHashPrime
2839+
}
2840+
if info != nil {
2841+
signature ^= uint64(info.ModTime().UnixNano())
2842+
signature *= attributeViewSearchCacheWarmupHashPrime
2843+
signature ^= uint64(info.Size())
2844+
signature *= attributeViewSearchCacheWarmupHashPrime
2845+
}
2846+
return signature
2847+
}
2848+
27672849
func matchAttributeViewSearchName(name string, keywords []string) (score float64, hit bool) {
27682850
if name == "" || len(keywords) == 0 {
27692851
return
@@ -2781,6 +2863,23 @@ func matchAttributeViewSearchName(name string, keywords []string) (score float64
27812863
return
27822864
}
27832865

2866+
func sortAndLimitAttributeViewSearchResults(results []*AvSearchTempResult, keyword string) []*AvSearchTempResult {
2867+
if keyword == "" {
2868+
sort.Slice(results, func(i, j int) bool { return results[i].AvUpdated > results[j].AvUpdated })
2869+
} else {
2870+
sort.SliceStable(results, func(i, j int) bool {
2871+
if results[i].Score == results[j].Score {
2872+
return results[i].AvUpdated > results[j].AvUpdated
2873+
}
2874+
return results[i].Score > results[j].Score
2875+
})
2876+
}
2877+
if 12 < len(results) {
2878+
return results[:12]
2879+
}
2880+
return results
2881+
}
2882+
27842883
func SearchAttributeView(keyword string, excludeAvIDs []string, currentAvID, currentBlockID string) []*AvSearchResult {
27852884
return SearchAttributeViewWithOptions(SearchAttributeViewOptions{
27862885
Keyword: keyword,
@@ -2823,6 +2922,9 @@ func SearchAttributeViewWithOptions(options SearchAttributeViewOptions) (ret []*
28232922

28242923
var avSearchTmpResults []*AvSearchTempResult
28252924
var warmupAvIDs []string
2925+
warmupSignature := attributeViewSearchCacheWarmupHashOffset
2926+
warmupSignature ^= cache.GetAVCacheGeneration()
2927+
warmupSignature *= attributeViewSearchCacheWarmupHashPrime
28262928
boxID := ""
28272929
if options.CurrentBlockID != "" {
28282930
if bt := treenode.GetBlockTree(options.CurrentBlockID); nil != bt && IsEncryptedBox(bt.BoxID) {
@@ -2831,6 +2933,9 @@ func SearchAttributeViewWithOptions(options SearchAttributeViewOptions) (ret []*
28312933
} else if options.CurrentAvID != "" {
28322934
_, boxID = av.FindAttributeViewPath(options.CurrentAvID)
28332935
}
2936+
if keyword != "" {
2937+
stopAttributeViewSearchCacheWarmup(boxID)
2938+
}
28342939
avDir := filepath.Join(util.DataDir, "storage", "av")
28352940
if boxID != "" {
28362941
avDir = filepath.Join(util.DataDir, boxID, "storage", "av")
@@ -2866,16 +2971,17 @@ func SearchAttributeViewWithOptions(options SearchAttributeViewOptions) (ret []*
28662971
if nil == avBlockRels[id] {
28672972
continue
28682973
}
2974+
readFileInfoStart := time.Now()
2975+
info, _ := entry.Info()
2976+
readFileInfoElapsed += time.Since(readFileInfoStart)
28692977
warmupAvIDs = append(warmupAvIDs, id)
2978+
warmupSignature = updateAttributeViewSearchCacheWarmupSignature(warmupSignature, id, info)
28702979

28712980
if gulu.Str.Contains(id, options.ExcludeAvIDs) {
28722981
continue
28732982
}
28742983
eligibleFileCount++
28752984

2876-
readFileInfoStart := time.Now()
2877-
info, _ := entry.Info()
2878-
readFileInfoElapsed += time.Since(readFileInfoStart)
28792985
if info != nil {
28802986
eligibleFileSize += info.Size()
28812987
}
@@ -2928,20 +3034,8 @@ func SearchAttributeViewWithOptions(options SearchAttributeViewOptions) (ret []*
29283034
scanElapsed = time.Since(scanStart)
29293035

29303036
sortStart := time.Now()
2931-
if "" == keyword {
2932-
sort.Slice(avSearchTmpResults, func(i, j int) bool { return avSearchTmpResults[i].AvUpdated > avSearchTmpResults[j].AvUpdated })
2933-
} else {
2934-
sort.SliceStable(avSearchTmpResults, func(i, j int) bool {
2935-
if avSearchTmpResults[i].Score == avSearchTmpResults[j].Score {
2936-
return avSearchTmpResults[i].AvUpdated > avSearchTmpResults[j].AvUpdated
2937-
}
2938-
return avSearchTmpResults[i].Score > avSearchTmpResults[j].Score
2939-
})
2940-
}
29413037
matchedCount = len(avSearchTmpResults)
2942-
if 12 <= len(avSearchTmpResults) {
2943-
avSearchTmpResults = avSearchTmpResults[:12]
2944-
}
3038+
avSearchTmpResults = sortAndLimitAttributeViewSearchResults(avSearchTmpResults, keyword)
29453039
sortElapsed = time.Since(sortStart)
29463040

29473041
resolveStart := time.Now()
@@ -3022,7 +3116,7 @@ func SearchAttributeViewWithOptions(options SearchAttributeViewOptions) (ret []*
30223116
}
30233117
resolveElapsed = time.Since(resolveStart)
30243118
if keyword == "" {
3025-
warmAttributeViewSearchCache(boxID, warmupAvIDs)
3119+
warmAttributeViewSearchCache(boxID, warmupAvIDs, warmupSignature)
30263120
}
30273121
return
30283122
}

0 commit comments

Comments
 (0)