Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ranger: Improve intsRanger performance #202

Merged
merged 5 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions default.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func init() {
return result
})),
"ints": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) {
var from, to int
var from, to int64
err := a.ParseInto(&from, &to)
if err != nil {
panic(err)
Expand All @@ -149,7 +149,7 @@ func init() {
if to <= from {
panic(errors.New("invalid range for ints ranger: 'from' must be smaller than 'to'"))
}
return reflect.ValueOf(&intsRanger{from: from, to: to})
return reflect.ValueOf(newIntsRanger(from, to))
})),
"dump": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) {
switch numArgs := a.NumOfArguments(); numArgs {
Expand Down
11 changes: 9 additions & 2 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- [Slices / Arrays](#slices--arrays)
- [Maps](#maps)
- [Channels](#channels)
- [Custom](#custom-ranger)
- [else](#else)
- [try](#try)
- [try / catch](#try--catch)
Expand Down Expand Up @@ -366,7 +367,7 @@ Use `range` to iterate over data, just like you would in Go, or how you would us
{{.}}
{{ end }}

Jet provides built-in rangers for Go slices, arrays, maps, and channels. You can add your own by implementing the Ranger interface. TODO
Jet provides built-in rangers for Go slices, arrays, maps, and channels. You can add your own by implementing the Ranger interface.
sauerbraten marked this conversation as resolved.
Show resolved Hide resolved

#### Slices / Arrays

Expand Down Expand Up @@ -411,6 +412,12 @@ When iterating over a channel, you can can have Jet assign the iteration value t

It's an error to use channels together with the two-variable syntax.

#### Custom Ranger

Any value that implements the
[Ranger](https://pkg.go.dev/github.com/CloudyKit/jet/v6#Ranger) interface can be
used for ranging over values. Look in the package docs for an example.

#### else

`range` statements can have an `else` block which is executed if there are non values to range over (as signalled by the Ranger). For example, it will run when iterating an empty slice, array or map or a closed channel:
Expand Down Expand Up @@ -687,4 +694,4 @@ Executing `index.jet` will produce:

`import` makes all the blocks from the imported template available in the importing template. There is no way to only import (a) specific block(s).

Since the imported template isn't actually executed, the blocks defined in it don't run until you `yield` them explicitely.
Since the imported template isn't actually executed, the blocks defined in it don't run until you `yield` them explicitely.
75 changes: 75 additions & 0 deletions eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,21 @@ func init() {
}

JetTestingSet.AddGlobal("dummy", dummy)
JetTestingSet.AddGlobalFunc("customFn", func(args Arguments) reflect.Value {
args.RequireNumOfArguments("customFn", 1, 1)
return args.Get(0)
})

JetTestingLoader.Set("actionNode_dummy", `hello {{dummy("WORLD")}}`)
JetTestingLoader.Set("noAllocFn", `hello {{ "José" }} {{1}} {{ "José" }}`)
JetTestingLoader.Set("rangeOverUsers", `{{range .}}{{.Name}}-{{.Email}}{{end}}`)
JetTestingLoader.Set("rangeOverUsers_Set", `{{range index,user:= . }}{{index}}{{user.Name}}-{{user.Email}}{{end}}`)
JetTestingLoader.Set("BenchNewBlock", "{{ block col(md=12,offset=0) }}\n<div class=\"col-md-{{md}} col-md-offset-{{offset}}\">{{ yield content }}</div>\n\t\t{{ end }}\n\t\t{{ block row(md=12) }}\n<div class=\"row {{md}}\">{{ yield content }}</div>\n\t\t{{ content }}\n<div class=\"col-md-1\"></div>\n<div class=\"col-md-1\"></div>\n<div class=\"col-md-1\"></div>\n\t\t{{ end }}\n\t\t{{ block header() }}\n<div class=\"header\">\n\t{{ yield row() content}}\n\t\t{{ yield col(md=6) content }}\n{{ yield content }}\n\t\t{{end}}\n\t{{end}}\n</div>\n\t\t{{content}}\n<h1>Hey</h1>\n\t\t{{ end }}")
JetTestingLoader.Set("BenchCustomRanger", "{{range .}}{{.Name}}{{end}}")
JetTestingLoader.Set("BenchIntsRanger", "{{range ints(0, .)}} {{end}}")
JetTestingLoader.Set("BenchCustomRender", "{{range k, v := ints(0, .N)}}{{.Field}}{{end}}")
JetTestingLoader.Set("BenchCallCustomFn", "{{range ints(0, .N)}}{{customFn(.)}}{{end}}")
JetTestingLoader.Set("BenchExecPipeline", "{{range ints(0, .N)}}{{. | customFn}}{{end}}")
}

func RunJetTest(t *testing.T, variables VarMap, context interface{}, testName, testContent, testExpected string) {
Expand Down Expand Up @@ -749,6 +758,7 @@ func TestRanger(t *testing.T) {
RunJetTest(t, data, nil, "map_ranger_key_value", `{{ range k, v := m }}{{k}}:{{v}},{{ end }}`, "asd:123,")
RunJetTest(t, data, nil, "chan_ranger", `{{ range v := c }}{{v}}{{ end }}`, "0123456789")
RunJetTest(t, nil, nil, "ints_ranger", `{{ range i := ints(0, 10) }}{{ (i == 0 ? "" : ", ") + i }}{{ end }}`, "0, 1, 2, 3, 4, 5, 6, 7, 8, 9")
RunJetTest(t, nil, nil, "ints_ranger_index_value", `{{ range k, v := ints(10, 20) }}{{k}}:{{v}} {{ end }}`, "0:10 1:11 2:12 3:13 4:14 5:15 6:16 7:17 8:18 9:19 ")
RunJetTest(t, data, nil, "custom_indexed_ranger", `{{ range ci }}{{.}},{{ end }}`, "asd,foo,bar,")
RunJetTest(t, data, nil, "custom_indexed_ranger_key_context", `{{ range k := ci }}{{k}}:{{.}},{{ end }}`, "0:asd,1:foo,2:bar,")
RunJetTest(t, data, nil, "custom_indexed_ranger_key_value", `{{ range k, v := ci }}{{k}}:{{v}},{{ end }}`, "0:asd,1:foo,2:bar,")
Expand Down Expand Up @@ -928,6 +938,71 @@ func BenchmarkCustomRanger(b *testing.B) {
}
}

// BenchmarkIntsRanger benchmarks the performance of doing one additional
// iteration using the ints ranger.
func BenchmarkIntsRanger(b *testing.B) {
t, _ := JetTestingSet.GetTemplate("BenchIntsRanger")
b.ResetTimer()
err := t.Execute(ww, nil, b.N)
if err != nil {
b.Error(err.Error())
}
}

type customRenderer struct {
data []byte
}

func (cr *customRenderer) Render(r *Runtime) {
r.Write(cr.data)
}

var _ Renderer = (*customRenderer)(nil)

// BenchmarkCustomRender benchmarks executing a template that calls the Render()
// method of a custom renderer object.
func BenchmarkCustomRender(b *testing.B) {
execCtx := struct {
N int
Field *customRenderer
}{
N: b.N,
Field: &customRenderer{data: []byte("foobar")},
}
t, _ := JetTestingSet.GetTemplate("BenchCustomRender")
b.ResetTimer()
err := t.Execute(ww, nil, execCtx)
if err != nil {
b.Error(err.Error())
}
}

// BenchmarkCallCustomFn benchmarks executing a template that calls a custom
// function repeatedly.
func BenchmarkCallCustomFn(b *testing.B) {
t, _ := JetTestingSet.GetTemplate("BenchCallCustomFn")
execCtx := struct{ N int }{N: b.N}
b.ResetTimer()
err := t.Execute(ww, nil, execCtx)
if err != nil {
b.Error(err.Error())
}

}

// BenchmarkExecPipeline benchmarks executing a template that calls a pipeline
// repeatedly.
func BenchmarkExecPipeline(b *testing.B) {
t, _ := JetTestingSet.GetTemplate("BenchExecPipeline")
execCtx := struct{ N int }{N: b.N}
b.ResetTimer()
err := t.Execute(ww, nil, execCtx)
if err != nil {
b.Error(err.Error())
}

}

// BenchmarkFieldAccess benchmarks executing a template that accesses fields
// in the current context.
//
Expand Down
23 changes: 19 additions & 4 deletions ranger.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,36 @@ type Ranger interface {
}

type intsRanger struct {
i, from, to int
i, val, to int64
}

var _ Ranger = &intsRanger{}

func (r *intsRanger) Range() (index, value reflect.Value, end bool) {
index = reflect.ValueOf(r.i)
value = reflect.ValueOf(r.from + r.i)
end = r.i == r.to-r.from
r.i++
r.val++
end = r.val == r.to

// The indirection in the ValueOf calls avoids an allocation versus
// using the concrete value of 'i' and 'val'. The downside is having
// to interpret 'r.i' as "the current value" after Range() returns,
// and so it needs to be initialized as -1.
index = reflect.ValueOf(&r.i).Elem()
value = reflect.ValueOf(&r.val).Elem()
return
}

func (r *intsRanger) ProvidesIndex() bool { return true }

func newIntsRanger(from, to int64) *intsRanger {
r := &intsRanger{
to: to,
i: -1,
val: from - 1,
}
return r
}

type pooledRanger interface {
Ranger
Setup(reflect.Value)
Expand Down
52 changes: 52 additions & 0 deletions ranger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package jet

import (
"fmt"
"os"
"reflect"
)

// exampleCustomBenchRanger satisfies the Ranger interface, generating fixed
// data.
type exampleCustomRanger struct {
i int
}

// Type assertion to verify exampleCustomRanger satisfies the Ranger interface.
var _ Ranger = (*exampleCustomRanger)(nil)

func (ecr *exampleCustomRanger) ProvidesIndex() bool {
// Return false if 'k' can't be filled in Range().
return true
}

func (ecr *exampleCustomRanger) Range() (k reflect.Value, v reflect.Value, done bool) {
if ecr.i >= 3 {
done = true
return
}

k = reflect.ValueOf(ecr.i)
v = reflect.ValueOf(fmt.Sprintf("custom ranger %d", ecr.i))
ecr.i += 1
return
}

// ExampleRanger demonstrates how to write a custom template ranger.
func ExampleRanger() {
// Error handling ignored for brevity.
//
// Setup template and rendering.
loader := NewInMemLoader()
loader.Set("template", "{{range k := ecr }}{{k}}:{{.}}; {{end}}")
set := NewSet(loader, WithSafeWriter(nil))
t, _ := set.GetTemplate("template")

// Pass a custom ranger instance as the 'ecr' var.
vars := VarMap{"ecr": reflect.ValueOf(&exampleCustomRanger{})}

// Execute template.
_ = t.Execute(os.Stdout, vars, nil)

// Output: 0:custom ranger 0; 1:custom ranger 1; 2:custom ranger 2;
}