all: Replace RWMutex struct caches with ConcurrentMap

This commit is contained in:
Bjørn Erik Pedersen
2026-05-23 12:12:51 +02:00
parent d8c70218b7
commit c4bbc2805c
13 changed files with 66 additions and 168 deletions
+4 -13
View File
@@ -31,6 +31,7 @@ import (
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/BurntSushi/locker" "github.com/BurntSushi/locker"
"github.com/bep/helpers/maphelpers"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@@ -56,8 +57,7 @@ type Cache struct {
} }
type lockTracker struct { type lockTracker struct {
seenMu sync.RWMutex seen *maphelpers.ConcurrentSet[string]
seen map[string]struct{}
*locker.Locker *locker.Locker
} }
@@ -65,16 +65,7 @@ type lockTracker struct {
// Lock tracks the ids in use. We use this information to do garbage collection // Lock tracks the ids in use. We use this information to do garbage collection
// after a Hugo build. // after a Hugo build.
func (l *lockTracker) Lock(id string) { func (l *lockTracker) Lock(id string) {
l.seenMu.RLock() l.seen.AddIfAbsent(id)
if _, seen := l.seen[id]; !seen {
l.seenMu.RUnlock()
l.seenMu.Lock()
l.seen[id] = struct{}{}
l.seenMu.Unlock()
} else {
l.seenMu.RUnlock()
}
l.Locker.Lock(id) l.Locker.Lock(id)
} }
@@ -92,7 +83,7 @@ func NewCache(fs afero.Fs, cfg FileCacheConfig) *Cache {
return &Cache{ return &Cache{
Fs: fs, Fs: fs,
entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})}, entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: maphelpers.NewConcurrentSet[string]()},
cfg: cfg, cfg: cfg,
} }
} }
+2 -3
View File
@@ -93,10 +93,9 @@ func (c *Cache) Prune(force bool) (int, error) {
shouldRemove := force || c.isExpired(info.ModTime()) shouldRemove := force || c.isExpired(info.ModTime())
if !shouldRemove && len(c.entryLocker.seen) > 0 { if !shouldRemove && c.entryLocker.seen.Len() > 0 {
// Remove it if it's not been touched/used in the last build. // Remove it if it's not been touched/used in the last build.
_, seen := c.entryLocker.seen[name] shouldRemove = !c.entryLocker.seen.Has(name)
shouldRemove = !seen
} }
if shouldRemove { if shouldRemove {
+7 -36
View File
@@ -19,8 +19,8 @@ import (
"slices" "slices"
"sort" "sort"
"strings" "strings"
"sync"
"github.com/bep/helpers/maphelpers"
"github.com/gohugoio/hugo/compare" "github.com/gohugoio/hugo/compare"
) )
@@ -55,46 +55,17 @@ func EqualAny(a string, b ...string) bool {
return slices.Contains(b, a) return slices.Contains(b, a)
} }
// regexpCache represents a cache of regexp objects protected by a mutex. var reCache = *maphelpers.NewConcurrentMap[string, *regexp.Regexp]()
type regexpCache struct {
mu sync.RWMutex
re map[string]*regexp.Regexp
}
func (rc *regexpCache) getOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
var ok bool
if re, ok = rc.get(pattern); !ok {
re, err = regexp.Compile(pattern)
if err != nil {
return nil, err
}
rc.set(pattern, re)
}
return re, nil
}
func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) {
rc.mu.RLock()
re, ok = rc.re[key]
rc.mu.RUnlock()
return
}
func (rc *regexpCache) set(key string, re *regexp.Regexp) {
rc.mu.Lock()
rc.re[key] = re
rc.mu.Unlock()
}
var reCache = regexpCache{re: make(map[string]*regexp.Regexp)}
// GetOrCompileRegexp retrieves a regexp object from the cache based upon the pattern. // GetOrCompileRegexp retrieves a regexp object from the cache based upon the pattern.
// If the pattern is not found in the cache, the pattern is compiled and added to // If the pattern is not found in the cache, the pattern is compiled and added to
// the cache. // the cache.
func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) { func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
return reCache.getOrCompileRegexp(pattern) return reCache.GetOrCreate(pattern,
func() (*regexp.Regexp, error) {
return regexp.Compile(pattern)
},
)
} }
// HasAnyPrefix checks if the string s has any of the prefixes given. // HasAnyPrefix checks if the string s has any of the prefixes given.
+1 -1
View File
@@ -14,7 +14,7 @@ require (
github.com/bep/golibsass v1.2.0 github.com/bep/golibsass v1.2.0
github.com/bep/golocales v0.1.0 github.com/bep/golocales v0.1.0
github.com/bep/goportabletext v0.2.0 github.com/bep/goportabletext v0.2.0
github.com/bep/helpers v0.8.0 github.com/bep/helpers v0.12.0
github.com/bep/imagemeta v0.17.2 github.com/bep/imagemeta v0.17.2
github.com/bep/lazycache v0.8.1 github.com/bep/lazycache v0.8.1
github.com/bep/logg v0.4.0 github.com/bep/logg v0.4.0
+2 -2
View File
@@ -162,8 +162,8 @@ github.com/bep/golocales v0.1.0 h1:rjWf1S4basIje+G+je5WMW8G+yzaoz4gEDFolrFVdvA=
github.com/bep/golocales v0.1.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE= github.com/bep/golocales v0.1.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE=
github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0= github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0=
github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU= github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU=
github.com/bep/helpers v0.8.0 h1:plg2BFgA9AgIHF2XemyZdZLqixjzQk3uyyArV48FngQ= github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY=
github.com/bep/helpers v0.8.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps= github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps=
github.com/bep/imagemeta v0.17.2 h1:fDyXM1eAqCfBeqGLqS6UsN4OfuLM0cdu70KuLCehjOg= github.com/bep/imagemeta v0.17.2 h1:fDyXM1eAqCfBeqGLqS6UsN4OfuLM0cdu70KuLCehjOg=
github.com/bep/imagemeta v0.17.2/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA= github.com/bep/imagemeta v0.17.2/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg= github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg=
+3 -2
View File
@@ -26,6 +26,7 @@ import (
"time" "time"
"github.com/bep/debounce" "github.com/bep/debounce"
"github.com/bep/helpers/maphelpers"
"github.com/bep/logg" "github.com/bep/logg"
"github.com/gohugoio/go-radix" "github.com/gohugoio/go-radix"
"github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/bufferpool"
@@ -692,7 +693,7 @@ func (h *HugoSites) postProcess(l logg.LevelLogger) error {
} }
var toPostProcess []postpub.PostPublishedResource var toPostProcess []postpub.PostPublishedResource
for _, r := range h.ResourceSpec.PostProcessResources { for _, r := range h.ResourceSpec.PostProcessResources.All() {
toPostProcess = append(toPostProcess, r) toPostProcess = append(toPostProcess, r)
} }
@@ -758,7 +759,7 @@ func (h *HugoSites) postProcess(l logg.LevelLogger) error {
// Prepare for a new build. // Prepare for a new build.
for _, s := range h.Sites { for _, s := range h.Sites {
s.ResourceSpec.PostProcessResources = make(map[string]postpub.PostPublishedResource) s.ResourceSpec.PostProcessResources = maphelpers.NewConcurrentMap[string, postpub.PostPublishedResource]()
} }
return g.Wait() return g.Wait()
+5 -10
View File
@@ -27,6 +27,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/bep/helpers/maphelpers"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hstore" "github.com/gohugoio/hugo/common/hstore"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
@@ -286,24 +287,18 @@ type shortcodeParseInfo struct {
shortcodes []*shortcode shortcodes []*shortcode
// All the shortcode names in this set. // All the shortcode names in this set.
nameSetMu sync.RWMutex nameSet *maphelpers.ConcurrentSet[string]
nameSet map[string]bool
// Configuration // Configuration
enableInlineShortcodes bool enableInlineShortcodes bool
} }
func (s *shortcodeParseInfo) addName(name string) { func (s *shortcodeParseInfo) addName(name string) {
s.nameSetMu.Lock() s.nameSet.Add(name)
defer s.nameSetMu.Unlock()
s.nameSet[name] = true
} }
func (s *shortcodeParseInfo) hasName(name string) bool { func (s *shortcodeParseInfo) hasName(name string) bool {
s.nameSetMu.RLock() return s.nameSet.Has(name)
defer s.nameSetMu.RUnlock()
_, ok := s.nameSet[name]
return ok
} }
func newShortcodeHandler(filename string, d *deps.Deps) *shortcodeParseInfo { func newShortcodeHandler(filename string, d *deps.Deps) *shortcodeParseInfo {
@@ -312,7 +307,7 @@ func newShortcodeHandler(filename string, d *deps.Deps) *shortcodeParseInfo {
firstTemplateStore: d.TemplateStore, firstTemplateStore: d.TemplateStore,
enableInlineShortcodes: d.ExecHelper.Sec().EnableInlineShortcodes, enableInlineShortcodes: d.ExecHelper.Sec().EnableInlineShortcodes,
shortcodes: make([]*shortcode, 0, 4), shortcodes: make([]*shortcode, 0, 4),
nameSet: make(map[string]bool), nameSet: maphelpers.NewConcurrentSet[string](),
} }
return sh return sh
+7 -23
View File
@@ -14,37 +14,21 @@
package chromalexers package chromalexers
import ( import (
"sync"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/lexers"
"github.com/bep/helpers/maphelpers"
) )
type lexersMap struct { var lexerCache = *maphelpers.NewConcurrentMap[string, chroma.Lexer]()
lexers map[string]chroma.Lexer
mu sync.RWMutex
}
var lexerCache = &lexersMap{lexers: make(map[string]chroma.Lexer)}
// Get returns a lexer for the given language name, nil if not found. // Get returns a lexer for the given language name, nil if not found.
// This is just a wrapper around chromalexers.Get that caches the result. // This is just a wrapper around chromalexers.Get that caches the result.
// Reasoning for this is that chromalexers.Get is slow in the case where the lexer is not found, // Reasoning for this is that chromalexers.Get is slow in the case where the lexer is not found,
// which is a common case in Hugo. // which is a common case in Hugo.
func Get(name string) chroma.Lexer { func Get(name string) chroma.Lexer {
lexerCache.mu.RLock() l, _ := lexerCache.GetOrCreate(name, func() (chroma.Lexer, error) {
lexer, found := lexerCache.lexers[name] l := lexers.Get(name)
lexerCache.mu.RUnlock() return l, nil
})
if found { return l
return lexer
}
lexer = lexers.Get(name)
lexerCache.mu.Lock()
lexerCache.lexers[name] = lexer
lexerCache.mu.Unlock()
return lexer
} }
+11 -23
View File
@@ -16,59 +16,47 @@ package jsconfig
import ( import (
"path/filepath" "path/filepath"
"sort" "sort"
"sync"
"github.com/bep/helpers/maphelpers"
) )
// Builder builds a jsconfig.json file that, currently, is used only to assist // Builder builds a jsconfig.json file that, currently, is used only to assist
// IntelliSense in editors. // IntelliSense in editors.
type Builder struct { type Builder struct {
sourceRootsMu sync.RWMutex sourceRoots *maphelpers.ConcurrentSet[string]
sourceRoots map[string]bool
} }
// NewBuilder creates a new Builder. // NewBuilder creates a new Builder.
func NewBuilder() *Builder { func NewBuilder() *Builder {
return &Builder{sourceRoots: make(map[string]bool)} return &Builder{sourceRoots: maphelpers.NewConcurrentSet[string]()}
} }
// Build builds a new Config with paths relative to dir. // Build builds a new Config with paths relative to dir.
// This method is thread safe. // This method is thread safe.
func (b *Builder) Build(dir string) *Config { func (b *Builder) Build(dir string) *Config {
b.sourceRootsMu.RLock() if b.sourceRoots.Len() == 0 {
defer b.sourceRootsMu.RUnlock()
if len(b.sourceRoots) == 0 {
return nil return nil
} }
conf := newJSConfig()
var roots []string var roots []string
for root := range b.sourceRoots { for root := range b.sourceRoots.All() {
rel, err := filepath.Rel(dir, filepath.Join(root, "*")) rel, err := filepath.Rel(dir, filepath.Join(root, "*"))
if err == nil { if err == nil {
roots = append(roots, rel) roots = append(roots, rel)
} }
} }
if len(roots) == 0 {
return nil
}
sort.Strings(roots) sort.Strings(roots)
conf := newJSConfig()
conf.CompilerOptions.Paths["*"] = roots conf.CompilerOptions.Paths["*"] = roots
return conf return conf
} }
// AddSourceRoot adds a new source root. // AddSourceRoot adds a new source root.
// This method is thread safe. // This method is thread safe.
func (b *Builder) AddSourceRoot(root string) { func (b *Builder) AddSourceRoot(root string) {
b.sourceRootsMu.RLock() b.sourceRoots.AddIfAbsent(root)
found := b.sourceRoots[root]
b.sourceRootsMu.RUnlock()
if found {
return
}
b.sourceRootsMu.Lock()
b.sourceRoots[root] = true
b.sourceRootsMu.Unlock()
} }
// CompilerOptions holds compilerOptions for jsonconfig.json. // CompilerOptions holds compilerOptions for jsonconfig.json.
+6 -22
View File
@@ -25,27 +25,11 @@ type transformationKeyer interface {
// PostProcess wraps the given Resource for later processing. // PostProcess wraps the given Resource for later processing.
func (spec *Spec) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) { func (spec *Spec) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) {
key := r.(transformationKeyer).TransformationKey() key := r.(transformationKeyer).TransformationKey()
spec.postProcessMu.RLock() return spec.PostProcessResources.GetOrCreate(key, func() (postpub.PostPublishedResource, error) {
result, found := spec.PostProcessResources[key] result := postpub.NewPostPublishResource(spec.incr.Incr(), r)
spec.postProcessMu.RUnlock() if result == nil {
if found { panic("got nil result")
}
return result, nil return result, nil
} })
spec.postProcessMu.Lock()
defer spec.postProcessMu.Unlock()
// Double check
result, found = spec.PostProcessResources[key]
if found {
return result, nil
}
result = postpub.NewPostPublishResource(spec.incr.Incr(), r)
if result == nil {
panic("got nil result")
}
spec.PostProcessResources[key] = result
return result, nil
} }
+3 -3
View File
@@ -18,6 +18,7 @@ import (
"path" "path"
"sync" "sync"
"github.com/bep/helpers/maphelpers"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/internal/warpc" "github.com/gohugoio/hugo/internal/warpc"
@@ -88,7 +89,7 @@ func NewSpec(
incr: incr, incr: incr,
FileCaches: fileCaches, FileCaches: fileCaches,
PostBuildAssets: &PostBuildAssets{ PostBuildAssets: &PostBuildAssets{
PostProcessResources: make(map[string]postpub.PostPublishedResource), PostProcessResources: maphelpers.NewConcurrentMap[string, postpub.PostPublishedResource](),
JSConfigBuilder: jsconfig.NewBuilder(), JSConfigBuilder: jsconfig.NewBuilder(),
}, },
} }
@@ -155,8 +156,7 @@ type SpecCommon struct {
} }
type PostBuildAssets struct { type PostBuildAssets struct {
postProcessMu sync.RWMutex PostProcessResources *maphelpers.ConcurrentMap[string, postpub.PostPublishedResource]
PostProcessResources map[string]postpub.PostPublishedResource
JSConfigBuilder *jsconfig.Builder JSConfigBuilder *jsconfig.Builder
} }
+13 -29
View File
@@ -20,8 +20,8 @@ import (
"image" "image"
"path" "path"
"path/filepath" "path/filepath"
"sync"
"github.com/bep/helpers/maphelpers"
"github.com/bep/overlayfs" "github.com/bep/overlayfs"
"github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
@@ -52,7 +52,7 @@ func New(d *deps.Deps) *Namespace {
return &Namespace{ return &Namespace{
readFileFs: readFileFs, readFileFs: readFileFs,
Filters: &images.Filters{}, Filters: &images.Filters{},
cache: map[string]image.Config{}, cache: maphelpers.NewConcurrentMap[string, image.Config](),
deps: d, deps: d,
createClient: create.New(d.ResourceSpec), createClient: create.New(d.ResourceSpec),
} }
@@ -62,8 +62,7 @@ func New(d *deps.Deps) *Namespace {
type Namespace struct { type Namespace struct {
*images.Filters *images.Filters
readFileFs afero.Fs readFileFs afero.Fs
cacheMu sync.RWMutex cache *maphelpers.ConcurrentMap[string, image.Config]
cache map[string]image.Config
deps *deps.Deps deps *deps.Deps
createClient *create.Client createClient *create.Client
} }
@@ -80,34 +79,19 @@ func (ns *Namespace) Config(path any) (image.Config, error) {
return image.Config{}, errors.New("config needs a filename") return image.Config{}, errors.New("config needs a filename")
} }
// Check cache for image config. return ns.cache.GetOrCreate(filename, func() (image.Config, error) {
ns.cacheMu.RLock() f, err := ns.readFileFs.Open(filename)
config, ok := ns.cache[filename] if err != nil {
ns.cacheMu.RUnlock() return image.Config{}, err
}
defer f.Close()
if ok { ext := filepath.Ext(filename)
return config, nil format, _ := images.ImageFormatFromExt(ext)
}
f, err := ns.readFileFs.Open(filename) config, _, err := ns.deps.ResourceSpec.Imaging.Codec.DecodeConfig(format, f)
if err != nil {
return image.Config{}, err
}
defer f.Close()
ext := filepath.Ext(filename)
format, _ := images.ImageFormatFromExt(ext)
config, _, err = ns.deps.ResourceSpec.Imaging.Codec.DecodeConfig(format, f)
if err != nil {
return config, err return config, err
} })
ns.cacheMu.Lock()
ns.cache[filename] = config
ns.cacheMu.Unlock()
return config, nil
} }
// Filter applies the given filters to the image given as the last element in args. // Filter applies the given filters to the image given as the last element in args.
+2 -1
View File
@@ -113,7 +113,8 @@ func TestNSConfig(t *testing.T) {
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) c.Assert(result, qt.Equals, test.expect)
c.Assert(len(ns.cache), qt.Not(qt.Equals), 0) _, cached := ns.cache.Lookup(sp)
c.Assert(cached, qt.IsTrue)
} }
} }