mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-26 21:51:02 +03:00
tpl: Add a partial lookup cache
```` │ stash.bench │ perf-v146.bench │ │ sec/op │ sec/op vs base │ LookupPartial-10 248.00n ± 0% 14.75n ± 2% -94.05% (p=0.002 n=6) │ stash.bench │ perf-v146.bench │ │ B/op │ B/op vs base │ LookupPartial-10 48.00 ± 0% 0.00 ± 0% -100.00% (p=0.002 n=6) │ stash.bench │ perf-v146.bench │ │ allocs/op │ allocs/op vs base │ LookupPartial-10 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.002 n=6) ``` THe speedup above assumes reuse of the same partials over and over again, which I think is not uncommon. This commits also adds some more lookup benchmarks. The current output of these on my MacBook looks decent: ``` BenchmarkLookupPagesLayout/Single_root-10 3031562 395.5 ns/op 0 B/op 0 allocs/op BenchmarkLookupPagesLayout/Single_sub_folder-10 2515915 480.9 ns/op 0 B/op 0 allocs/op BenchmarkLookupPartial-10 84808112 14.13 ns/op 0 B/op 0 allocs/op BenchmarkLookupShortcode/toplevelpage-10 8111779 148.2 ns/op 0 B/op 0 allocs/op BenchmarkLookupShortcode/nestedpage-10 8088183 148.6 ns/op 0 B/op 0 allocs/op ``` Note that in the above the partial lookups are cahced, the others not (they are harder to cache because of the page path). Closes #13571
This commit is contained in:
parent
18d2d2f985
commit
208a0de6c3
11 changed files with 158 additions and 73 deletions
|
@ -160,7 +160,7 @@ func (c *Cache[K, T]) Len() int {
|
||||||
|
|
||||||
func (c *Cache[K, T]) Reset() {
|
func (c *Cache[K, T]) Reset() {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
c.m = make(map[K]T)
|
clear(c.m)
|
||||||
c.hasBeenInitialized = false
|
c.hasBeenInitialized = false
|
||||||
c.Unlock()
|
c.Unlock()
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err
|
||||||
var templateDesc tplimpl.TemplateDescriptor
|
var templateDesc tplimpl.TemplateDescriptor
|
||||||
var base string = ""
|
var base string = ""
|
||||||
if ps, ok := p.(*pageState); ok {
|
if ps, ok := p.(*pageState); ok {
|
||||||
base, templateDesc = ps.getTemplateBasePathAndDescriptor()
|
base, templateDesc = ps.GetInternalTemplateBasePathAndDescriptor()
|
||||||
}
|
}
|
||||||
templateDesc.Layout = ""
|
templateDesc.Layout = ""
|
||||||
templateDesc.Kind = ""
|
templateDesc.Kind = ""
|
||||||
|
|
|
@ -476,7 +476,8 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
|
||||||
return nil
|
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
|
p := po.p
|
||||||
f := po.f
|
f := po.f
|
||||||
base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
|
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) {
|
func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
|
||||||
dir, d := p.getTemplateBasePathAndDescriptor()
|
dir, d := p.GetInternalTemplateBasePathAndDescriptor()
|
||||||
|
|
||||||
if len(layouts) > 0 {
|
if len(layouts) > 0 {
|
||||||
d.Layout = layouts[0]
|
d.Layout = layouts[0]
|
||||||
|
|
|
@ -97,7 +97,7 @@ type pageCommon struct {
|
||||||
pageMenus *pageMenus
|
pageMenus *pageMenus
|
||||||
|
|
||||||
// Internal use
|
// Internal use
|
||||||
page.InternalDependencies
|
page.RelatedDocsHandlerProvider
|
||||||
|
|
||||||
contentConverterInit sync.Once
|
contentConverterInit sync.Once
|
||||||
contentConverter converter.Converter
|
contentConverter converter.Converter
|
||||||
|
|
|
@ -209,11 +209,11 @@ func (h *HugoSites) doNewPage(m *pageMeta) (*pageState, *paths.Path, error) {
|
||||||
ShortcodeInfoProvider: page.NopPage,
|
ShortcodeInfoProvider: page.NopPage,
|
||||||
LanguageProvider: m.s,
|
LanguageProvider: m.s,
|
||||||
|
|
||||||
InternalDependencies: m.s,
|
RelatedDocsHandlerProvider: m.s,
|
||||||
init: lazy.New(),
|
init: lazy.New(),
|
||||||
m: m,
|
m: m,
|
||||||
s: m.s,
|
s: m.s,
|
||||||
sWrapped: page.WrapSite(m.s),
|
sWrapped: page.WrapSite(m.s),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -275,7 +275,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
|
||||||
// Inherit the descriptor from the page/current output format.
|
// Inherit the descriptor from the page/current output format.
|
||||||
// This allows for fine-grained control of the template used for
|
// This allows for fine-grained control of the template used for
|
||||||
// rendering of e.g. links.
|
// rendering of e.g. links.
|
||||||
base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
|
base, layoutDescriptor := pco.po.p.GetInternalTemplateBasePathAndDescriptor()
|
||||||
|
|
||||||
switch tp {
|
switch tp {
|
||||||
case hooks.LinkRendererType:
|
case hooks.LinkRendererType:
|
||||||
|
|
|
@ -397,7 +397,7 @@ func doRenderShortcode(
|
||||||
ofCount[match.D.OutputFormat]++
|
ofCount[match.D.OutputFormat]++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
|
base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor()
|
||||||
q := tplimpl.TemplateQuery{
|
q := tplimpl.TemplateQuery{
|
||||||
Path: base,
|
Path: base,
|
||||||
Name: sc.name,
|
Name: sc.name,
|
||||||
|
|
|
@ -148,8 +148,8 @@ type InSectionPositioner interface {
|
||||||
PrevInSection() Page
|
PrevInSection() Page
|
||||||
}
|
}
|
||||||
|
|
||||||
// InternalDependencies is considered an internal interface.
|
// RelatedDocsHandlerProvider is considered an internal interface.
|
||||||
type InternalDependencies interface {
|
type RelatedDocsHandlerProvider interface {
|
||||||
// GetInternalRelatedDocsHandler is for internal use only.
|
// GetInternalRelatedDocsHandler is for internal use only.
|
||||||
GetInternalRelatedDocsHandler() *RelatedDocsHandler
|
GetInternalRelatedDocsHandler() *RelatedDocsHandler
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d, ok := p[0].(InternalDependencies)
|
d, ok := p[0].(RelatedDocsHandlerProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid type %T in related search", p[0])
|
return nil, fmt.Errorf("invalid type %T in related search", p[0])
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,16 +97,16 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
|
||||||
panic("HTML output format not found")
|
panic("HTML output format not found")
|
||||||
}
|
}
|
||||||
s := &TemplateStore{
|
s := &TemplateStore{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
siteOpts: siteOpts,
|
siteOpts: siteOpts,
|
||||||
optsOrig: opts,
|
optsOrig: opts,
|
||||||
siteOptsOrig: siteOpts,
|
siteOptsOrig: siteOpts,
|
||||||
htmlFormat: html,
|
htmlFormat: html,
|
||||||
storeSite: configureSiteStorage(siteOpts, opts.Watching),
|
storeSite: configureSiteStorage(siteOpts, opts.Watching),
|
||||||
treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
|
treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
|
||||||
treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
|
treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
|
||||||
templatesByPath: maps.NewCache[string, *TemplInfo](),
|
templatesByPath: maps.NewCache[string, *TemplInfo](),
|
||||||
templateDescriptorByPath: maps.NewCache[string, PathTemplateDescriptor](),
|
cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
|
||||||
|
|
||||||
// Note that the funcs passed below is just for name validation.
|
// Note that the funcs passed below is just for name validation.
|
||||||
tns: newTemplateNamespace(siteOpts.TemplateFuncs),
|
tns: newTemplateNamespace(siteOpts.TemplateFuncs),
|
||||||
|
@ -400,10 +400,9 @@ type TemplateStore struct {
|
||||||
siteOpts SiteOptions
|
siteOpts SiteOptions
|
||||||
htmlFormat output.Format
|
htmlFormat output.Format
|
||||||
|
|
||||||
treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
|
treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
|
||||||
treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
|
treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
|
||||||
templatesByPath *maps.Cache[string, *TemplInfo]
|
templatesByPath *maps.Cache[string, *TemplInfo]
|
||||||
templateDescriptorByPath *maps.Cache[string, PathTemplateDescriptor]
|
|
||||||
|
|
||||||
dh descriptorHandler
|
dh descriptorHandler
|
||||||
|
|
||||||
|
@ -417,6 +416,9 @@ type TemplateStore struct {
|
||||||
// For testing benchmarking.
|
// For testing benchmarking.
|
||||||
optsOrig StoreOptions
|
optsOrig StoreOptions
|
||||||
siteOptsOrig SiteOptions
|
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.
|
// 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 {
|
func (s *TemplateStore) LookupPartial(pth string) *TemplInfo {
|
||||||
d := s.templateDescriptorFromPath(pth)
|
ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) {
|
||||||
desc := d.Desc
|
d := s.templateDescriptorFromPath(pth)
|
||||||
if desc.Layout != "" {
|
desc := d.Desc
|
||||||
panic("shortcode template descriptor must not have a layout")
|
if desc.Layout != "" {
|
||||||
}
|
panic("shortcode template descriptor must not have a layout")
|
||||||
best := s.getBest()
|
}
|
||||||
defer s.putBest(best)
|
best := s.getBest()
|
||||||
s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best)
|
defer s.putBest(best)
|
||||||
return best.templ
|
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 {
|
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.
|
// RefreshFiles refreshes this store for the files matching the given predicate.
|
||||||
func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
|
func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
|
||||||
|
s.clearCaches()
|
||||||
|
|
||||||
if err := s.tns.createPrototypesParse(); err != nil {
|
if err := s.tns.createPrototypesParse(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1370,43 +1382,38 @@ type PathTemplateDescriptor struct {
|
||||||
// templateDescriptorFromPath returns a template descriptor from the given path.
|
// templateDescriptorFromPath returns a template descriptor from the given path.
|
||||||
// This is currently used in partial lookups only.
|
// This is currently used in partial lookups only.
|
||||||
func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor {
|
func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor {
|
||||||
// Check cache first.
|
var (
|
||||||
d, _ := s.templateDescriptorByPath.GetOrCreate(pth, func() (PathTemplateDescriptor, error) {
|
mt media.Type
|
||||||
var (
|
of output.Format
|
||||||
mt media.Type
|
)
|
||||||
of output.Format
|
|
||||||
)
|
|
||||||
|
|
||||||
// Common cases.
|
// Common cases.
|
||||||
dotCount := strings.Count(pth, ".")
|
dotCount := strings.Count(pth, ".")
|
||||||
if dotCount <= 1 {
|
if dotCount <= 1 {
|
||||||
if dotCount == 0 {
|
if dotCount == 0 {
|
||||||
// Asume HTML.
|
// Asume HTML.
|
||||||
of, mt = s.resolveOutputFormatAndOrMediaType("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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
|
pth = strings.TrimPrefix(pth, "/")
|
||||||
pth = path.PathNoIdentifier()
|
ext := path.Ext(pth)
|
||||||
of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
|
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{
|
return PathTemplateDescriptor{
|
||||||
Path: pth,
|
Path: pth,
|
||||||
Desc: TemplateDescriptor{
|
Desc: TemplateDescriptor{
|
||||||
OutputFormat: of.Name,
|
OutputFormat: of.Name,
|
||||||
MediaType: mt.Type,
|
MediaType: mt.Type,
|
||||||
IsPlainText: of.IsPlainText,
|
IsPlainText: of.IsPlainText,
|
||||||
},
|
},
|
||||||
}, nil
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
|
// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
qt "github.com/frankban/quicktest"
|
qt "github.com/frankban/quicktest"
|
||||||
"github.com/gohugoio/hugo/hugolib"
|
"github.com/gohugoio/hugo/hugolib"
|
||||||
"github.com/gohugoio/hugo/resources/kinds"
|
"github.com/gohugoio/hugo/resources/kinds"
|
||||||
|
"github.com/gohugoio/hugo/resources/page"
|
||||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -849,7 +850,7 @@ func BenchmarkExecuteWithContext(b *testing.B) {
|
||||||
disableKinds = ["taxonomy", "term", "home"]
|
disableKinds = ["taxonomy", "term", "home"]
|
||||||
-- layouts/all.html --
|
-- layouts/all.html --
|
||||||
{{ .Title }}|
|
{{ .Title }}|
|
||||||
{{ partial "p1.html" . }}
|
{{ partial "p1.html" . }}
|
||||||
-- layouts/_partials/p1.html --
|
-- layouts/_partials/p1.html --
|
||||||
p1.
|
p1.
|
||||||
{{ partial "p2.html" . }}
|
{{ partial "p2.html" . }}
|
||||||
|
@ -878,6 +879,82 @@ p3
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
err := store.ExecuteWithContext(context.Background(), ti, io.Discard, p)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue