hugo/hugolib/pagecollections.go
Bjørn Erik Pedersen 022c479551
hugofs: Make FileMeta a struct
This commit started out investigating a `concurrent map read write` issue, ending by replacing the map with a struct.

This is easier to reason about, and it's more effective:

```
name                                  old time/op    new time/op    delta
SiteNew/Regular_Deep_content_tree-16    71.5ms ± 3%    69.4ms ± 5%    ~     (p=0.200 n=4+4)

name                                  old alloc/op   new alloc/op   delta
SiteNew/Regular_Deep_content_tree-16    29.7MB ± 0%    27.9MB ± 0%  -5.82%  (p=0.029 n=4+4)

name                                  old allocs/op  new allocs/op  delta
SiteNew/Regular_Deep_content_tree-16      313k ± 0%      303k ± 0%  -3.35%  (p=0.029 n=4+4)
```

See #8749
2021-07-15 17:14:26 +02:00

340 lines
8.2 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 (
"fmt"
"path"
"path/filepath"
"strings"
"sync"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/page"
)
// PageCollections contains the page collections for a site.
type PageCollections struct {
pageMap *pageMap
// Lazy initialized page collections
pages *lazyPagesFactory
regularPages *lazyPagesFactory
allPages *lazyPagesFactory
allRegularPages *lazyPagesFactory
}
// Pages returns all pages.
// This is for the current language only.
func (c *PageCollections) Pages() page.Pages {
return c.pages.get()
}
// RegularPages returns all the regular pages.
// This is for the current language only.
func (c *PageCollections) RegularPages() page.Pages {
return c.regularPages.get()
}
// AllPages returns all pages for all languages.
func (c *PageCollections) AllPages() page.Pages {
return c.allPages.get()
}
// AllPages returns all regular pages for all languages.
func (c *PageCollections) AllRegularPages() page.Pages {
return c.allRegularPages.get()
}
type lazyPagesFactory struct {
pages page.Pages
init sync.Once
factory page.PagesFactory
}
func (l *lazyPagesFactory) get() page.Pages {
l.init.Do(func() {
l.pages = l.factory()
})
return l.pages
}
func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory {
return &lazyPagesFactory{factory: factory}
}
func newPageCollections(m *pageMap) *PageCollections {
if m == nil {
panic("must provide a pageMap")
}
c := &PageCollections{pageMap: m}
c.pages = newLazyPagesFactory(func() page.Pages {
return m.createListAllPages()
})
c.regularPages = newLazyPagesFactory(func() page.Pages {
return c.findPagesByKindIn(page.KindPage, c.pages.get())
})
return c
}
// This is an adapter func for the old API with Kind as first argument.
// This is invoked when you do .Site.GetPage. We drop the Kind and fails
// if there are more than 2 arguments, which would be ambiguous.
func (c *PageCollections) getPageOldVersion(ref ...string) (page.Page, error) {
var refs []string
for _, r := range ref {
// A common construct in the wild is
// .Site.GetPage "home" "" or
// .Site.GetPage "home" "/"
if r != "" && r != "/" {
refs = append(refs, r)
}
}
var key string
if len(refs) > 2 {
// This was allowed in Hugo <= 0.44, but we cannot support this with the
// new API. This should be the most unusual case.
return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref)
}
if len(refs) == 0 || refs[0] == page.KindHome {
key = "/"
} else if len(refs) == 1 {
if len(ref) == 2 && refs[0] == page.KindSection {
// This is an old style reference to the "Home Page section".
// Typically fetched via {{ .Site.GetPage "section" .Section }}
// See https://github.com/gohugoio/hugo/issues/4989
key = "/"
} else {
key = refs[0]
}
} else {
key = refs[1]
}
key = filepath.ToSlash(key)
if !strings.HasPrefix(key, "/") {
key = "/" + key
}
return c.getPageNew(nil, key)
}
// Only used in tests.
func (c *PageCollections) getPage(typ string, sections ...string) page.Page {
refs := append([]string{typ}, path.Join(sections...))
p, _ := c.getPageOldVersion(refs...)
return p
}
// getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive
// search path than getPageNew.
func (c *PageCollections) getPageRef(context page.Page, ref string) (page.Page, error) {
n, err := c.getContentNode(context, true, ref)
if err != nil || n == nil || n.p == nil {
return nil, err
}
return n.p, nil
}
func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) {
n, err := c.getContentNode(context, false, ref)
if err != nil || n == nil || n.p == nil {
return nil, err
}
return n.p, nil
}
func (c *PageCollections) getSectionOrPage(ref string) (*contentNode, string) {
var n *contentNode
pref := helpers.AddTrailingSlash(ref)
s, v, found := c.pageMap.sections.LongestPrefix(pref)
if found {
n = v.(*contentNode)
}
if found && s == pref {
// A section
return n, ""
}
m := c.pageMap
filename := strings.TrimPrefix(strings.TrimPrefix(ref, s), "/")
langSuffix := "." + m.s.Lang()
// Trim both extension and any language code.
name := paths.PathNoExt(filename)
name = strings.TrimSuffix(name, langSuffix)
// These are reserved bundle names and will always be stored by their owning
// folder name.
name = strings.TrimSuffix(name, "/index")
name = strings.TrimSuffix(name, "/_index")
if !found {
return nil, name
}
// Check if it's a section with filename provided.
if !n.p.File().IsZero() && n.p.File().LogicalName() == filename {
return n, name
}
return m.getPage(s, name), name
}
// For Ref/Reflink and .Site.GetPage do simple name lookups for the potentially ambigous myarticle.md and /myarticle.md,
// but not when we get ./myarticle*, section/myarticle.
func shouldDoSimpleLookup(ref string) bool {
if ref[0] == '.' {
return false
}
slashCount := strings.Count(ref, "/")
if slashCount > 1 {
return false
}
return slashCount == 0 || ref[0] == '/'
}
func (c *PageCollections) getContentNode(context page.Page, isReflink bool, ref string) (*contentNode, error) {
ref = filepath.ToSlash(strings.ToLower(strings.TrimSpace(ref)))
if ref == "" {
ref = "/"
}
inRef := ref
navUp := strings.HasPrefix(ref, "..")
var doSimpleLookup bool
if isReflink || context == nil {
doSimpleLookup = shouldDoSimpleLookup(ref)
}
if context != nil && !strings.HasPrefix(ref, "/") {
// Try the page-relative path.
var base string
if context.File().IsZero() {
base = context.SectionsPath()
} else {
meta := context.File().FileInfo().Meta()
base = filepath.ToSlash(filepath.Dir(meta.Path))
if meta.Classifier == files.ContentClassLeaf {
// Bundles are stored in subfolders e.g. blog/mybundle/index.md,
// so if the user has not explicitly asked to go up,
// look on the "blog" level.
if !navUp {
base = path.Dir(base)
}
}
}
ref = path.Join("/", strings.ToLower(base), ref)
}
if !strings.HasPrefix(ref, "/") {
ref = "/" + ref
}
m := c.pageMap
// It's either a section, a page in a section or a taxonomy node.
// Start with the most likely:
n, name := c.getSectionOrPage(ref)
if n != nil {
return n, nil
}
if !strings.HasPrefix(inRef, "/") {
// Many people will have "post/foo.md" in their content files.
if n, _ := c.getSectionOrPage("/" + inRef); n != nil {
return n, nil
}
}
// Check if it's a taxonomy node
pref := helpers.AddTrailingSlash(ref)
s, v, found := m.taxonomies.LongestPrefix(pref)
if found {
if !m.onSameLevel(pref, s) {
return nil, nil
}
return v.(*contentNode), nil
}
getByName := func(s string) (*contentNode, error) {
n := m.pageReverseIndex.Get(s)
if n != nil {
if n == ambiguousContentNode {
return nil, fmt.Errorf("page reference %q is ambiguous", ref)
}
return n, nil
}
return nil, nil
}
var module string
if context != nil && !context.File().IsZero() {
module = context.File().FileInfo().Meta().Module
}
if module == "" && !c.pageMap.s.home.File().IsZero() {
module = c.pageMap.s.home.File().FileInfo().Meta().Module
}
if module != "" {
n, err := getByName(module + ref)
if err != nil {
return nil, err
}
if n != nil {
return n, nil
}
}
if !doSimpleLookup {
return nil, nil
}
// Ref/relref supports this potentially ambigous lookup.
return getByName(path.Base(name))
}
func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
var pages page.Pages
for _, p := range inPages {
if p.Kind() == kind {
pages = append(pages, p)
}
}
return pages
}