From 474eb454dfb6e150d0a7a79edf32906f7a2355d8 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Mon, 10 Oct 2016 17:03:30 -0500 Subject: [PATCH] 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 --- tpl/template_funcs.go | 153 ++++++++++++++++++++++++------------- tpl/template_funcs_test.go | 136 +++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 53 deletions(-) diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go index 099f9d74e..e820ccf8d 100644 --- a/tpl/template_funcs.go +++ b/tpl/template_funcs.go @@ -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 @@ -1915,59 +1961,60 @@ func init() { } return template.HTML(helpers.AbsURL(s, true)), nil }, - "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') }, - "after": after, - "apply": apply, - "base64Decode": base64Decode, - "base64Encode": base64Encode, - "chomp": chomp, - "countrunes": countRunes, - "countwords": countWords, - "default": dfault, - "dateFormat": dateFormat, - "delimit": delimit, - "dict": dictionary, - "div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') }, - "echoParam": returnWhenSet, - "emojify": emojify, - "eq": eq, - "findRE": findRE, - "first": first, - "ge": ge, - "getCSV": getCSV, - "getJSON": getJSON, - "getenv": func(varName string) string { return os.Getenv(varName) }, - "gt": gt, - "hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) }, - "highlight": highlight, - "htmlEscape": htmlEscape, - "htmlUnescape": htmlUnescape, - "humanize": humanize, - "in": in, - "index": index, - "int": func(v interface{}) (int, error) { return cast.ToIntE(v) }, - "intersect": intersect, - "isSet": isSet, - "isset": isSet, - "jsonify": jsonify, - "last": last, - "le": le, - "lower": func(a string) string { return strings.ToLower(a) }, - "lt": lt, - "markdownify": markdownify, - "md5": md5, - "mod": mod, - "modBool": modBool, - "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') }, - "ne": ne, - "partial": partial, - "plainify": plainify, - "pluralize": pluralize, - "querify": querify, - "readDir": readDirFromWorkingDir, - "readFile": readFileFromWorkingDir, - "ref": ref, - "relURL": relURL, + "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') }, + "after": after, + "apply": apply, + "base64Decode": base64Decode, + "base64Encode": base64Encode, + "chomp": chomp, + "countrunes": countRunes, + "countwords": countWords, + "default": dfault, + "dateFormat": dateFormat, + "delimit": delimit, + "dict": dictionary, + "div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') }, + "echoParam": returnWhenSet, + "emojify": emojify, + "eq": eq, + "findRE": findRE, + "first": first, + "ge": ge, + "getCSV": getCSV, + "getJSON": getJSON, + "getenv": func(varName string) string { return os.Getenv(varName) }, + "gt": gt, + "hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) }, + "highlight": highlight, + "htmlEscape": htmlEscape, + "htmlUnescape": htmlUnescape, + "humanize": humanize, + "in": in, + "index": index, + "int": func(v interface{}) (int, error) { return cast.ToIntE(v) }, + "intersect": intersect, + "isSet": isSet, + "isset": isSet, + "jsonify": jsonify, + "last": last, + "le": le, + "lower": func(a string) string { return strings.ToLower(a) }, + "lt": lt, + "markdownify": markdownify, + "md5": md5, + "mod": mod, + "modBool": modBool, + "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') }, + "ne": ne, + "partial": partial, + "partialCached": partialCached, + "plainify": plainify, + "pluralize": pluralize, + "querify": querify, + "readDir": readDirFromWorkingDir, + "readFile": readFileFromWorkingDir, + "ref": ref, + "relURL": relURL, "relLangURL": func(i interface{}) (template.HTML, error) { s, err := cast.ToStringE(i) if err != nil { diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go index 2ace8d257..7f46aeba9 100644 --- a/tpl/template_funcs_test.go +++ b/tpl/template_funcs_test.go @@ -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() + } +}