tpl: Add partialCached template function

Supports an optional variant string parameter so that a given partial
will be cached based upon the name+variant.

Fixes #1368
Closes #2552
This commit is contained in:
Cameron Moore 2016-10-10 17:03:30 -05:00 committed by Bjørn Erik Pedersen
parent d2bc64bee3
commit 474eb454df
2 changed files with 236 additions and 53 deletions

View file

@ -1392,6 +1392,52 @@ func replace(a, b, c interface{}) (string, error) {
return strings.Replace(aStr, bStr, cStr, -1), nil
}
// partialCache represents a cache of partials protected by a mutex.
type partialCache struct {
sync.RWMutex
p map[string]template.HTML
}
// Get retrieves partial output from the cache based upon the partial name.
// If the partial is not found in the cache, the partial is rendered and added
// to the cache.
func (c *partialCache) Get(key, name string, context interface{}) (p template.HTML) {
var ok bool
c.RLock()
p, ok = c.p[key]
c.RUnlock()
if ok {
return p
}
c.Lock()
if p, ok = c.p[key]; !ok {
p = partial(name, context)
c.p[key] = p
}
c.Unlock()
return p
}
var cachedPartials = partialCache{p: make(map[string]template.HTML)}
// partialCached executes and caches partial templates. An optional variant
// string parameter (a string slice actually, but be only use a variadic
// argument to make it optional) can be passed so that a given partial can have
// multiple uses. The cache is created with name+variant as the key.
func partialCached(name string, context interface{}, variant ...string) template.HTML {
key := name
if len(variant) > 0 {
for i := 0; i < len(variant); i++ {
key += variant[i]
}
}
return cachedPartials.Get(key, name, context)
}
// regexpCache represents a cache of regexp objects protected by a mutex.
type regexpCache struct {
mu sync.RWMutex
@ -1961,6 +2007,7 @@ func init() {
"mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') },
"ne": ne,
"partial": partial,
"partialCached": partialCached,
"plainify": plainify,
"pluralize": pluralize,
"querify": querify,

View file

@ -2471,3 +2471,139 @@ func TestReadFile(t *testing.T) {
}
}
}
func TestPartialCached(t *testing.T) {
testCases := []struct {
name string
partial string
tmpl string
variant string
}{
// name and partial should match between test cases.
{"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . }}`, ""},
{"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"},
{"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "footer"},
{"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"},
}
results := make(map[string]string, len(testCases))
var data struct {
Title string
Section string
Params map[string]interface{}
}
data.Title = "**BatMan**"
data.Section = "blog"
data.Params = map[string]interface{}{"langCode": "en"}
InitializeT()
for i, tc := range testCases {
var tmp string
if tc.variant != "" {
tmp = fmt.Sprintf(tc.tmpl, tc.variant)
} else {
tmp = tc.tmpl
}
tmpl, err := New().New("testroot").Parse(tmp)
if err != nil {
t.Fatalf("[%d] unable to create new html template: %s", i, err)
}
if tmpl == nil {
t.Fatalf("[%d] tmpl should not be nil!", i)
}
tmpl.New("partials/" + tc.name).Parse(tc.partial)
buf := new(bytes.Buffer)
err = tmpl.Execute(buf, &data)
if err != nil {
t.Fatalf("[%d] error executing template: %s", i, err)
}
for j := 0; j < 10; j++ {
buf2 := new(bytes.Buffer)
err = tmpl.Execute(buf2, nil)
if err != nil {
t.Fatalf("[%d] error executing template 2nd time: %s", i, err)
}
if !reflect.DeepEqual(buf, buf2) {
t.Fatalf("[%d] cached results do not match:\nResult 1:\n%q\nResult 2:\n%q", i, buf, buf2)
}
}
// double-check against previous test cases of the same variant
previous, ok := results[tc.name+tc.variant]
if !ok {
results[tc.name+tc.variant] = buf.String()
} else {
if previous != buf.String() {
t.Errorf("[%d] cached variant differs from previous rendering; got:\n%q\nwant:\n%q", i, buf.String(), previous)
}
}
}
}
func BenchmarkPartial(b *testing.B) {
InitializeT()
tmpl, err := New().New("testroot").Parse(`{{ partial "bench1" . }}`)
if err != nil {
b.Fatalf("unable to create new html template: %s", err)
}
tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`)
buf := new(bytes.Buffer)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err = tmpl.Execute(buf, nil); err != nil {
b.Fatalf("error executing template: %s", err)
}
buf.Reset()
}
}
func BenchmarkPartialCached(b *testing.B) {
InitializeT()
tmpl, err := New().New("testroot").Parse(`{{ partialCached "bench1" . }}`)
if err != nil {
b.Fatalf("unable to create new html template: %s", err)
}
tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`)
buf := new(bytes.Buffer)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err = tmpl.Execute(buf, nil); err != nil {
b.Fatalf("error executing template: %s", err)
}
buf.Reset()
}
}
func BenchmarkPartialCachedVariants(b *testing.B) {
InitializeT()
tmpl, err := New().New("testroot").Parse(`{{ partialCached "bench1" . "header" }}`)
if err != nil {
b.Fatalf("unable to create new html template: %s", err)
}
tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`)
buf := new(bytes.Buffer)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err = tmpl.Execute(buf, nil); err != nil {
b.Fatalf("error executing template: %s", err)
}
buf.Reset()
}
}