mirror of
https://github.com/gohugoio/hugo.git
synced 2025-12-13 20:36:04 +01:00
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%
```
369 lines
8.8 KiB
Go
369 lines
8.8 KiB
Go
// Copyright 2019 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 (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/bep/logg"
|
|
"github.com/gohugoio/go-radix"
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
"github.com/gohugoio/hugo/hugolib/doctree"
|
|
"github.com/gohugoio/hugo/tpl/tplimpl"
|
|
|
|
"github.com/gohugoio/hugo/config"
|
|
|
|
"github.com/gohugoio/hugo/resources/kinds"
|
|
"github.com/gohugoio/hugo/resources/page"
|
|
)
|
|
|
|
type siteRenderContext struct {
|
|
cfg *BuildCfg
|
|
|
|
infol logg.LevelLogger
|
|
|
|
// languageIdx is the zero based index of the site.
|
|
languageIdx int
|
|
|
|
// Zero based index for all output formats combined.
|
|
sitesOutIdx int
|
|
|
|
// Zero based index of the output formats configured within a Site.
|
|
// Note that these outputs are sorted.
|
|
outIdx int
|
|
|
|
multihost bool
|
|
}
|
|
|
|
// Whether to render 404.html, robotsTXT.txt and similar.
|
|
// These are usually rendered once in the root of public.
|
|
func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool {
|
|
if s.multihost || kind == kinds.KindSitemap {
|
|
// 1 per site
|
|
return s.outIdx == 0
|
|
}
|
|
|
|
if kind == kinds.KindTemporary || kind == kinds.KindStatus404 {
|
|
// 1 for all output formats
|
|
return s.outIdx == 0
|
|
}
|
|
|
|
// 1 for all sites and output formats.
|
|
return s.languageIdx == 0 && s.outIdx == 0
|
|
}
|
|
|
|
// renderPages renders pages concurrently.
|
|
func (s *Site) renderPages(ctx *siteRenderContext) error {
|
|
numWorkers := config.GetNumWorkerMultiplier()
|
|
|
|
results := make(chan error)
|
|
pages := make(chan *pageState, numWorkers) // buffered for performance
|
|
errs := make(chan error)
|
|
|
|
go s.errorCollator(results, errs)
|
|
|
|
wg := &sync.WaitGroup{}
|
|
|
|
for range numWorkers {
|
|
wg.Add(1)
|
|
go pageRenderer(ctx, s, pages, results, wg)
|
|
}
|
|
|
|
cfg := ctx.cfg
|
|
|
|
w := &doctree.NodeShiftTreeWalker[contentNode]{
|
|
Tree: s.pageMap.treePages,
|
|
Handle: func(key string, n contentNode) (radix.WalkFlag, error) {
|
|
if p, ok := n.(*pageState); ok {
|
|
if cfg.shouldRender(ctx.infol, p) {
|
|
select {
|
|
case <-s.h.Done():
|
|
return radix.WalkStop, nil
|
|
default:
|
|
pages <- p
|
|
}
|
|
}
|
|
}
|
|
return radix.WalkContinue, nil
|
|
},
|
|
}
|
|
|
|
if err := w.Walk(context.Background()); err != nil {
|
|
return err
|
|
}
|
|
|
|
close(pages)
|
|
|
|
wg.Wait()
|
|
|
|
close(results)
|
|
|
|
err := <-errs
|
|
if err != nil {
|
|
return fmt.Errorf("%v failed to render pages: %w", s.resolveDimensionNames(), herrors.ImproveRenderErr(err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func pageRenderer(
|
|
ctx *siteRenderContext,
|
|
s *Site,
|
|
pages <-chan *pageState,
|
|
results chan<- error,
|
|
wg *sync.WaitGroup,
|
|
) {
|
|
defer wg.Done()
|
|
|
|
for p := range pages {
|
|
|
|
if p.m.isStandalone() && !ctx.shouldRenderStandalonePage(p.Kind()) {
|
|
continue
|
|
}
|
|
|
|
if p.m.pageConfig.Build.PublishResources {
|
|
if err := p.renderResources(); err != nil {
|
|
s.SendError(p.errorf(err, "failed to render page resources"))
|
|
continue
|
|
}
|
|
}
|
|
|
|
if !p.render {
|
|
// Nothing more to do for this page.
|
|
continue
|
|
}
|
|
|
|
templ, found, err := p.resolveTemplate()
|
|
if err != nil {
|
|
s.SendError(p.errorf(err, "failed to resolve template"))
|
|
continue
|
|
}
|
|
|
|
if !found {
|
|
s.Log.Trace(
|
|
func() string {
|
|
return fmt.Sprintf("no layout for kind %q found", p.Kind())
|
|
},
|
|
)
|
|
// Don't emit warning for missing 404 etc. pages.
|
|
if !p.m.isStandalone() {
|
|
s.logMissingLayout("", p.Layout(), p.Kind(), p.f.Name)
|
|
}
|
|
continue
|
|
}
|
|
|
|
targetPath := p.targetPaths().TargetFilename
|
|
|
|
s.Log.Trace(
|
|
func() string {
|
|
return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath)
|
|
},
|
|
)
|
|
|
|
var d any = p
|
|
switch p.Kind() {
|
|
case kinds.KindSitemapIndex:
|
|
d = s.h.Sites
|
|
}
|
|
|
|
if err := s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, targetPath, p, d, templ); err != nil {
|
|
results <- err
|
|
}
|
|
|
|
if p.paginator != nil && p.paginator.current != nil {
|
|
if err := s.renderPaginator(p, templ); err != nil {
|
|
results <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) {
|
|
log := s.Log.Warn()
|
|
if name != "" && infoOnMissingLayout[name] {
|
|
log = s.Log.Info()
|
|
}
|
|
|
|
errMsg := "You should create a template file which matches Hugo Layouts Lookup Rules for this combination."
|
|
var args []any
|
|
msg := "found no layout file for"
|
|
if outputFormat != "" {
|
|
msg += " %q"
|
|
args = append(args, outputFormat)
|
|
}
|
|
|
|
if layout != "" {
|
|
msg += " for layout %q"
|
|
args = append(args, layout)
|
|
}
|
|
|
|
if kind != "" {
|
|
msg += " for kind %q"
|
|
args = append(args, kind)
|
|
}
|
|
|
|
if name != "" {
|
|
msg += " for %q"
|
|
args = append(args, name)
|
|
}
|
|
|
|
msg += ": " + errMsg
|
|
|
|
log.Logf(msg, args...)
|
|
}
|
|
|
|
// renderPaginator must be run after the owning Page has been rendered.
|
|
func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
|
|
paginatePath := s.Conf.Pagination().Path
|
|
|
|
d := p.targetPathDescriptor
|
|
f := p.outputFormat()
|
|
d.Type = f
|
|
|
|
if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() {
|
|
panic(fmt.Sprintf("invalid paginator state for %q", p.pathOrTitle()))
|
|
}
|
|
|
|
if f.IsHTML && !s.Conf.Pagination().DisableAliases {
|
|
// Write alias for page 1
|
|
d.Addends = fmt.Sprintf("/%s/%d", paginatePath, 1)
|
|
targetPaths := page.CreateTargetPaths(d)
|
|
|
|
if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, p); err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
// Render pages for the rest
|
|
for current := p.paginator.current.Next(); current != nil; current = current.Next() {
|
|
|
|
p.paginator.current = current
|
|
d.Addends = fmt.Sprintf("/%s/%d", paginatePath, current.PageNumber())
|
|
targetPaths := page.CreateTargetPaths(d)
|
|
|
|
if err := s.renderAndWritePage(
|
|
&s.PathSpec.ProcessingStats.PaginatorPages,
|
|
targetPaths.TargetFilename, p, p, templ); err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// renderAliases renders shell pages that simply have a redirect in the header.
|
|
func (s *Site) renderAliases() error {
|
|
w := &doctree.NodeShiftTreeWalker[contentNode]{
|
|
Tree: s.pageMap.treePages,
|
|
Handle: func(key string, n contentNode) (radix.WalkFlag, error) {
|
|
p := n.(*pageState)
|
|
|
|
// We cannot alias a page that's not rendered.
|
|
if p.m.noLink() || p.skipRender() {
|
|
return radix.WalkContinue, nil
|
|
}
|
|
|
|
if len(p.Aliases()) == 0 {
|
|
return radix.WalkContinue, nil
|
|
}
|
|
|
|
pathSeen := make(map[string]bool)
|
|
for _, of := range p.OutputFormats() {
|
|
if !of.Format.IsHTML {
|
|
continue
|
|
}
|
|
|
|
f := of.Format
|
|
|
|
if pathSeen[f.Path] {
|
|
continue
|
|
}
|
|
pathSeen[f.Path] = true
|
|
|
|
plink := of.Permalink()
|
|
|
|
for _, a := range p.Aliases() {
|
|
isRelative := !strings.HasPrefix(a, "/")
|
|
|
|
if isRelative {
|
|
// Make alias relative, where "." will be on the
|
|
// same directory level as the current page.
|
|
basePath := path.Join(p.targetPaths().SubResourceBaseLink, "..")
|
|
a = path.Join(basePath, a)
|
|
|
|
} else {
|
|
// Make sure AMP and similar doesn't clash with regular aliases.
|
|
a = path.Join(f.Path, a)
|
|
}
|
|
|
|
if s.conf.C.IsUglyURLSection(p.Section()) && !strings.HasSuffix(a, ".html") {
|
|
a += ".html"
|
|
}
|
|
|
|
lang := p.Language().Lang
|
|
|
|
if s.h.Configs.IsMultihost && !strings.HasPrefix(a, "/"+lang) {
|
|
// These need to be in its language root.
|
|
a = path.Join(lang, a)
|
|
}
|
|
|
|
err := s.writeDestAlias(a, plink, f, p)
|
|
if err != nil {
|
|
return radix.WalkStop, err
|
|
}
|
|
}
|
|
}
|
|
return radix.WalkContinue, nil
|
|
},
|
|
}
|
|
return w.Walk(context.TODO())
|
|
}
|
|
|
|
// renderMainLanguageRedirect creates a redirect to the main language home,
|
|
// depending on if it lives in sub folder (e.g. /en) or not.
|
|
func (s *Site) renderMainLanguageRedirect() error {
|
|
if s.conf.DisableDefaultLanguageRedirect {
|
|
return nil
|
|
}
|
|
if s.h.Conf.IsMultihost() || !(s.h.Conf.DefaultContentLanguageInSubdir() || s.h.Conf.IsMultilingual()) {
|
|
// No need for a redirect
|
|
return nil
|
|
}
|
|
|
|
html, found := s.conf.OutputFormats.Config.GetByName("html")
|
|
if found {
|
|
mainLang := s.conf.DefaultContentLanguage
|
|
if s.conf.DefaultContentLanguageInSubdir {
|
|
mainLangURL := s.PathSpec.AbsURL(mainLang+"/", false)
|
|
s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL)
|
|
if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
mainLangURL := s.PathSpec.AbsURL("", false)
|
|
s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL)
|
|
if err := s.publishDestAlias(true, mainLang, mainLangURL, html, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|