mirror of
https://github.com/gohugoio/hugo.git
synced 2025-12-13 20:36:04 +01:00
Hugo's build process is roughly divided into three steps: 1. Process content (walk file system and insert source nodes into content tree) 2. Assemble content (assemble pages and resources according to sites matrix) 3. Render content In #13679 we consolidated the page creation logic into one place (the assemble step). This made it much simpler to reason about, but it lost us some performance esp. in big content trees. This commit re-introduces parallelization in the first step in the assemble step by handling each top level section in its own goroutine. This gives significant performance improvements for content trees with many sections. Compared to master: ``` AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10 19.26m ± ∞ ¹ 14.54m ± ∞ ¹ -24.52% (p=0.029 n=4) AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=2/pagesPerSection=100-10 19.74m ± ∞ ¹ 16.45m ± ∞ ¹ -16.71% (p=0.029 n=4) AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10 106.18m ± ∞ ¹ 71.23m ± ∞ ¹ -32.91% (p=0.029 n=4) AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10 38.85m ± ∞ ¹ 30.47m ± ∞ ¹ -21.59% (p=0.029 n=4) ```
163 lines
3.9 KiB
Go
163 lines
3.9 KiB
Go
// Copyright 2025 The Hugo Authors. All rights reserved.
|
|
//
|
|
// 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 hugolib
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/gohugoio/hugo/config"
|
|
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
)
|
|
|
|
type contentNodeShifter struct {
|
|
conf config.AllProvider // Used for logging/debugging.
|
|
}
|
|
|
|
func (s *contentNodeShifter) Delete(n contentNode, vec sitesmatrix.Vector) (contentNode, bool, bool) {
|
|
switch v := n.(type) {
|
|
case contentNodesMap:
|
|
deleted, wasDeleted := v[vec]
|
|
if wasDeleted {
|
|
delete(v, vec)
|
|
resource.MarkStale(deleted)
|
|
}
|
|
return deleted, wasDeleted, len(v) == 0
|
|
case contentNodeForSite:
|
|
if v.siteVector() != vec {
|
|
return nil, false, false
|
|
}
|
|
resource.MarkStale(v)
|
|
return v, true, true
|
|
default:
|
|
v = v.(contentNodeSingle) // Ensure single node.
|
|
resource.MarkStale(v)
|
|
return v, true, true
|
|
|
|
}
|
|
}
|
|
|
|
func (s *contentNodeShifter) DeleteFunc(v contentNode, f func(n contentNode) bool) bool {
|
|
switch ss := v.(type) {
|
|
case contentNodeSingle:
|
|
if f(ss) {
|
|
resource.MarkStale(ss)
|
|
return true
|
|
}
|
|
return false
|
|
case contentNodes:
|
|
for i, n := range ss {
|
|
if f(n) {
|
|
resource.MarkStale(n)
|
|
ss = append(ss[:i], ss[i+1:]...)
|
|
}
|
|
}
|
|
return len(ss) == 0
|
|
case contentNodesMap:
|
|
for k, n := range ss {
|
|
if f(n) {
|
|
resource.MarkStale(n)
|
|
delete(ss, k)
|
|
}
|
|
}
|
|
return len(ss) == 0
|
|
default:
|
|
panic(fmt.Sprintf("DeleteFunc: unknown type %T", v))
|
|
}
|
|
}
|
|
|
|
func (s *contentNodeShifter) ForEeachInAllDimensions(n contentNode, f func(contentNode) bool) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
if v, ok := n.(interface {
|
|
// Implemented by all the list nodes.
|
|
ForEeachInAllDimensions(f func(contentNode) bool)
|
|
}); ok {
|
|
v.ForEeachInAllDimensions(f)
|
|
return
|
|
}
|
|
f(n)
|
|
}
|
|
|
|
func (s *contentNodeShifter) ForEeachInDimension(n contentNode, vec sitesmatrix.Vector, d int, f func(contentNode) bool) {
|
|
LOOP1:
|
|
for vec2, v := range contentNodeToSeq2(n) {
|
|
for i, v := range vec2 {
|
|
if i != d && v != vec[i] {
|
|
continue LOOP1
|
|
}
|
|
}
|
|
if !f(v) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *contentNodeShifter) Insert(old, new contentNode) (contentNode, contentNode, bool) {
|
|
new = new.(contentNodeSingle) // Ensure single node.
|
|
|
|
switch vv := old.(type) {
|
|
case contentNodeSingle:
|
|
return contentNodes{vv, new}, old, false
|
|
case contentNodes:
|
|
s := make(contentNodes, 0, len(vv)+1)
|
|
s = append(s, vv...)
|
|
s = append(s, new)
|
|
return s, old, false
|
|
case contentNodesMap:
|
|
switch new := new.(type) {
|
|
case contentNodeForSite:
|
|
oldp := vv[new.siteVector()]
|
|
updated := oldp != new
|
|
if updated {
|
|
resource.MarkStale(oldp)
|
|
}
|
|
vv[new.siteVector()] = new
|
|
return vv, oldp, updated
|
|
default:
|
|
s := make(contentNodes, 0, len(vv)+1)
|
|
for _, v := range vv {
|
|
s = append(s, v)
|
|
}
|
|
s = append(s, new)
|
|
return s, vv, false
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("Insert: unknown type %T", old))
|
|
}
|
|
}
|
|
|
|
func (s *contentNodeShifter) Shift(n contentNode, siteVector sitesmatrix.Vector, fallback bool) (contentNode, bool) {
|
|
switch v := n.(type) {
|
|
case contentNodeLookupContentNode:
|
|
if vv := v.lookupContentNode(siteVector); vv != nil {
|
|
return vv, true
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("Shift: unknown type %T for %q", n, n.Path()))
|
|
}
|
|
|
|
if !fallback {
|
|
// Done
|
|
return nil, false
|
|
}
|
|
|
|
if vvv := cnh.findContentNodeForSiteVector(siteVector, fallback, contentNodeToSeq(n)); vvv != nil {
|
|
return vvv, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|