Skip to content

Commit 208a0de

Browse files
committed
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
1 parent 18d2d2f commit 208a0de

File tree

11 files changed

+160
-75
lines changed

11 files changed

+160
-75
lines changed

Diff for: common/maps/cache.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func (c *Cache[K, T]) Len() int {
160160

161161
func (c *Cache[K, T]) Reset() {
162162
c.Lock()
163-
c.m = make(map[K]T)
163+
clear(c.m)
164164
c.hasBeenInitialized = false
165165
c.Unlock()
166166
}

Diff for: hugolib/alias.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err
5151
var templateDesc tplimpl.TemplateDescriptor
5252
var base string = ""
5353
if ps, ok := p.(*pageState); ok {
54-
base, templateDesc = ps.getTemplateBasePathAndDescriptor()
54+
base, templateDesc = ps.GetInternalTemplateBasePathAndDescriptor()
5555
}
5656
templateDesc.Layout = ""
5757
templateDesc.Kind = ""

Diff for: hugolib/page.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,8 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
476476
return nil
477477
}
478478

479-
func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
479+
// Exported so it can be used in integration tests.
480+
func (po *pageOutput) GetInternalTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
480481
p := po.p
481482
f := po.f
482483
base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
@@ -491,7 +492,7 @@ func (po *pageOutput) getTemplateBasePathAndDescriptor() (string, tplimpl.Templa
491492
}
492493

493494
func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
494-
dir, d := p.getTemplateBasePathAndDescriptor()
495+
dir, d := p.GetInternalTemplateBasePathAndDescriptor()
495496

496497
if len(layouts) > 0 {
497498
d.Layout = layouts[0]

Diff for: hugolib/page__common.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ type pageCommon struct {
9797
pageMenus *pageMenus
9898

9999
// Internal use
100-
page.InternalDependencies
100+
page.RelatedDocsHandlerProvider
101101

102102
contentConverterInit sync.Once
103103
contentConverter converter.Converter

Diff for: hugolib/page__new.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,11 @@ func (h *HugoSites) doNewPage(m *pageMeta) (*pageState, *paths.Path, error) {
209209
ShortcodeInfoProvider: page.NopPage,
210210
LanguageProvider: m.s,
211211

212-
InternalDependencies: m.s,
213-
init: lazy.New(),
214-
m: m,
215-
s: m.s,
216-
sWrapped: page.WrapSite(m.s),
212+
RelatedDocsHandlerProvider: m.s,
213+
init: lazy.New(),
214+
m: m,
215+
s: m.s,
216+
sWrapped: page.WrapSite(m.s),
217217
},
218218
}
219219

Diff for: hugolib/page__per_output.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
275275
// Inherit the descriptor from the page/current output format.
276276
// This allows for fine-grained control of the template used for
277277
// rendering of e.g. links.
278-
base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
278+
base, layoutDescriptor := pco.po.p.GetInternalTemplateBasePathAndDescriptor()
279279

280280
switch tp {
281281
case hooks.LinkRendererType:

Diff for: hugolib/shortcode.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ func doRenderShortcode(
397397
ofCount[match.D.OutputFormat]++
398398
return true
399399
}
400-
base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
400+
base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor()
401401
q := tplimpl.TemplateQuery{
402402
Path: base,
403403
Name: sc.name,

Diff for: resources/page/page.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ type InSectionPositioner interface {
148148
PrevInSection() Page
149149
}
150150

151-
// InternalDependencies is considered an internal interface.
152-
type InternalDependencies interface {
151+
// RelatedDocsHandlerProvider is considered an internal interface.
152+
type RelatedDocsHandlerProvider interface {
153153
// GetInternalRelatedDocsHandler is for internal use only.
154154
GetInternalRelatedDocsHandler() *RelatedDocsHandler
155155
}

Diff for: resources/page/pages_related.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.I
124124
return nil, nil
125125
}
126126

127-
d, ok := p[0].(InternalDependencies)
127+
d, ok := p[0].(RelatedDocsHandlerProvider)
128128
if !ok {
129129
return nil, fmt.Errorf("invalid type %T in related search", p[0])
130130
}

Diff for: tpl/tplimpl/templatestore.go

+65-58
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,16 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
9797
panic("HTML output format not found")
9898
}
9999
s := &TemplateStore{
100-
opts: opts,
101-
siteOpts: siteOpts,
102-
optsOrig: opts,
103-
siteOptsOrig: siteOpts,
104-
htmlFormat: html,
105-
storeSite: configureSiteStorage(siteOpts, opts.Watching),
106-
treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
107-
treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
108-
templatesByPath: maps.NewCache[string, *TemplInfo](),
109-
templateDescriptorByPath: maps.NewCache[string, PathTemplateDescriptor](),
100+
opts: opts,
101+
siteOpts: siteOpts,
102+
optsOrig: opts,
103+
siteOptsOrig: siteOpts,
104+
htmlFormat: html,
105+
storeSite: configureSiteStorage(siteOpts, opts.Watching),
106+
treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
107+
treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
108+
templatesByPath: maps.NewCache[string, *TemplInfo](),
109+
cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
110110

111111
// Note that the funcs passed below is just for name validation.
112112
tns: newTemplateNamespace(siteOpts.TemplateFuncs),
@@ -400,10 +400,9 @@ type TemplateStore struct {
400400
siteOpts SiteOptions
401401
htmlFormat output.Format
402402

403-
treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
404-
treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
405-
templatesByPath *maps.Cache[string, *TemplInfo]
406-
templateDescriptorByPath *maps.Cache[string, PathTemplateDescriptor]
403+
treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
404+
treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
405+
templatesByPath *maps.Cache[string, *TemplInfo]
407406

408407
dh descriptorHandler
409408

@@ -417,6 +416,9 @@ type TemplateStore struct {
417416
// For testing benchmarking.
418417
optsOrig StoreOptions
419418
siteOptsOrig SiteOptions
419+
420+
// caches. These need to be refreshed when the templates are refreshed.
421+
cacheLookupPartials *maps.Cache[string, *TemplInfo]
420422
}
421423

422424
// NewFromOpts creates a new store with the same configuration as the original.
@@ -540,15 +542,19 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
540542
}
541543

542544
func (s *TemplateStore) LookupPartial(pth string) *TemplInfo {
543-
d := s.templateDescriptorFromPath(pth)
544-
desc := d.Desc
545-
if desc.Layout != "" {
546-
panic("shortcode template descriptor must not have a layout")
547-
}
548-
best := s.getBest()
549-
defer s.putBest(best)
550-
s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best)
551-
return best.templ
545+
ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) {
546+
d := s.templateDescriptorFromPath(pth)
547+
desc := d.Desc
548+
if desc.Layout != "" {
549+
panic("shortcode template descriptor must not have a layout")
550+
}
551+
best := s.getBest()
552+
defer s.putBest(best)
553+
s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best)
554+
return best.templ, nil
555+
})
556+
557+
return ti
552558
}
553559

554560
func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
@@ -619,8 +625,14 @@ func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer
619625
})
620626
}
621627

628+
func (s *TemplateStore) clearCaches() {
629+
s.cacheLookupPartials.Reset()
630+
}
631+
622632
// RefreshFiles refreshes this store for the files matching the given predicate.
623633
func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
634+
s.clearCaches()
635+
624636
if err := s.tns.createPrototypesParse(); err != nil {
625637
return err
626638
}
@@ -1370,43 +1382,38 @@ type PathTemplateDescriptor struct {
13701382
// templateDescriptorFromPath returns a template descriptor from the given path.
13711383
// This is currently used in partial lookups only.
13721384
func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor {
1373-
// Check cache first.
1374-
d, _ := s.templateDescriptorByPath.GetOrCreate(pth, func() (PathTemplateDescriptor, error) {
1375-
var (
1376-
mt media.Type
1377-
of output.Format
1378-
)
1379-
1380-
// Common cases.
1381-
dotCount := strings.Count(pth, ".")
1382-
if dotCount <= 1 {
1383-
if dotCount == 0 {
1384-
// Asume HTML.
1385-
of, mt = s.resolveOutputFormatAndOrMediaType("html", "")
1386-
} else {
1387-
pth = strings.TrimPrefix(pth, "/")
1388-
ext := path.Ext(pth)
1389-
pth = strings.TrimSuffix(pth, ext)
1390-
ext = ext[1:]
1391-
of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
1392-
}
1385+
var (
1386+
mt media.Type
1387+
of output.Format
1388+
)
1389+
1390+
// Common cases.
1391+
dotCount := strings.Count(pth, ".")
1392+
if dotCount <= 1 {
1393+
if dotCount == 0 {
1394+
// Asume HTML.
1395+
of, mt = s.resolveOutputFormatAndOrMediaType("html", "")
13931396
} else {
1394-
path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
1395-
pth = path.PathNoIdentifier()
1396-
of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
1397-
}
1398-
1399-
return PathTemplateDescriptor{
1400-
Path: pth,
1401-
Desc: TemplateDescriptor{
1402-
OutputFormat: of.Name,
1403-
MediaType: mt.Type,
1404-
IsPlainText: of.IsPlainText,
1405-
},
1406-
}, nil
1407-
})
1397+
pth = strings.TrimPrefix(pth, "/")
1398+
ext := path.Ext(pth)
1399+
pth = strings.TrimSuffix(pth, ext)
1400+
ext = ext[1:]
1401+
of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
1402+
}
1403+
} else {
1404+
path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
1405+
pth = path.PathNoIdentifier()
1406+
of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
1407+
}
14081408

1409-
return d
1409+
return PathTemplateDescriptor{
1410+
Path: pth,
1411+
Desc: TemplateDescriptor{
1412+
OutputFormat: of.Name,
1413+
MediaType: mt.Type,
1414+
IsPlainText: of.IsPlainText,
1415+
},
1416+
}
14101417
}
14111418

14121419
// resolveOutputFormatAndOrMediaType resolves the output format and/or media type

Diff for: tpl/tplimpl/templatestore_integration_test.go

+79-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
qt "github.com/frankban/quicktest"
99
"github.com/gohugoio/hugo/hugolib"
1010
"github.com/gohugoio/hugo/resources/kinds"
11+
"github.com/gohugoio/hugo/resources/page"
1112
"github.com/gohugoio/hugo/tpl/tplimpl"
1213
)
1314

@@ -849,7 +850,7 @@ func BenchmarkExecuteWithContext(b *testing.B) {
849850
disableKinds = ["taxonomy", "term", "home"]
850851
-- layouts/all.html --
851852
{{ .Title }}|
852-
{{ partial "p1.html" . }}
853+
{{ partial "p1.html" . }}
853854
-- layouts/_partials/p1.html --
854855
p1.
855856
{{ partial "p2.html" . }}
@@ -878,6 +879,82 @@ p3
878879
b.ResetTimer()
879880
for i := 0; i < b.N; i++ {
880881
err := store.ExecuteWithContext(context.Background(), ti, io.Discard, p)
881-
bb.Assert(err, qt.IsNil)
882+
if err != nil {
883+
b.Fatal(err)
884+
}
885+
}
886+
}
887+
888+
func BenchmarkLookupPartial(b *testing.B) {
889+
files := `
890+
-- hugo.toml --
891+
disableKinds = ["taxonomy", "term", "home"]
892+
-- layouts/all.html --
893+
{{ .Title }}|
894+
-- layouts/_partials/p1.html --
895+
-- layouts/_partials/p2.html --
896+
-- layouts/_partials/p2.json --
897+
-- layouts/_partials/p3.html --
898+
`
899+
bb := hugolib.Test(b, files)
900+
901+
store := bb.H.TemplateStore
902+
903+
for i := 0; i < b.N; i++ {
904+
fi := store.LookupPartial("p3.html")
905+
if fi == nil {
906+
b.Fatal("not found")
907+
}
882908
}
883909
}
910+
911+
// Implemented by pageOutput.
912+
type getDescriptorProvider interface {
913+
GetInternalTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor)
914+
}
915+
916+
func BenchmarkLookupShortcode(b *testing.B) {
917+
files := `
918+
-- hugo.toml --
919+
disableKinds = ["taxonomy", "term", "home"]
920+
-- content/toplevelpage.md --
921+
-- content/a/b/c/nested.md --
922+
-- layouts/all.html --
923+
{{ .Title }}|
924+
-- layouts/_shortcodes/s.html --
925+
s1.
926+
-- layouts/_shortcodes/a/b/s.html --
927+
s2.
928+
929+
`
930+
bb := hugolib.Test(b, files)
931+
store := bb.H.TemplateStore
932+
933+
runOne := func(p page.Page) {
934+
pth, desc := p.(getDescriptorProvider).GetInternalTemplateBasePathAndDescriptor()
935+
q := tplimpl.TemplateQuery{
936+
Path: pth,
937+
Name: "s",
938+
Category: tplimpl.CategoryShortcode,
939+
Desc: desc,
940+
}
941+
v := store.LookupShortcode(q)
942+
if v == nil {
943+
b.Fatal("not found")
944+
}
945+
}
946+
947+
b.Run("toplevelpage", func(b *testing.B) {
948+
toplevelpage, _ := bb.H.Sites[0].GetPage("/toplevelpage")
949+
for i := 0; i < b.N; i++ {
950+
runOne(toplevelpage)
951+
}
952+
})
953+
954+
b.Run("nestedpage", func(b *testing.B) {
955+
toplevelpage, _ := bb.H.Sites[0].GetPage("/a/b/c/nested")
956+
for i := 0; i < b.N; i++ {
957+
runOne(toplevelpage)
958+
}
959+
})
960+
}

0 commit comments

Comments
 (0)