From 30698fa56b3b82f618cd51a7dd8b4c47f89e7fc0 Mon Sep 17 00:00:00 2001 From: Matt Stone Date: Thu, 15 Jan 2026 09:45:05 +1000 Subject: [PATCH] LRU cache optimisation --- pkg/file/stashignore.go | 64 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/pkg/file/stashignore.go b/pkg/file/stashignore.go index be3571ca5..160b5c224 100644 --- a/pkg/file/stashignore.go +++ b/pkg/file/stashignore.go @@ -8,17 +8,26 @@ import ( "strings" "sync" + lru "github.com/hashicorp/golang-lru/v2" ignore "github.com/sabhiram/go-gitignore" "github.com/stashapp/stash/pkg/logger" ) const stashIgnoreFilename = ".stashignore" +// entriesCacheSize is the size of the LRU cache for collected ignore entries. +// This cache stores the computed list of ignore entries per directory, avoiding +// repeated directory tree walks for files in the same directory. +const entriesCacheSize = 500 + // StashIgnoreFilter implements PathFilter to exclude files/directories // based on .stashignore files with gitignore-style patterns. type StashIgnoreFilter struct { // cache stores compiled ignore patterns per directory. cache sync.Map // map[string]*ignoreEntry + // entriesCache stores collected ignore entries per (dir, libraryRoot) pair. + // This avoids recomputing the entry list for every file in the same directory. + entriesCache *lru.Cache[string, []*ignoreEntry] } // ignoreEntry holds the compiled ignore patterns for a directory. @@ -31,7 +40,12 @@ type ignoreEntry struct { // NewStashIgnoreFilter creates a new StashIgnoreFilter. func NewStashIgnoreFilter() *StashIgnoreFilter { - return &StashIgnoreFilter{} + // Create the LRU cache for collected entries. + // Ignore error as it only fails if size <= 0. + entriesCache, _ := lru.New[string, []*ignoreEntry](entriesCacheSize) + return &StashIgnoreFilter{ + entriesCache: entriesCache, + } } // Accept returns true if the path should be included in the scan. @@ -80,16 +94,44 @@ func (f *StashIgnoreFilter) Accept(ctx context.Context, path string, info fs.Fil // collectIgnoreEntries gathers all ignore entries from library root to the given directory. // It walks up the directory tree from dir to libraryRoot and returns entries in order -// from root to most specific. +// from root to most specific. Results are cached to avoid repeated computation for +// files in the same directory. func (f *StashIgnoreFilter) collectIgnoreEntries(dir string, libraryRoot string) []*ignoreEntry { - // Collect directories from library root down to current dir. - var dirs []string - - // Clean paths for consistent comparison. + // Clean paths for consistent comparison and cache key generation. dir = filepath.Clean(dir) libraryRoot = filepath.Clean(libraryRoot) + // Build cache key from dir and libraryRoot. + cacheKey := dir + "\x00" + libraryRoot + + // Check the entries cache first. + if cached, ok := f.entriesCache.Get(cacheKey); ok { + return cached + } + + // Try subdirectory shortcut: if parent's entries are cached, extend them. + if dir != libraryRoot { + parent := filepath.Dir(dir) + if isPathInOrEqual(libraryRoot, parent) { + parentKey := parent + "\x00" + libraryRoot + if parentEntries, ok := f.entriesCache.Get(parentKey); ok { + // Parent is cached - just check if current dir has a .stashignore. + entries := parentEntries + if entry := f.getOrLoadIgnoreEntry(dir); entry != nil { + // Copy parent slice and append to avoid mutating cached slice. + entries = make([]*ignoreEntry, len(parentEntries), len(parentEntries)+1) + copy(entries, parentEntries) + entries = append(entries, entry) + } + f.entriesCache.Add(cacheKey, entries) + return entries + } + } + } + + // No cache hit - compute from scratch. // Walk up from dir to library root, collecting directories. + var dirs []string current := dir for { // Check if we're still within the library root. @@ -97,7 +139,7 @@ func (f *StashIgnoreFilter) collectIgnoreEntries(dir string, libraryRoot string) break } - dirs = append([]string{current}, dirs...) // Prepend to maintain root-to-leaf order. + dirs = append(dirs, current) // Stop if we've reached the library root. if current == libraryRoot { @@ -112,6 +154,11 @@ func (f *StashIgnoreFilter) collectIgnoreEntries(dir string, libraryRoot string) current = parent } + // Reverse to get root-to-leaf order. + for i, j := 0, len(dirs)-1; i < j; i, j = i+1, j-1 { + dirs[i], dirs[j] = dirs[j], dirs[i] + } + // Check each directory for .stashignore files. var entries []*ignoreEntry for _, d := range dirs { @@ -120,6 +167,9 @@ func (f *StashIgnoreFilter) collectIgnoreEntries(dir string, libraryRoot string) } } + // Cache the result. + f.entriesCache.Add(cacheKey, entries) + return entries }