]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Optimize memory allocations for sites matrix vector stores
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 16 Nov 2025 12:28:43 +0000 (13:28 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 17 Nov 2025 15:01:37 +0000 (16:01 +0100)
By

* Caching common sites matrix setups (e.g. the single site in single site setups).
* Adding a fast path to IntSets.HasAnyVector for the common case of single vector input.

```
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10   31.62m ± 46%   30.68m ± 42%  ~ (p=0.310 n=6)

                                                                                   │ master.bench │          perfcommon.bench          │
                                                                                   │     B/op     │     B/op      vs base              │
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10   31.98Mi ± 0%   31.24Mi ± 0%  -2.30% (p=0.002 n=6)

                                                                                   │ master.bench │         perfcommon.bench          │
                                                                                   │  allocs/op   │  allocs/op   vs base              │
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10    460.9k ± 0%   419.9k ± 0%  -8.90% (p=0.002 n=6)
````

common/paths/pathparser.go
config/allconfig/allconfig.go
hugolib/content_map_page_assembler.go
hugolib/content_map_page_contentnode.go
hugolib/roles/roles.go
hugolib/sitesmatrix/dimensions.go
hugolib/sitesmatrix/vectorstores.go
hugolib/sitesmatrix/vectorstores_test.go
hugolib/versions/versions.go
langs/config.go
resources/page/pagemeta/page_frontmatter.go

index 1246694ccbeaaf5b390ecaf84c0162b2bcbdab9d..8029b6cdaebb931421eaa071fdea7562825e562b 100644 (file)
@@ -54,7 +54,7 @@ type PathParser struct {
 
        // Below gets created on demand.
        initOnce         sync.Once
-       sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language code to sites matrix vector store.
+       sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
 }
 
 func (pp *PathParser) init() {
index d5d00d679579c42948cf87efa7ed46b549d87fb5..792cf1a20b8dcb209d3b308cc8ea38a7b66ee946 100644 (file)
@@ -878,6 +878,10 @@ func (c *Configs) Init(logger loggers.Logger) error {
                ConfiguredRoles:     c.Base.Roles.Config,
        }
 
+       if err := c.ConfiguredDimensions.Init(); err != nil {
+               return err
+       }
+
        intSetsCfg := sitesmatrix.IntSetsConfig{
                ApplyDefaults: sitesmatrix.IntSetsConfigApplyDefaultsIfNotSet,
        }
index 7396ec5e465a227c80a479460720500f8c12f67c..60d06704d62f222b98a5d9f5e44114b5909fc0f4 100644 (file)
@@ -575,7 +575,7 @@ func (a *allPagesAssembler) doCreatePages(prefix string) error {
                                func() error {
                                        if i := len(missingVectorsForHomeOrRootSection); i > 0 {
                                                // Pick one, the rest will be created later.
-                                               vec := missingVectorsForHomeOrRootSection.Sample()
+                                               vec := missingVectorsForHomeOrRootSection.VectorSample()
 
                                                kind := kinds.KindSection
                                                if s == "" {
index 46f59a43ffcd76aaf988b343b8997ec2fb768f03..220ba630e8ddfd6012a7917f91a302dd7e0029b9 100644 (file)
@@ -426,12 +426,25 @@ func (n contentNodesMap) sample() contentNode {
 }
 
 func (n contentNodesMap) siteVectors() sitesmatrix.VectorIterator {
-       return sitesmatrix.VectorIteratorFunc(func(yield func(v sitesmatrix.Vector) bool) bool {
-               for k := range n {
-                       if !yield(k) {
-                               return false
-                       }
+       return n
+}
+
+func (n contentNodesMap) ForEachVector(yield func(v sitesmatrix.Vector) bool) bool {
+       for v := range n {
+               if !yield(v) {
+                       return false
                }
-               return true
-       })
+       }
+       return true
+}
+
+func (n contentNodesMap) LenVectors() int {
+       return len(n)
+}
+
+func (n contentNodesMap) VectorSample() sitesmatrix.Vector {
+       for v := range n {
+               return v
+       }
+       panic("no vectors")
 }
index 6b1b5d3f6edbf74d42a5984207521067dbf41ca9..2aaa913c5a356b4a7f3bc36134800d7113efd8d8 100644 (file)
@@ -76,6 +76,10 @@ type RolesInternal struct {
        Sorted      []RoleInternal
 }
 
+func (r RolesInternal) Len() int {
+       return len(r.Sorted)
+}
+
 func (r RolesInternal) IndexDefault() int {
        for i, role := range r.Sorted {
                if role.Default {
index 6bae609c97c000ef0151eed974920514fe210757..2dbc745c0850135222a4c6c448f63d60c008597f 100644 (file)
@@ -76,9 +76,13 @@ func (v1 Vector) HasVector(v2 Vector) bool {
 }
 
 func (v1 Vector) HasAnyVector(vp VectorProvider) bool {
-       if vp.LenVectors() == 0 {
+       n := vp.LenVectors()
+       if n == 0 {
                return false
        }
+       if n == 1 {
+               return v1 == vp.VectorSample()
+       }
 
        return !vp.ForEachVector(func(v2 Vector) bool {
                if v1 == v2 {
@@ -144,12 +148,16 @@ func (vs Vectors) ForEachVector(yield func(v Vector) bool) bool {
        return true
 }
 
+func (vs Vectors) LenVectors() int {
+       return len(vs)
+}
+
 func (vs Vectors) ToVectorStore() VectorStore {
        return newVectorStoreMapFromVectors(vs)
 }
 
-// Sample returns one of the vectors in the set.
-func (vs Vectors) Sample() Vector {
+// VectorSample returns one of the vectors in the set.
+func (vs Vectors) VectorSample() Vector {
        for v := range vs {
                return v
        }
@@ -161,14 +169,15 @@ type (
                // ForEachVector iterates over all vectors in the provider.
                // It returns false if the iteration was stopped early.
                ForEachVector(func(v Vector) bool) bool
-       }
-)
 
-type VectorIteratorFunc func(func(v Vector) bool) bool
+               // LenVectors returns the number of vectors in the provider.
+               LenVectors() int
 
-func (f VectorIteratorFunc) ForEachVector(yield func(v Vector) bool) bool {
-       return f(yield)
-}
+               // VectorSample returns one of the vectors in the provider, usually the first or the only one.
+               // This will panic if the provider is empty.
+               VectorSample() Vector
+       }
+)
 
 // Bools holds boolean values for each dimension in the Hugo build matrix.
 type Bools [3]bool
@@ -198,13 +207,6 @@ type VectorProvider interface {
        // HasAnyVector returns true if any of the vectors in the provider matches any of the vectors in v.
        HasAnyVector(v VectorProvider) bool
 
-       // LenVectors returns the number of vectors in the provider.
-       LenVectors() int
-
-       // VectorSample returns one of the vectors in the provider, usually the first or the only one.
-       // This will panic if the provider is empty.
-       VectorSample() Vector
-
        // Equals returns true if this provider is equal to the other provider.
        EqualsVector(other VectorProvider) bool
 }
@@ -227,6 +229,22 @@ type ToVectorStoreProvider interface {
        ToVectorStore() VectorStore
 }
 
+func VectorIteratorToStore(vi VectorIterator) VectorStore {
+       switch v := vi.(type) {
+       case VectorStore:
+               return v
+       case ToVectorStoreProvider:
+               return v.ToVectorStore()
+       }
+
+       vectors := make(Vectors)
+       vi.ForEachVector(func(v Vector) bool {
+               vectors[v] = struct{}{}
+               return true
+       })
+       return vectors.ToVectorStore()
+}
+
 type weightedVectorStore struct {
        VectorStore
        weight int
index 4f51be7a3d9480f76b35c6e853b03cc5446a39e6..015efb6413fccc2f302ac1bedd307e24f2284fed 100644 (file)
@@ -71,10 +71,6 @@ func (s *vectorStoreMap) setVector(vec Vector) {
        s.sets[vec] = struct{}{}
 }
 
-func (s *vectorStoreMap) Ordinal() int {
-       return 0
-}
-
 func (s *vectorStoreMap) KeysSorted() ([]int, []int, []int) {
        var k0, k1, k2 []int
        for v := range s.sets {
@@ -271,12 +267,45 @@ type ConfiguredDimension interface {
        ResolveIndex(string) int
        ResolveName(int) string
        ForEachIndex() iter.Seq[int]
+       Len() int
 }
 
+// ConfiguredDimensions holds the configured dimensions for the site matrix.
 type ConfiguredDimensions struct {
        ConfiguredLanguages ConfiguredDimension
        ConfiguredVersions  ConfiguredDimension
        ConfiguredRoles     ConfiguredDimension
+       CommonSitesMatrix   CommonSitestMatrix
+
+       singleVectorStoreCache *maps.Cache[Vector, *IntSets]
+}
+
+func (c *ConfiguredDimensions) IsSingleVector() bool {
+       return c.ConfiguredLanguages.Len() == 1 && c.ConfiguredRoles.Len() == 1 && c.ConfiguredVersions.Len() == 1
+}
+
+// GetOrCreateSingleVectorStore returns a VectorStore for the given vector.
+func (c *ConfiguredDimensions) GetOrCreateSingleVectorStore(vec Vector) *IntSets {
+       store, _ := c.singleVectorStoreCache.GetOrCreate(vec, func() (*IntSets, error) {
+               is := &IntSets{}
+               is.setValuesInNilSets(vec, true, true, true)
+               return is, nil
+       })
+       return store
+}
+
+func (c *ConfiguredDimensions) Init() error {
+       c.singleVectorStoreCache = maps.NewCache[Vector, *IntSets]()
+       b := NewIntSetsBuilder(c).WithDefaultsIfNotSet().Build()
+       defaultVec := b.VectorSample()
+       c.singleVectorStoreCache.Set(defaultVec, b)
+       c.CommonSitesMatrix.DefaultSite = b
+
+       return nil
+}
+
+type CommonSitestMatrix struct {
+       DefaultSite VectorStore
 }
 
 func (c *ConfiguredDimensions) ResolveNames(v Vector) types.Strings3 {
@@ -317,6 +346,8 @@ type IntSets struct {
        h *hashOnce
 }
 
+var NilStore *IntSets = nil
+
 type hashOnce struct {
        once sync.Once
        hash uint64
@@ -363,7 +394,7 @@ func (s *IntSets) Intersects(other *IntSets) bool {
 // Complement returns a new VectorStore that contains all vectors in s that are not in any of ss.
 func (s *IntSets) Complement(ss ...VectorProvider) VectorStore {
        if len(ss) == 0 || (len(ss) == 1 && ss[0] == s) {
-               return nil
+               return NilStore
        }
 
        for _, v := range ss {
@@ -372,8 +403,7 @@ func (s *IntSets) Complement(ss ...VectorProvider) VectorStore {
                        continue
                }
                if vv.IsSuperSet(s) {
-                       var s *IntSets
-                       return s
+                       return NilStore
                }
        }
 
@@ -484,6 +514,9 @@ func (s *IntSets) HasLanguage(lang int) bool {
        return s.languages.Has(lang)
 }
 
+// LenVectors returns the total number of vectors represented by the IntSets.
+// This is the Cartesian product of the lengths of the individual sets.
+// This will be 0 if s is nil or any of the sets is empty.
 func (s *IntSets) LenVectors() int {
        if s == nil {
                return 0
@@ -526,6 +559,10 @@ func (s *IntSets) HasAnyVector(v VectorProvider) bool {
        if s.LenVectors() == 0 || v.LenVectors() == 0 {
                return false
        }
+       if v.LenVectors() == 1 {
+               // Fast path.
+               return s.HasVector(v.VectorSample())
+       }
 
        if vs, ok := v.(*IntSets); ok {
                // Fast path.
@@ -688,6 +725,14 @@ type IntSetsBuilder struct {
 
 func (b *IntSetsBuilder) Build() *IntSets {
        b.s.init()
+
+       if b.s.LenVectors() == 1 {
+               // Cache it or use the existing cached version, which will allow b.s to be GCed.
+               bb, _ := b.cfg.singleVectorStoreCache.GetOrCreate(b.s.VectorSample(), func() (*IntSets, error) {
+                       return b.s, nil
+               })
+               return bb
+       }
        return b.s
 }
 
@@ -889,6 +934,10 @@ type testDimension struct {
        names []string
 }
 
+func (m testDimension) Len() int {
+       return len(m.names)
+}
+
 func (m testDimension) IndexDefault() int {
        return 0
 }
@@ -933,9 +982,13 @@ func (m *testDimension) IndexMatch(match predicate.P[string]) (iter.Seq[int], er
 
 // NewTestingDimensions creates a new ConfiguredDimensions for testing.
 func NewTestingDimensions(languages, versions, roles []string) *ConfiguredDimensions {
-       return &ConfiguredDimensions{
+       c := &ConfiguredDimensions{
                ConfiguredLanguages: &testDimension{names: languages},
                ConfiguredVersions:  &testDimension{names: versions},
                ConfiguredRoles:     &testDimension{names: roles},
        }
+       if err := c.Init(); err != nil {
+               panic(err)
+       }
+       return c
 }
index 1365c90556fde5005734a227d7ce9142983a63c1..45015afdd8ad1fb4ac7a7083c9f8c19d588c559a 100644 (file)
 package sitesmatrix_test
 
 import (
-       "fmt"
        "testing"
 
-       "github.com/bits-and-blooms/bitset"
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/common/hashing"
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/hugolib/sitesmatrix"
 )
 
-var testDims = sitesmatrix.NewTestingDimensions([]string{"en", "no"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
+func newTestDims() *sitesmatrix.ConfiguredDimensions {
+       return sitesmatrix.NewTestingDimensions([]string{"en", "no"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
+}
 
 func TestIntSets(t *testing.T) {
        c := qt.New(t)
+       testDims := newTestDims()
 
        sets := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
                maps.NewOrderedIntSet(1, 2),
@@ -103,31 +104,6 @@ func TestIntSets(t *testing.T) {
        c.Assert(allCount, qt.Equals, 18)
 }
 
-func TestBitSetExperiments(t *testing.T) {
-       c := qt.New(t)
-
-       a, b, d := bitset.New(10), bitset.New(10), bitset.New(10)
-
-       ints := func(v *bitset.BitSet) []uint {
-               var ints []uint
-               for i := range v.EachSet() {
-                       ints = append(ints, i)
-               }
-               return ints
-       }
-
-       a.Set(3).Set(4)
-       b.Set(4).Set(5).Set(6).Set(7)
-       d.Set(3).Set(4)
-
-       c.Assert(b.Test(5), qt.Equals, true)
-
-       fmt.Println("a:", ints(a), "==>", ints(b), "=> Intersection:", ints(a.Intersection(b)), "=> Symdiff:", ints(a.SymmetricDifference(b)))
-       fmt.Println("===>", ints(a.Difference(b)))
-       fmt.Println("===>", ints(b.Difference(a)))
-       fmt.Println("===> d", d.Difference(a).Count())
-}
-
 func TestIntSetsComplement(t *testing.T) {
        c := qt.New(t)
 
@@ -147,6 +123,8 @@ func TestIntSetsComplement(t *testing.T) {
        runOne := func(c *qt.C, test test) {
                c.Helper()
 
+               testDims := newTestDims()
+
                self := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
                        maps.NewOrderedIntSet(test.left.v0...),
                        maps.NewOrderedIntSet(test.left.v1...),
@@ -281,6 +259,8 @@ func TestIntSetsComplement(t *testing.T) {
 func TestIntSetsComplementOfComplement(t *testing.T) {
        c := qt.New(t)
 
+       testDims := newTestDims()
+
        sets1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
                maps.NewOrderedIntSet(1),
                maps.NewOrderedIntSet(1),
@@ -313,6 +293,8 @@ func TestIntSetsComplementOfComplement(t *testing.T) {
 }
 
 func BenchmarkIntSetsComplement(b *testing.B) {
+       testDims := newTestDims()
+
        sets1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
                maps.NewOrderedIntSet(1, 2, 3),
                maps.NewOrderedIntSet(1, 2, 3),
@@ -376,6 +358,8 @@ func BenchmarkIntSetsComplement(b *testing.B) {
 }
 
 func BenchmarkSets(b *testing.B) {
+       testDims := newTestDims()
+
        sets1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
                maps.NewOrderedIntSet(1, 2),
                maps.NewOrderedIntSet(1, 2, 3),
@@ -485,6 +469,7 @@ func BenchmarkSets(b *testing.B) {
 
 func TestVectorStoreMap(t *testing.T) {
        c := qt.New(t)
+       testDims := newTestDims()
 
        c.Run("Complement", func(c *qt.C) {
                v1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
@@ -509,3 +494,18 @@ func TestVectorStoreMap(t *testing.T) {
                })
        })
 }
+
+func BenchmarkHasAnyVectorSingle(b *testing.B) {
+       testDims := newTestDims()
+       set := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
+               maps.NewOrderedIntSet(0),
+               maps.NewOrderedIntSet(0),
+               maps.NewOrderedIntSet(0),
+       ).Build()
+
+       v := sitesmatrix.Vector{0, 0, 0}
+
+       for b.Loop() {
+               _ = set.HasAnyVector(v)
+       }
+}
index acb71d63ad91007878d408886f87a401cd6e6cec..18b7ec91a442f00dd82cebdf878ce81f5b12713f 100644 (file)
@@ -78,6 +78,10 @@ type VersionsInternal struct {
        Sorted []VersionInternal
 }
 
+func (r VersionsInternal) Len() int {
+       return len(r.Sorted)
+}
+
 func (r VersionsInternal) IndexDefault() int {
        for i, version := range r.Sorted {
                if version.Default {
index b6bc0ffead028fe103df41a605aa3ddd6c2d4327..bde2c2f112fbe5b41842997354073396327459c6 100644 (file)
@@ -92,6 +92,10 @@ func (ls LanguagesInternal) ResolveIndex(name string) int {
        panic(fmt.Sprintf("no language found for name %q", name))
 }
 
+func (ls LanguagesInternal) Len() int {
+       return len(ls.Sorted)
+}
+
 // IndexMatch returns an iterator for the roles that match the filter.
 func (ls LanguagesInternal) IndexMatch(match predicate.P[string]) (iter.Seq[int], error) {
        return func(yield func(i int) bool) {
index aa299d0d1a14b15dae677c536c7d4aed4eec64f1..d90ae074c84da438a10adfd323fd57eb27b3c7c1 100644 (file)
@@ -245,7 +245,13 @@ func buildSitesComplementsFromSitesConfig(
        conf config.AllProvider,
        fim *hugofs.FileMeta,
        sitesConfig sitesmatrix.Sites,
-) *sitesmatrix.IntSets {
+) sitesmatrix.VectorStore {
+       if sitesConfig.Complements.IsZero() {
+               if fim != nil && fim.SitesComplements != nil {
+                       return fim.SitesComplements
+               }
+               return sitesmatrix.NilStore
+       }
        intsetsCfg := sitesmatrix.IntSetsConfig{
                Globs: sitesConfig.Complements,
        }
@@ -259,27 +265,22 @@ func buildSitesComplementsFromSitesConfig(
        return sitesComplements.Build()
 }
 
-func buildSitesMatrixFromVectorIterator(
-       sitesMatrix sitesmatrix.VectorIterator,
-) sitesmatrix.VectorStore {
-       switch v := sitesMatrix.(type) {
-       case sitesmatrix.VectorStore:
-               return v
-       case sitesmatrix.ToVectorStoreProvider:
-               return v.ToVectorStore()
-       default:
-               if v == nil {
-                       return nil
-               }
-               panic(fmt.Sprintf("unsupported type %T", v))
-       }
-}
-
 func buildSitesMatrixFromSitesConfig(
        conf config.AllProvider,
        sitesMatrixBase sitesmatrix.VectorIterator,
        sitesConfig sitesmatrix.Sites,
-) *sitesmatrix.IntSets {
+) sitesmatrix.VectorStore {
+       if sitesConfig.Matrix.IsZero() && sitesMatrixBase != nil {
+               if sitesMatrixBase.LenVectors() == 1 {
+                       return conf.ConfiguredDimensions().GetOrCreateSingleVectorStore(sitesMatrixBase.VectorSample())
+               }
+               return sitesmatrix.VectorIteratorToStore(sitesMatrixBase)
+       }
+
+       if conf.ConfiguredDimensions().IsSingleVector() {
+               return conf.ConfiguredDimensions().CommonSitesMatrix.DefaultSite
+       }
+
        intsetsCfg := sitesmatrix.IntSetsConfig{
                Globs: sitesConfig.Matrix,
        }
@@ -317,7 +318,7 @@ func (p *PageConfigEarly) CompileEarly(pi *paths.Path, cascades *page.PageMatche
        }
 
        if sitesMatrixBaseOnly {
-               p.SitesMatrix = buildSitesMatrixFromVectorIterator(sitesMatrixBase)
+               p.SitesMatrix = sitesmatrix.VectorIteratorToStore(sitesMatrixBase)
        } else {
                p.SitesMatrix = buildSitesMatrixFromSitesConfig(
                        conf,