Skip to content

Commit 4ef9baf

Browse files
committed
Only invoke a given cached partial once
Note that this is backed by a LRU cache (which we soon shall see more usage of), so if you're a heavy user of cached partials it may be evicted and refreshed if needed. But in most cases every partial is only invoked once. This commit also adds a timeout (the global `timeout` config option) to make infinite recursion in partials easier to reason about. ``` name old time/op new time/op delta IncludeCached-10 8.92ms ± 0% 8.48ms ± 1% -4.87% (p=0.016 n=4+5) name old alloc/op new alloc/op delta IncludeCached-10 6.65MB ± 0% 5.17MB ± 0% -22.32% (p=0.002 n=6+6) name old allocs/op new allocs/op delta IncludeCached-10 117k ± 0% 71k ± 0% -39.44% (p=0.002 n=6+6) ``` Closes gohugoio#4086 Updates gohugoio#9588
1 parent 93ed6e4 commit 4ef9baf

21 files changed

+345
-202
lines changed

deps/deps.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/gohugoio/hugo/cache/filecache"
1212
"github.com/gohugoio/hugo/common/hexec"
1313
"github.com/gohugoio/hugo/common/loggers"
14+
"github.com/gohugoio/hugo/common/types"
1415
"github.com/gohugoio/hugo/config"
1516
"github.com/gohugoio/hugo/config/security"
1617
"github.com/gohugoio/hugo/helpers"
@@ -298,11 +299,14 @@ func New(cfg DepsCfg) (*Deps, error) {
298299

299300
sp := source.NewSourceSpec(ps, nil, fs.Source)
300301

301-
timeoutms := cfg.Language.GetInt("timeout")
302-
if timeoutms <= 0 {
303-
timeoutms = 3000
302+
timeout := 30 * time.Second
303+
if cfg.Cfg.IsSet("timeout") {
304+
v := cfg.Cfg.Get("timeout")
305+
d, err := types.ToDurationE(v)
306+
if err == nil {
307+
timeout = d
308+
}
304309
}
305-
306310
ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors"))
307311
ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...)
308312

@@ -329,7 +333,7 @@ func New(cfg DepsCfg) (*Deps, error) {
329333
BuildClosers: &Closers{},
330334
BuildState: buildState,
331335
Running: cfg.Running,
332-
Timeout: time.Duration(timeoutms) * time.Millisecond,
336+
Timeout: timeout,
333337
globalErrHandler: errorHandler,
334338
}
335339

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/bep/godartsass v0.16.0
1414
github.com/bep/golibsass v1.1.0
1515
github.com/bep/gowebp v0.2.0
16+
github.com/bep/lazycache v0.2.0
1617
github.com/bep/overlayfs v0.6.0
1718
github.com/bep/tmc v0.5.1
1819
github.com/clbanning/mxj/v2 v2.5.7
@@ -103,6 +104,7 @@ require (
103104
github.com/google/wire v0.5.0 // indirect
104105
github.com/googleapis/gax-go/v2 v2.3.0 // indirect
105106
github.com/googleapis/go-type-adapters v1.0.0 // indirect
107+
github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect
106108
github.com/inconshreveable/mousetrap v1.0.1 // indirect
107109
github.com/invopop/yaml v0.1.0 // indirect
108110
github.com/jmespath/go-jmespath v0.4.0 // indirect

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ github.com/bep/golibsass v1.1.0 h1:pjtXr00IJZZaOdfryNa9wARTB3Q0BmxC3/V1KNcgyTw=
179179
github.com/bep/golibsass v1.1.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
180180
github.com/bep/gowebp v0.2.0 h1:ZVfK8i9PpZqKHEmthQSt3qCnnHycbLzBPEsVtk2ch2Q=
181181
github.com/bep/gowebp v0.2.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
182+
github.com/bep/lazycache v0.2.0 h1:HKrlZTrDxHIrNKqmnurH42ryxkngCMYLfBpyu40VcwY=
183+
github.com/bep/lazycache v0.2.0/go.mod h1:xUIsoRD824Vx0Q/n57+ZO7kmbEhMBOnTjM/iPixNGbg=
182184
github.com/bep/overlayfs v0.6.0 h1:sgLcq/qtIzbaQNl2TldGXOkHvqeZB025sPvHOQL+DYo=
183185
github.com/bep/overlayfs v0.6.0/go.mod h1:NFjSmn3kCqG7KX2Lmz8qT8VhPPCwZap3UNogXawoQHM=
184186
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
@@ -238,8 +240,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
238240
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
239241
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
240242
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
241-
github.com/evanw/esbuild v0.15.18 h1:CM7eAoUjjNkZs1LH0p6fkwtADrbFr4JV2SlT1bUMjEo=
242-
github.com/evanw/esbuild v0.15.18/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
243243
github.com/evanw/esbuild v0.17.0 h1:gGx9TCZDO9k9x1PJdizx6syIpUq29RwrtHWlgDIdQH8=
244244
github.com/evanw/esbuild v0.17.0/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
245245
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@@ -401,6 +401,8 @@ github.com/hairyhenderson/go-codeowners v0.2.3-0.20201026200250-cdc7c0759690 h1:
401401
github.com/hairyhenderson/go-codeowners v0.2.3-0.20201026200250-cdc7c0759690/go.mod h1:8Qu9UmnhCRunfRv365Z3w+mT/WfLGKJiK+vugY9qNCU=
402402
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
403403
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
404+
github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4=
405+
github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
404406
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
405407
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
406408
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=

helpers/general.go

+1-21
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,13 @@ import (
2323
"os"
2424
"path/filepath"
2525
"sort"
26-
"strconv"
2726
"strings"
2827
"sync"
2928
"unicode"
3029
"unicode/utf8"
3130

3231
"github.com/gohugoio/hugo/common/loggers"
3332

34-
"github.com/mitchellh/hashstructure"
35-
3633
"github.com/gohugoio/hugo/common/hugo"
3734

3835
"github.com/spf13/afero"
@@ -219,7 +216,7 @@ func ReaderContains(r io.Reader, subslice []byte) bool {
219216
// GetTitleFunc returns a func that can be used to transform a string to
220217
// title case.
221218
//
222-
// The supported styles are
219+
// # The supported styles are
223220
//
224221
// - "Go" (strings.Title)
225222
// - "AP" (see https://www.apstylebook.com/)
@@ -523,20 +520,3 @@ func PrintFs(fs afero.Fs, path string, w io.Writer) {
523520
return nil
524521
})
525522
}
526-
527-
// HashString returns a hash from the given elements.
528-
// It will panic if the hash cannot be calculated.
529-
func HashString(elements ...any) string {
530-
var o any
531-
if len(elements) == 1 {
532-
o = elements[0]
533-
} else {
534-
o = elements
535-
}
536-
537-
hash, err := hashstructure.Hash(o, nil)
538-
if err != nil {
539-
panic(err)
540-
}
541-
return strconv.FormatUint(hash, 10)
542-
}

helpers/general_test.go

-7
Original file line numberDiff line numberDiff line change
@@ -458,10 +458,3 @@ func BenchmarkUniqueStrings(b *testing.B) {
458458
}
459459
})
460460
}
461-
462-
func TestHashString(t *testing.T) {
463-
c := qt.New(t)
464-
465-
c.Assert(HashString("a", "b"), qt.Equals, "2712570657419664240")
466-
c.Assert(HashString("ab"), qt.Equals, "590647783936702392")
467-
}

hugolib/page_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/bep/clock"
2626
"github.com/gohugoio/hugo/htesting"
27+
"github.com/gohugoio/hugo/identity"
2728
"github.com/gohugoio/hugo/markup/asciidocext"
2829
"github.com/gohugoio/hugo/markup/rst"
2930
"github.com/gohugoio/hugo/tpl"
@@ -2001,3 +2002,37 @@ Page1: {{ $p1.Path }}
20012002

20022003
b.AssertFileContent("public/index.html", "Lang: no", filepath.FromSlash("Page1: a/B/C/Page1.md"))
20032004
}
2005+
2006+
func TestPageHashString(t *testing.T) {
2007+
files := `
2008+
-- config.toml --
2009+
baseURL = "https://example.org"
2010+
[languages]
2011+
[languages.en]
2012+
weight = 1
2013+
title = "English"
2014+
[languages.no]
2015+
weight = 2
2016+
title = "Norsk"
2017+
-- content/p1.md --
2018+
---
2019+
title: "p1"
2020+
---
2021+
-- content/p2.md --
2022+
---
2023+
title: "p2"
2024+
---
2025+
`
2026+
2027+
b := NewIntegrationTestBuilder(IntegrationTestConfig{
2028+
T: t,
2029+
TxtarString: files,
2030+
}).Build()
2031+
2032+
p1 := b.H.Sites[0].RegularPages()[0]
2033+
p2 := b.H.Sites[0].RegularPages()[1]
2034+
sites := p1.Sites()
2035+
2036+
b.Assert(identity.HashString(p1), qt.Not(qt.Equals), identity.HashString(p2))
2037+
b.Assert(identity.HashString(sites[0]), qt.Not(qt.Equals), identity.HashString(sites[1]))
2038+
}

hugolib/resource_chain_test.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ import (
2626
"testing"
2727
"time"
2828

29-
"github.com/gohugoio/hugo/helpers"
30-
3129
qt "github.com/frankban/quicktest"
3230

3331
"github.com/gohugoio/hugo/common/loggers"
32+
"github.com/gohugoio/hugo/identity"
3433
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
3534
)
3635

@@ -122,7 +121,7 @@ FAILED REMOTE ERROR DETAILS CONTENT: |failed to fetch remote resource: Internal
122121
|StatusCode: 500|ContentLength: 16|ContentType: text/plain; charset=utf-8|
123122
124123
125-
`, helpers.HashString(ts.URL+"/sunset.jpg", map[string]any{})))
124+
`, identity.HashString(ts.URL+"/sunset.jpg", map[string]any{})))
126125

127126
b.AssertFileContent("public/styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css", "body{background-color:#add8e6}")
128127
b.AssertFileContent("public//styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css", "body{background-color:orange}")

hugolib/site.go

+4
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,10 @@ func (s *SiteInfo) DisqusShortname() string {
778778
return s.Config().Services.Disqus.Shortname
779779
}
780780

781+
func (s *SiteInfo) GetIdentity() identity.Identity {
782+
return identity.KeyValueIdentity{Key: "site", Value: s.language.Lang}
783+
}
784+
781785
// SiteSocial is a place to put social details on a site level. These are the
782786
// standard keys that themes will expect to have available, but can be
783787
// expanded to any others on a per site basis

identity/identity.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
// Copyright 2023 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
114
package identity
215

316
import (
@@ -107,7 +120,7 @@ func (id KeyValueIdentity) Name() string {
107120
return id.Key
108121
}
109122

110-
// Provider provides the hashable Identity.
123+
// Provider provides the comparable Identity.
111124
type Provider interface {
112125
// GetIdentity is for internal use.
113126
GetIdentity() Identity

identity/identityhash.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2023 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package identity
15+
16+
import (
17+
"strconv"
18+
19+
"github.com/mitchellh/hashstructure"
20+
)
21+
22+
// HashString returns a hash from the given elements.
23+
// It will panic if the hash cannot be calculated.
24+
// Note that this hash should be used primarily for identity, not for change detection as
25+
// it in the more complex values (e.g. Page) will not hash the full content.
26+
func HashString(vs ...any) string {
27+
hash := HashUint64(vs...)
28+
return strconv.FormatUint(hash, 10)
29+
}
30+
31+
// HashUint64 returns a hash from the given elements.
32+
// It will panic if the hash cannot be calculated.
33+
// Note that this hash should be used primarily for identity, not for change detection as
34+
// it in the more complex values (e.g. Page) will not hash the full content.
35+
func HashUint64(vs ...any) uint64 {
36+
var o any
37+
if len(vs) == 1 {
38+
o = toHashable(vs[0])
39+
} else {
40+
elements := make([]any, len(vs))
41+
for i, e := range vs {
42+
elements[i] = toHashable(e)
43+
}
44+
o = elements
45+
}
46+
47+
hash, err := hashstructure.Hash(o, nil)
48+
if err != nil {
49+
panic(err)
50+
}
51+
return hash
52+
}
53+
54+
type keyer interface {
55+
Key() string
56+
}
57+
58+
// For structs, hashstructure.Hash only works on the exported fields,
59+
// so rewrite the input slice for known identity types.
60+
func toHashable(v any) any {
61+
switch t := v.(type) {
62+
case Provider:
63+
return t.GetIdentity()
64+
case keyer:
65+
return t.Key()
66+
default:
67+
return v
68+
}
69+
}

identity/identityhash_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2023 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package identity
15+
16+
import (
17+
"testing"
18+
19+
qt "github.com/frankban/quicktest"
20+
)
21+
22+
func TestHashString(t *testing.T) {
23+
c := qt.New(t)
24+
25+
c.Assert(HashString("a", "b"), qt.Equals, "2712570657419664240")
26+
c.Assert(HashString("ab"), qt.Equals, "590647783936702392")
27+
28+
var vals []any = []any{"a", "b", tstKeyer{"c"}}
29+
30+
c.Assert(HashString(vals...), qt.Equals, "12599484872364427450")
31+
c.Assert(vals[2], qt.Equals, tstKeyer{"c"})
32+
33+
}
34+
35+
type tstKeyer struct {
36+
key string
37+
}
38+
39+
func (t tstKeyer) Key() string {
40+
return t.key
41+
}
42+
43+
func (t tstKeyer) String() string {
44+
return "key: " + t.key
45+
}

metrics/metrics.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727

2828
"github.com/gohugoio/hugo/common/types"
2929
"github.com/gohugoio/hugo/compare"
30-
"github.com/gohugoio/hugo/helpers"
30+
"github.com/gohugoio/hugo/identity"
3131
)
3232

3333
// The Provider interface defines an interface for measuring metrics.
@@ -242,7 +242,7 @@ func howSimilar(a, b any) int {
242242
return 90
243243
}
244244

245-
h1, h2 := helpers.HashString(a), helpers.HashString(b)
245+
h1, h2 := identity.HashString(a), identity.HashString(b)
246246
if h1 == h2 {
247247
return 100
248248
}

0 commit comments

Comments
 (0)