Files
hugo/tpl/tplimpl/templates.go
T
Bjørn Erik Pedersen 555dfa207a Speedup and simplify page assembly for deeper content trees
This commit moves to a forked version go-radix (fork source has not had any updates in 3 years.), whith 2 notable changes:

* It's generic (using Go generics) and thus removes a lot of type conversions/assertions.
* It allows nodes to be replaced during walk, which allows to partition the tree for parallel processing without worrying about locking.

For this repo, this means:

* The assembly step now processes nested sections in parallel, which gives a speedup for deep content trees with a slight allocation penalty (see benchmarks below).
* Nodes that needs to be reinserted are inserted directly.
* Also, there are some drive-by fixes of some allocation issues, e.g. avoid wrapping mutexes in returned anonomous functions, a common source of hidden allocations.

```
                                                                                   │ master.bench │           perf-p3.bench            │
                                                                                   │    sec/op    │   sec/op     vs base               │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10     6.958m ± 3%   7.015m ± 3%        ~ (p=0.589 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10    14.25m ± 1%   14.56m ± 8%        ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10    48.07m ± 3%   49.23m ± 3%        ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10    66.66m ± 4%   66.47m ± 6%        ~ (p=0.485 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10    59.57m ± 4%   50.73m ± 5%  -14.85% (p=0.002 n=6)
geomean                                                                               28.54m        27.92m        -2.18%

                                                                                   │ master.bench │           perf-p3.bench            │
                                                                                   │     B/op     │     B/op      vs base              │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10    4.513Mi ± 0%   4.527Mi ± 0%  +0.33% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10   15.35Mi ± 0%   15.49Mi ± 0%  +0.94% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10   62.50Mi ± 0%   63.19Mi ± 0%  +1.10% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10   86.78Mi ± 0%   87.73Mi ± 0%  +1.09% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10   62.96Mi ± 0%   63.66Mi ± 0%  +1.12% (p=0.002 n=6)
geomean                                                                              29.84Mi        30.11Mi       +0.92%

                                                                                   │ master.bench │           perf-p3.bench           │
                                                                                   │  allocs/op   │  allocs/op   vs base              │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10     60.44k ± 0%   60.97k ± 0%  +0.87% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10    205.8k ± 0%   211.4k ± 0%  +2.70% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10    831.1k ± 0%   858.3k ± 0%  +3.27% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10    1.157M ± 0%   1.197M ± 0%  +3.41% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10    839.9k ± 0%   867.8k ± 0%  +3.31% (p=0.002 n=6)
geomean                                                                               398.5k        409.3k       +2.71%
```
2025-11-25 11:37:05 +01:00

311 lines
7.3 KiB
Go

package tplimpl
import (
"io"
"iter"
"regexp"
"strconv"
"strings"
"sync/atomic"
"unicode"
"unicode/utf8"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/tpl"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
if err := func() error {
meta := templ.Fi.Meta()
f, err := meta.Open()
if err != nil {
return err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return err
}
templ.content = removeLeadingBOM(string(b))
if !templ.noBaseOf {
templ.noBaseOf = !needsBaseTemplate(templ.content)
}
return nil
}(); err != nil {
return err
}
return nil
}
// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
// in v0.141.0. We can remove these aliases in v0.155.0 or later.
var embeddedTemplatesAliases = map[string][]string{
"_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
}
func (s *TemplateStore) parseTemplate(ti *TemplInfo, replace bool) error {
err := s.tns.doParseTemplate(ti, replace)
if err != nil {
return s.addFileContext(ti, "parse of template failed", err)
}
return err
}
func (t *templateNamespace) doParseTemplate(ti *TemplInfo, replace bool) error {
if !ti.noBaseOf || ti.category == CategoryBaseof {
// Delay parsing until we have the base template.
return nil
}
pi := ti.PathInfo
name := pi.PathNoLeadingSlash()
var (
templ tpl.Template
err error
)
if ti.D.IsPlainText {
prototype := t.parseText
if !replace && prototype.Lookup(name) != nil {
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
}
templ, err = prototype.New(name).Parse(ti.content)
if err != nil {
return err
}
} else {
prototype := t.parseHTML
if !replace && prototype.Lookup(name) != nil {
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
}
templ, err = prototype.New(name).Parse(ti.content)
if err != nil {
return err
}
if ti.subCategory == SubCategoryEmbedded {
// In Hugo 0.146.0 we moved the internal templates around.
// For the "_internal/twitter_cards.html" style templates, they
// were moved to the _partials directory.
// But we need to make them accessible from the old path for a while.
if pi.Type() == paths.TypePartial {
aliasName := strings.TrimPrefix(name, "_partials/")
aliasName = "_internal/" + aliasName
_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
if err != nil {
return err
}
}
// This was also possible before Hugo 0.146.0, but this should be deprecated.
if pi.Type() == paths.TypeShortcode {
aliasName := strings.TrimPrefix(name, "_shortcodes/")
aliasName = "_internal/shortcodes/" + aliasName
_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
if err != nil {
return err
}
}
}
// Issue #13599.
if ti.category == CategoryPartial && ti.Fi != nil && ti.Fi.Meta().PathInfo.Section() == "partials" {
aliasName := strings.TrimPrefix(name, "_")
if _, err := prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree); err != nil {
return err
}
}
}
ti.Template = templ
return nil
}
func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTemplateInfo) error {
tb := &TemplWithBaseApplied{
Overlay: overlay,
Base: base.Info,
}
base.Info.overlays = append(base.Info.overlays, overlay)
var templ tpl.Template
if overlay.D.IsPlainText {
tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
var err error
tt, err = tt.Parse(base.Info.content)
if err != nil {
return err
}
tt, err = tt.Parse(overlay.content)
if err != nil {
return err
}
templ = tt
t.baseofTextClones = append(t.baseofTextClones, tt)
} else {
tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
var err error
tt, err = tt.Parse(base.Info.content)
if err != nil {
return err
}
tt, err = tt.Parse(overlay.content)
if err != nil {
return err
}
templ = tt
t.baseofHtmlClones = append(t.baseofHtmlClones, tt)
}
tb.Template = &TemplInfo{
Template: templ,
base: base.Info,
PathInfo: overlay.PathInfo,
Fi: overlay.Fi,
D: overlay.D,
matrix: overlay.matrix,
noBaseOf: true,
}
variants := overlay.baseVariants.Get(base.Key)
if variants == nil {
variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
overlay.baseVariants.Insert(base.Key, variants)
}
variants[base.Info.D] = tb
return nil
}
func (t *templateNamespace) templatesIn(in tpl.Template) iter.Seq[tpl.Template] {
return func(yield func(t tpl.Template) bool) {
switch in := in.(type) {
case *htmltemplate.Template:
for t := range in.All() {
if !yield(t) {
return
}
}
case *texttemplate.Template:
for t := range in.All() {
if !yield(t) {
return
}
}
}
}
}
var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
// needsBaseTemplate returns true if the first non-comment template block is a
// define block.
func needsBaseTemplate(templ string) bool {
idx := -1
inComment := false
for i := 0; i < len(templ); {
if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
inComment = true
i += 4
} else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
inComment = true
i += 6
} else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
inComment = false
i += 4
} else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
inComment = false
i += 6
} else {
r, size := utf8.DecodeRuneInString(templ[i:])
if !inComment {
if strings.HasPrefix(templ[i:], "{{") {
idx = i
break
} else if !unicode.IsSpace(r) {
break
}
}
i += size
}
}
if idx == -1 {
return false
}
return baseTemplateDefineRe.MatchString(templ[idx:])
}
func removeLeadingBOM(s string) string {
const bom = '\ufeff'
for i, r := range s {
if i == 0 && r != bom {
return s
}
if i > 0 {
return s[i:]
}
}
return s
}
type templateNamespace struct {
parseText *texttemplate.Template
parseHTML *htmltemplate.Template
prototypeText *texttemplate.Template
prototypeHTML *htmltemplate.Template
nameCounter atomic.Uint64
standaloneText *texttemplate.Template
baseofTextClones []*texttemplate.Template
baseofHtmlClones []*htmltemplate.Template
}
func (t *templateNamespace) createPrototypesParse() error {
if t.prototypeHTML == nil {
panic("prototypeHTML not set")
}
t.parseHTML = htmltemplate.Must(t.prototypeHTML.Clone())
t.parseText = texttemplate.Must(t.prototypeText.Clone())
return nil
}
func (t *templateNamespace) createPrototypes(init bool) error {
if init {
t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
t.prototypeText = texttemplate.Must(t.parseText.Clone())
}
return nil
}
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
return &templateNamespace{
parseHTML: htmltemplate.New("").Funcs(funcs),
parseText: texttemplate.New("").Funcs(funcs),
standaloneText: texttemplate.New("").Funcs(funcs),
}
}
func isText(t tpl.Template) bool {
switch t.(type) {
case *texttemplate.Template:
return true
case *htmltemplate.Template:
return false
default:
panic("unknown template type")
}
}