diff --git a/common/maps/cache.go b/common/maps/cache.go index 7304a7a6f..de1535994 100644 --- a/common/maps/cache.go +++ b/common/maps/cache.go @@ -160,7 +160,7 @@ func (c *Cache[K, T]) Len() int { func (c *Cache[K, T]) Reset() { c.Lock() - c.m = make(map[K]T) + clear(c.m) c.hasBeenInitialized = false c.Unlock() } diff --git a/hugolib/alias.go b/hugolib/alias.go index 0d182042a..3beee44db 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -51,7 +51,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err var templateDesc tplimpl.TemplateDescriptor var base string = "" if ps, ok := p.(*pageState); ok { - base, templateDesc = ps.getTemplateBasePathAndDescriptor() + base, templateDesc = ps.GetInternalTemplateBasePathAndDescriptor() } templateDesc.Layout = "" templateDesc.Kind = "" diff --git a/hugolib/page.go b/hugolib/page.go index 41519909b..b36b9712c 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -476,7 +476,8 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error { return nil } -func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) { +// Exported so it can be used in integration tests. +func (po *pageOutput) GetInternalTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) { p := po.p f := po.f base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type) @@ -491,7 +492,7 @@ func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.Templa } func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) { - dir, d := p.getTemplateBasePathAndDescriptor() + dir, d := p.GetInternalTemplateBasePathAndDescriptor() if len(layouts) > 0 { d.Layout = layouts[0] diff --git a/hugolib/page__common.go b/hugolib/page__common.go index 27d2c3089..f6f01bbe2 100644 --- a/hugolib/page__common.go +++ b/hugolib/page__common.go @@ -97,7 +97,7 @@ type pageCommon struct { pageMenus *pageMenus // Internal use - page.InternalDependencies + page.RelatedDocsHandlerProvider contentConverterInit sync.Once contentConverter converter.Converter diff --git a/hugolib/page__new.go b/hugolib/page__new.go index b8c3483ad..10336e255 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -209,11 +209,11 @@ func (h *HugoSites) doNewPage(m *pageMeta) (*pageState, *paths.Path, error) { ShortcodeInfoProvider: page.NopPage, LanguageProvider: m.s, - InternalDependencies: m.s, - init: lazy.New(), - m: m, - s: m.s, - sWrapped: page.WrapSite(m.s), + RelatedDocsHandlerProvider: m.s, + init: lazy.New(), + m: m, + s: m.s, + sWrapped: page.WrapSite(m.s), }, } diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 71bc51828..1f7f3411e 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -275,7 +275,7 @@ func (pco *pageContentOutput) initRenderHooks() error { // Inherit the descriptor from the page/current output format. // This allows for fine-grained control of the template used for // rendering of e.g. links. - base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor() + base, layoutDescriptor := pco.po.p.GetInternalTemplateBasePathAndDescriptor() switch tp { case hooks.LinkRendererType: diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 7a23b15d2..3ac0940e2 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -397,7 +397,7 @@ func doRenderShortcode( ofCount[match.D.OutputFormat]++ return true } - base, layoutDescriptor := po.getTemplateBasePathAndDescriptor() + base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor() q := tplimpl.TemplateQuery{ Path: base, Name: sc.name, diff --git a/resources/page/page.go b/resources/page/page.go index da7b6beec..cbcfad557 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -148,8 +148,8 @@ type InSectionPositioner interface { PrevInSection() Page } -// InternalDependencies is considered an internal interface. -type InternalDependencies interface { +// RelatedDocsHandlerProvider is considered an internal interface. +type RelatedDocsHandlerProvider interface { // GetInternalRelatedDocsHandler is for internal use only. GetInternalRelatedDocsHandler() *RelatedDocsHandler } diff --git a/resources/page/pages_related.go b/resources/page/pages_related.go index 9debda324..402ed905f 100644 --- a/resources/page/pages_related.go +++ b/resources/page/pages_related.go @@ -124,7 +124,7 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I return nil, nil } - d, ok := p[0].(InternalDependencies) + d, ok := p[0].(RelatedDocsHandlerProvider) if !ok { return nil, fmt.Errorf("invalid type %T in related search", p[0]) } diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go index fe3180660..f6a119e9b 100644 --- a/tpl/tplimpl/templatestore.go +++ b/tpl/tplimpl/templatestore.go @@ -97,16 +97,16 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) { panic("HTML output format not found") } s := &TemplateStore{ - opts: opts, - siteOpts: siteOpts, - optsOrig: opts, - siteOptsOrig: siteOpts, - htmlFormat: html, - storeSite: configureSiteStorage(siteOpts, opts.Watching), - treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](), - treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](), - templatesByPath: maps.NewCache[string, *TemplInfo](), - templateDescriptorByPath: maps.NewCache[string, PathTemplateDescriptor](), + opts: opts, + siteOpts: siteOpts, + optsOrig: opts, + siteOptsOrig: siteOpts, + htmlFormat: html, + storeSite: configureSiteStorage(siteOpts, opts.Watching), + treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](), + treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](), + templatesByPath: maps.NewCache[string, *TemplInfo](), + cacheLookupPartials: maps.NewCache[string, *TemplInfo](), // Note that the funcs passed below is just for name validation. tns: newTemplateNamespace(siteOpts.TemplateFuncs), @@ -400,10 +400,9 @@ type TemplateStore struct { siteOpts SiteOptions htmlFormat output.Format - treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] - treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] - templatesByPath *maps.Cache[string, *TemplInfo] - templateDescriptorByPath *maps.Cache[string, PathTemplateDescriptor] + treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] + treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] + templatesByPath *maps.Cache[string, *TemplInfo] dh descriptorHandler @@ -417,6 +416,9 @@ type TemplateStore struct { // For testing benchmarking. optsOrig StoreOptions siteOptsOrig SiteOptions + + // caches. These need to be refreshed when the templates are refreshed. + cacheLookupPartials *maps.Cache[string, *TemplInfo] } // NewFromOpts creates a new store with the same configuration as the original. @@ -540,15 +542,19 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo { } func (s *TemplateStore) LookupPartial(pth string) *TemplInfo { - d := s.templateDescriptorFromPath(pth) - desc := d.Desc - if desc.Layout != "" { - panic("shortcode template descriptor must not have a layout") - } - best := s.getBest() - defer s.putBest(best) - s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best) - return best.templ + ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) { + d := s.templateDescriptorFromPath(pth) + desc := d.Desc + if desc.Layout != "" { + panic("shortcode template descriptor must not have a layout") + } + best := s.getBest() + defer s.putBest(best) + s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best) + return best.templ, nil + }) + + return ti } func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo { @@ -619,8 +625,14 @@ func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer }) } +func (s *TemplateStore) clearCaches() { + s.cacheLookupPartials.Reset() +} + // RefreshFiles refreshes this store for the files matching the given predicate. func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error { + s.clearCaches() + if err := s.tns.createPrototypesParse(); err != nil { return err } @@ -1370,43 +1382,38 @@ type PathTemplateDescriptor struct { // templateDescriptorFromPath returns a template descriptor from the given path. // This is currently used in partial lookups only. func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor { - // Check cache first. - d, _ := s.templateDescriptorByPath.GetOrCreate(pth, func() (PathTemplateDescriptor, error) { - var ( - mt media.Type - of output.Format - ) + var ( + mt media.Type + of output.Format + ) - // Common cases. - dotCount := strings.Count(pth, ".") - if dotCount <= 1 { - if dotCount == 0 { - // Asume HTML. - of, mt = s.resolveOutputFormatAndOrMediaType("html", "") - } else { - pth = strings.TrimPrefix(pth, "/") - ext := path.Ext(pth) - pth = strings.TrimSuffix(pth, ext) - ext = ext[1:] - of, mt = s.resolveOutputFormatAndOrMediaType("", ext) - } + // Common cases. + dotCount := strings.Count(pth, ".") + if dotCount <= 1 { + if dotCount == 0 { + // Asume HTML. + of, mt = s.resolveOutputFormatAndOrMediaType("html", "") } else { - path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth) - pth = path.PathNoIdentifier() - of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext()) + pth = strings.TrimPrefix(pth, "/") + ext := path.Ext(pth) + pth = strings.TrimSuffix(pth, ext) + ext = ext[1:] + of, mt = s.resolveOutputFormatAndOrMediaType("", ext) } + } else { + path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth) + pth = path.PathNoIdentifier() + of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext()) + } - return PathTemplateDescriptor{ - Path: pth, - Desc: TemplateDescriptor{ - OutputFormat: of.Name, - MediaType: mt.Type, - IsPlainText: of.IsPlainText, - }, - }, nil - }) - - return d + return PathTemplateDescriptor{ + Path: pth, + Desc: TemplateDescriptor{ + OutputFormat: of.Name, + MediaType: mt.Type, + IsPlainText: of.IsPlainText, + }, + } } // resolveOutputFormatAndOrMediaType resolves the output format and/or media type diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go index 2f450e58d..2f3849562 100644 --- a/tpl/tplimpl/templatestore_integration_test.go +++ b/tpl/tplimpl/templatestore_integration_test.go @@ -8,6 +8,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/resources/kinds" + "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/tpl/tplimpl" ) @@ -849,7 +850,7 @@ func BenchmarkExecuteWithContext(b *testing.B) { disableKinds = ["taxonomy", "term", "home"] -- layouts/all.html -- {{ .Title }}| - {{ partial "p1.html" . }} +{{ partial "p1.html" . }} -- layouts/_partials/p1.html -- p1. {{ partial "p2.html" . }} @@ -878,6 +879,82 @@ p3 b.ResetTimer() for i := 0; i < b.N; i++ { err := store.ExecuteWithContext(context.Background(), ti, io.Discard, p) - bb.Assert(err, qt.IsNil) + if err != nil { + b.Fatal(err) + } } } + +func BenchmarkLookupPartial(b *testing.B) { + files := ` +-- hugo.toml -- +disableKinds = ["taxonomy", "term", "home"] +-- layouts/all.html -- +{{ .Title }}| +-- layouts/_partials/p1.html -- +-- layouts/_partials/p2.html -- +-- layouts/_partials/p2.json -- +-- layouts/_partials/p3.html -- +` + bb := hugolib.Test(b, files) + + store := bb.H.TemplateStore + + for i := 0; i < b.N; i++ { + fi := store.LookupPartial("p3.html") + if fi == nil { + b.Fatal("not found") + } + } +} + +// Implemented by pageOutput. +type getDescriptorProvider interface { + GetInternalTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) +} + +func BenchmarkLookupShortcode(b *testing.B) { + files := ` +-- hugo.toml -- +disableKinds = ["taxonomy", "term", "home"] +-- content/toplevelpage.md -- +-- content/a/b/c/nested.md -- +-- layouts/all.html -- +{{ .Title }}| +-- layouts/_shortcodes/s.html -- +s1. +-- layouts/_shortcodes/a/b/s.html -- +s2. + +` + bb := hugolib.Test(b, files) + store := bb.H.TemplateStore + + runOne := func(p page.Page) { + pth, desc := p.(getDescriptorProvider).GetInternalTemplateBasePathAndDescriptor() + q := tplimpl.TemplateQuery{ + Path: pth, + Name: "s", + Category: tplimpl.CategoryShortcode, + Desc: desc, + } + v := store.LookupShortcode(q) + if v == nil { + b.Fatal("not found") + } + } + + b.Run("toplevelpage", func(b *testing.B) { + toplevelpage, _ := bb.H.Sites[0].GetPage("/toplevelpage") + for i := 0; i < b.N; i++ { + runOne(toplevelpage) + } + }) + + b.Run("nestedpage", func(b *testing.B) { + toplevelpage, _ := bb.H.Sites[0].GetPage("/a/b/c/nested") + for i := 0; i < b.N; i++ { + runOne(toplevelpage) + } + }) +}