wasm/sdk: add benchmarks (#3103)

This is porting some of the existing topdown tests --
but run with the Wasm SDK. I've factored out the data
generation bits into the util package; using it in both places.

I've come to believe that whatever memory usage data we're
collecting there is probably bogus: too many of my runs yield

    1597 B/op	      41 allocs/op

regardless of the input size. I'd think that the problem lies in
the boundary crossing to cgo-land, but I have yet to find a
source for that.

That aside, disabling the allocation reporting, and gathering
benchmark data for the run times is probably already useful,
so let's go with that.

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
This commit is contained in:
Stephan Renatus
2021-02-03 14:02:50 +01:00
committed by GitHub
parent f45286caa9
commit 01d2554f80
10 changed files with 433 additions and 230 deletions
-1
View File
@@ -196,7 +196,6 @@ func (p *Pool) SetPolicyData(policy []byte, data []byte) error {
if bytes.Equal(policy, currentPolicy) && bytes.Equal(data, currentData) {
return nil
}
err := p.setPolicyData(policy, data)
+4 -3
View File
@@ -4,12 +4,13 @@
package wasm
const wasmPageSize = 65535
// PageSize represents the WASM page size in bytes.
const PageSize = 65535
// Pages converts a byte size to Pages, rounding up as necessary.
func Pages(n uint32) uint32 {
pages := n / wasmPageSize
if pages*wasmPageSize == n {
pages := n / PageSize
if pages*PageSize == n {
return pages
}
+1 -2
View File
@@ -8,7 +8,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
@@ -259,7 +258,7 @@ func (i *VM) Eval(ctx context.Context, entrypoint int32, input *interface{}, met
if e := recover(); e != nil {
switch e := e.(type) {
case abortError:
err = errors.New(e.message)
err = fmt.Errorf(e.message)
case builtinError:
err = e.err
if _, ok := err.(topdown.Halt); !ok {
+1 -1
View File
@@ -64,7 +64,7 @@ func (o *OPA) WithDataJSON(data interface{}) *OPA {
// WithMemoryLimits configures the memory limits (in bytes) for a single policy
// evaluation.
func (o *OPA) WithMemoryLimits(min, max uint32) *OPA {
if min < 2*65535 {
if min < 2*wasm.PageSize {
o.configErr = fmt.Errorf("too low minimum memory limit: %w", errors.ErrInvalidConfig)
return o
}
+1 -1
View File
@@ -182,7 +182,7 @@ func (o *OPA) Eval(ctx context.Context, opts EvalOpts) (*Result, error) {
return nil, fmt.Errorf("%v: %w", err, errors.ErrInternal)
}
return &Result{result}, nil
return &Result{Result: result}, nil
}
// Close waits until all the pending evaluations complete and then
+194
View File
@@ -0,0 +1,194 @@
package opa_test
import (
"context"
"fmt"
"testing"
"github.com/open-policy-agent/opa/internal/wasm/sdk/internal/wasm"
"github.com/open-policy-agent/opa/internal/wasm/sdk/opa"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/util/test"
)
func BenchmarkWasmRego(b *testing.B) {
policy := compileRegoToWasm("a = true", "data.p.a = x", false)
instance, _ := opa.New().
WithPolicyBytes(policy).
WithMemoryLimits(131070, 2*131070). // TODO: For some reason unlimited memory slows down the eval_ctx_new().
WithPoolSize(1).
Init()
b.ReportAllocs()
b.ResetTimer()
ctx := context.Background()
var input interface{} = make(map[string]interface{})
for i := 0; i < b.N; i++ {
if _, err := instance.Eval(ctx, opa.EvalOpts{Input: &input}); err != nil {
panic(err)
}
}
}
func BenchmarkGoRego(b *testing.B) {
pq := compileRego(`package p
a = true`, "data.p.a = x")
b.ReportAllocs()
b.ResetTimer()
ctx := context.Background()
input := make(map[string]interface{})
for i := 0; i < b.N; i++ {
if _, err := pq.Eval(ctx, rego.EvalInput(input)); err != nil {
panic(err)
}
}
}
func BenchmarkWASMArrayIteration(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, n := range sizes {
b.Run(fmt.Sprint(n), func(b *testing.B) {
benchmarkIteration(b, test.ArrayIterationBenchmarkModule(n))
})
}
}
func BenchmarkWASMSetIteration(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, n := range sizes {
b.Run(fmt.Sprint(n), func(b *testing.B) {
benchmarkIteration(b, test.SetIterationBenchmarkModule(n))
})
}
}
func BenchmarkWASMObjectIteration(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, n := range sizes {
b.Run(fmt.Sprint(n), func(b *testing.B) {
benchmarkIteration(b, test.ObjectIterationBenchmarkModule(n))
})
}
}
var r *opa.Result
func benchmarkIteration(b *testing.B, module string) {
query := "data.test.main = x"
policy := compileRegoToWasm(module, query, false)
instance, err := opa.New().
WithPolicyBytes(policy).
WithMemoryLimits(2*wasm.PageSize, 47*wasm.PageSize).
WithPoolSize(1).
Init()
if err != nil {
b.Fatalf("init sdk: %v", err)
}
b.ResetTimer()
ctx := context.Background()
var input interface{} = make(map[string]interface{})
for i := 0; i < b.N; i++ {
r, err = instance.Eval(ctx, opa.EvalOpts{Input: &input})
if err != nil {
b.Fatalf("Unexpected query error: %v", err)
}
if string(r.Result) != `{{"x":true}}` {
b.Errorf("unexpected result: %s", string(r.Result))
}
}
}
func BenchmarkWASMLargeJSON(b *testing.B) {
for _, kv := range []struct{ key, val int }{
{10, 10},
{10, 100},
{10, 1000},
{10, 10000},
{100, 100},
{100, 1000},
} {
b.Run(fmt.Sprintf("%dx%d", kv.key, kv.val), func(b *testing.B) {
ctx := context.Background()
data := test.GenerateJSONBenchmarkData(kv.key, kv.val)
// Read data.values N times inside query.
query := "data.keys[_] = x; data.values = y"
policy := compileRegoToWasm("", query, false)
instance, err := opa.New().
WithPolicyBytes(policy).
WithDataJSON(data).
WithMemoryLimits(200*wasm.PageSize, 600*wasm.PageSize). // This is rather much
WithPoolSize(1).
Init()
if err != nil {
b.Fatalf("init sdk: %v", err)
}
b.ResetTimer()
var input interface{} = make(map[string]interface{})
for i := 0; i < b.N; i++ {
r, err = instance.Eval(ctx, opa.EvalOpts{Input: &input})
if err != nil {
b.Fatalf("Unexpected query error: %v", err)
}
}
})
}
}
func BenchmarkWASMVirtualDocs(b *testing.B) {
for _, kv := range []struct{ total, hit int }{
{1, 1},
{10, 1},
{100, 1},
{1000, 1},
{10, 10},
{100, 10},
{1000, 10},
{100, 100},
{1000, 100},
{1000, 1000},
} {
b.Run(fmt.Sprintf("total=%d/hit=%d", kv.total, kv.hit), func(b *testing.B) {
runVirtualDocsBenchmark(b, kv.total, kv.hit)
})
}
}
func runVirtualDocsBenchmark(b *testing.B, numTotalRules, numHitRules int) {
ctx := context.Background()
module, input := test.GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules)
query := "data.a.b.c.allow = x"
policy := compileRegoToWasm(module, query, false)
instance, err := opa.New().
WithPolicyBytes(policy).
WithMemoryLimits(8*wasm.PageSize, 8*wasm.PageSize).
WithPoolSize(1).
Init()
if err != nil {
b.Fatalf("init sdk: %v", err)
}
b.ResetTimer()
var inp interface{} = input
for i := 0; i < b.N; i++ {
r, err = instance.Eval(ctx, opa.EvalOpts{Input: &inp})
if err != nil {
b.Fatalf("Unexpected query error: %v", err)
}
}
}
+20 -45
View File
@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"os"
"strings"
"testing"
"github.com/open-policy-agent/opa/ast"
@@ -17,6 +18,9 @@ import (
"github.com/open-policy-agent/opa/util"
)
// control dumping in this file
const dump = true
func TestOPA(t *testing.T) {
type Eval struct {
NewPolicy string
@@ -220,7 +224,7 @@ a = "c" { input > 2 }`,
for _, test := range tests {
t.Run(test.Description, func(t *testing.T) {
policy := compileRegoToWasm(test.Policy, test.Query)
policy := compileRegoToWasm(test.Policy, test.Query, dump)
data := []byte(test.Data)
if len(data) == 0 {
data = nil
@@ -240,14 +244,14 @@ a = "c" { input > 2 }`,
for _, eval := range test.Evals {
switch {
case eval.NewPolicy != "" && eval.NewData != "":
policy := compileRegoToWasm(eval.NewPolicy, test.Query)
policy := compileRegoToWasm(eval.NewPolicy, test.Query, dump)
data := parseJSON(eval.NewData)
if err := instance.SetPolicyData(policy, data); err != nil {
t.Errorf(err.Error())
}
case eval.NewPolicy != "":
policy := compileRegoToWasm(eval.NewPolicy, test.Query)
policy := compileRegoToWasm(eval.NewPolicy, test.Query, dump)
if err := instance.SetPolicy(policy); err != nil {
t.Errorf(err.Error())
}
@@ -349,51 +353,22 @@ func TestNamedEntrypoint(t *testing.T) {
}
}
func BenchmarkWasmRego(b *testing.B) {
policy := compileRegoToWasm("a = true", "data.p.a = x")
instance, _ := opa.New().
WithPolicyBytes(policy).
WithMemoryLimits(131070, 2*131070). // TODO: For some reason unlimited memory slows down the eval_ctx_new().
WithPoolSize(1).
Init()
b.ReportAllocs()
b.ResetTimer()
ctx := context.Background()
var input interface{} = make(map[string]interface{})
for i := 0; i < b.N; i++ {
if _, err := instance.Eval(ctx, opa.EvalOpts{Input: &input}); err != nil {
panic(err)
}
// compileRegoToWasm is shared with the benchmarking functions in opa_bench_test.go;
// those function use helpers shared with topdown_bench_test.go, and they all use
// `package test` -- whereas the callers in this file don't provide the package at
// all and assume it'll be `p`.
func compileRegoToWasm(module string, query string, dump bool) []byte {
if !strings.HasPrefix(module, "package") {
module = fmt.Sprintf("package p\n%s", module)
}
}
func BenchmarkGoRego(b *testing.B) {
pq := compileRego(`package p
a = true`, "data.p.a = x")
b.ReportAllocs()
b.ResetTimer()
input := make(map[string]interface{})
for i := 0; i < b.N; i++ {
if _, err := pq.Eval(context.Background(), rego.EvalInput(input)); err != nil {
panic(err)
}
}
}
func compileRegoToWasm(policy string, query string) []byte {
module := fmt.Sprintf("package p\n%s", policy)
cr, err := rego.New(
opts := []func(*rego.Rego){
rego.Query(query),
rego.Module("module.rego", module),
rego.Dump(os.Stderr),
).Compile(context.Background(), rego.CompilePartial(false))
}
if dump {
opts = append(opts, rego.Dump(os.Stderr))
}
cr, err := rego.New(opts...).Compile(context.Background(), rego.CompilePartial(false))
if err != nil {
panic(err)
}
+1 -1
View File
@@ -1257,7 +1257,7 @@ func (r *Rego) Compile(ctx context.Context, opts ...CompileOption) (*CompileResu
}
} else {
var err error
// If creating a new transacation it should be closed before calling the
// If creating a new transaction it should be closed before calling the
// planner to avoid holding open the transaction longer than needed.
//
// TODO(tsandall): in future, planner could make use of store, in which
+13 -176
View File
@@ -16,14 +16,14 @@ import (
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/storage/inmem"
"github.com/open-policy-agent/opa/util"
"github.com/open-policy-agent/opa/util/test"
)
func BenchmarkArrayIteration(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, n := range sizes {
b.Run(fmt.Sprint(n), func(b *testing.B) {
benchmarkIteration(b, getArrayIterationBenchmarkModule(n))
benchmarkIteration(b, test.ArrayIterationBenchmarkModule(n))
})
}
}
@@ -32,7 +32,7 @@ func BenchmarkSetIteration(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, n := range sizes {
b.Run(fmt.Sprint(n), func(b *testing.B) {
benchmarkIteration(b, getSetIterationBenchmarkModule(n))
benchmarkIteration(b, test.SetIterationBenchmarkModule(n))
})
}
}
@@ -41,7 +41,7 @@ func BenchmarkObjectIteration(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, n := range sizes {
b.Run(fmt.Sprint(n), func(b *testing.B) {
benchmarkIteration(b, getObjectIterationBenchmarkModule(n))
benchmarkIteration(b, test.ObjectIterationBenchmarkModule(n))
})
}
}
@@ -65,32 +65,8 @@ func benchmarkIteration(b *testing.B, module string) {
}
}
func getArrayIterationBenchmarkModule(n int) string {
return fmt.Sprintf(`package test
fixture = [ x | x := numbers.range(1, %d)[_] ]
main { fixture[i] }`, n)
}
func getSetIterationBenchmarkModule(n int) string {
return fmt.Sprintf(`package test
fixture = { x | x := numbers.range(1, %d)[_] }
main { fixture[i] }`, n)
}
func getObjectIterationBenchmarkModule(n int) string {
return fmt.Sprintf(`package test
fixture = { x: x | x := numbers.range(1, %d)[_] }
main { fixture[i] }`, n)
}
func BenchmarkLargeJSON(b *testing.B) {
data := generateLargeJSONBenchmarkData()
data := test.GenerateLargeJSONBenchmarkData()
ctx := context.Background()
store := inmem.NewFromObject(data)
compiler := ast.NewCompiler()
@@ -128,26 +104,6 @@ func BenchmarkLargeJSON(b *testing.B) {
}
}
func generateLargeJSONBenchmarkData() map[string]interface{} {
// create array of null values that can be iterated over
keys := make([]interface{}, 100)
for i := range keys {
keys[i] = nil
}
// create large JSON object value (100,000 entries is about 2MB on disk)
values := map[string]interface{}{}
for i := 0; i < 100*1000; i++ {
values[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
}
return map[string]interface{}{
"keys": keys,
"values": values,
}
}
func BenchmarkConcurrency1(b *testing.B) {
benchmarkConcurrency(b, getParams(1, 0))
}
@@ -174,10 +130,10 @@ func BenchmarkConcurrency8Writers(b *testing.B) {
func benchmarkConcurrency(b *testing.B, params []storage.TransactionParams) {
mod, data := generateConcurrencyBenchmarkData()
mod, data := test.GenerateConcurrencyBenchmarkData()
ctx := context.Background()
store := inmem.NewFromObject(data)
mods := map[string]*ast.Module{"module": mod}
mods := map[string]*ast.Module{"module": ast.MustParseModule(mod)}
compiler := ast.NewCompiler()
if compiler.Compile(mods); compiler.Failed() {
@@ -226,53 +182,6 @@ func getParams(nReaders, nWriters int) (sl []storage.TransactionParams) {
return sl
}
func generateConcurrencyBenchmarkData() (*ast.Module, map[string]interface{}) {
obj := util.MustUnmarshalJSON([]byte(`
{
"objs": [
{
"attr1": "get",
"path": "/foo/bar",
"user": "bob"
},
{
"attr1": "set",
"path": "/foo/bar/baz",
"user": "alice"
},
{
"attr1": "get",
"path": "/foo",
"groups": [
"admin",
"eng"
]
},
{
"path": "/foo/bar",
"user": "alice"
}
]
}
`))
mod := `package test
import data.objs
p {
objs[i].attr1 = "get"
objs[i].groups[j] = "eng"
}
p {
objs[i].user = "alice"
}
`
return ast.MustParseModule(mod), obj.(map[string]interface{})
}
func BenchmarkVirtualDocs1x1(b *testing.B) {
runVirtualDocsBenchmark(b, 1, 1)
}
@@ -315,24 +224,25 @@ func BenchmarkVirtualDocs1000x1000(b *testing.B) {
func runVirtualDocsBenchmark(b *testing.B, numTotalRules, numHitRules int) {
mod, input := generateVirtualDocsBenchmarkData(numTotalRules, numHitRules)
mod, inp := test.GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules)
ctx := context.Background()
compiler := ast.NewCompiler()
mods := map[string]*ast.Module{"module": mod}
mods := map[string]*ast.Module{"module": ast.MustParseModule(mod)}
input := ast.NewTerm(ast.MustInterfaceToValue(inp))
store := inmem.New()
txn := storage.NewTransactionOrDie(ctx, store)
if compiler.Compile(mods); compiler.Failed() {
b.Fatalf("Unexpected compiler error: %v", compiler.Errors)
}
body := ast.MustParseBody("data.a.b.c.allow = x")
query := ast.MustParseBody("data.a.b.c.allow = x")
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
query := NewQuery(body).
query := NewQuery(query).
WithCompiler(compiler).
WithStore(store).
WithTransaction(txn).
@@ -345,84 +255,11 @@ func runVirtualDocsBenchmark(b *testing.B, numTotalRules, numHitRules int) {
b.Fatalf("Unexpected topdown query error: %v", err)
}
if len(rs) != 1 || !rs[0][ast.Var("x")].Equal(ast.BooleanTerm(true)) {
b.Fatalf("Unexpecfted undefined/extra/bad result: %v", rs)
b.Fatalf("Unexpected undefined/extra/bad result: %v", rs)
}
}
}
func generateVirtualDocsBenchmarkData(numTotalRules, numHitRules int) (*ast.Module, *ast.Term) {
hitRule := `
allow {
input.method = "POST"
input.path = ["accounts", account_id]
input.user_id = account_id
}
`
missRule := `
allow {
input.method = "GET"
input.path = ["salaries", account_id]
input.user_id = account_id
}
`
testModuleTmpl := `
package a.b.c
{{range .MissRules }}
{{ . }}
{{end}}
{{range .HitRules }}
{{ . }}
{{end}}
`
tmpl, err := template.New("Test").Parse(testModuleTmpl)
if err != nil {
panic(err)
}
var buf bytes.Buffer
var missRules []string
if numTotalRules > numHitRules {
missRules = make([]string, numTotalRules-numHitRules)
for i := range missRules {
missRules[i] = missRule
}
}
hitRules := make([]string, numHitRules)
for i := range hitRules {
hitRules[i] = hitRule
}
params := struct {
MissRules []string
HitRules []string
}{
MissRules: missRules,
HitRules: hitRules,
}
err = tmpl.Execute(&buf, params)
if err != nil {
panic(err)
}
input := ast.MustParseTerm(`{
"path": ["accounts", "alice"],
"method": "POST",
"user_id": "alice"
}`)
return ast.MustParseModule(buf.String()), input
}
func BenchmarkPartialEval(b *testing.B) {
sizes := []int{1, 10, 100, 1000}
for _, n := range sizes {
+198
View File
@@ -0,0 +1,198 @@
package test
// This file collects some helpers for generating data used in
// benchmarks,
// - topdown/topdown_bench_test.go
import (
"bytes"
"encoding/json"
"fmt"
"text/template"
)
// ArrayIterationBenchmarkModule returns a module that iterates an array
// with `n` elements
func ArrayIterationBenchmarkModule(n int) string {
return fmt.Sprintf(`package test
fixture = [ x | x := numbers.range(1, %d)[_] ]
main { fixture[i] }`, n)
}
// SetIterationBenchmarkModule returns a module that iterates a set
// with `n` elements
func SetIterationBenchmarkModule(n int) string {
return fmt.Sprintf(`package test
fixture = { x | x := numbers.range(1, %d)[_] }
main { fixture[i] }`, n)
}
// ObjectIterationBenchmarkModule returns a module that iterates an object
// with `n` key/val pairs
func ObjectIterationBenchmarkModule(n int) string {
return fmt.Sprintf(`package test
fixture = { x: x | x := numbers.range(1, %d)[_] }
main { fixture[i] }`, n)
}
// GenerateLargeJSONBenchmarkData returns a map of 100 keys and 100.000 key/value
// pairs.
func GenerateLargeJSONBenchmarkData() map[string]interface{} {
return GenerateJSONBenchmarkData(100, 100*1000)
}
// GenerateJSONBenchmarkData returns a map of `k` keys and `v` key/value pairs.
func GenerateJSONBenchmarkData(k, v int) map[string]interface{} {
// create array of null values that can be iterated over
keys := make([]interface{}, k)
for i := range keys {
keys[i] = nil
}
// create large JSON object value (100,000 entries is about 2MB on disk)
values := map[string]interface{}{}
for i := 0; i < v; i++ {
values[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
}
return map[string]interface{}{
"keys": keys,
"values": values,
}
}
// GenerateConcurrencyBenchmarkData returns a module and data; the module
// checks some input parameters against that data in a simple API authz
// scheme.
func GenerateConcurrencyBenchmarkData() (string, map[string]interface{}) {
obj := []byte(`
{
"objs": [
{
"attr1": "get",
"path": "/foo/bar",
"user": "bob"
},
{
"attr1": "set",
"path": "/foo/bar/baz",
"user": "alice"
},
{
"attr1": "get",
"path": "/foo",
"groups": [
"admin",
"eng"
]
},
{
"path": "/foo/bar",
"user": "alice"
}
]
}
`)
var data map[string]interface{}
if err := json.Unmarshal(obj, &data); err != nil {
panic(err)
}
mod := `package test
import data.objs
p {
objs[i].attr1 = "get"
objs[i].groups[j] = "eng"
}
p {
objs[i].user = "alice"
}
`
return mod, data
}
// GenerateVirtualDocsBenchmarkData generates a module and input; the
// numTotalRules and numHitRules create as many rules in the module to
// match/miss the returned input.
func GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules int) (string, map[string]interface{}) {
hitRule := `
allow {
input.method = "POST"
input.path = ["accounts", account_id]
input.user_id = account_id
}
`
missRule := `
allow {
input.method = "GET"
input.path = ["salaries", account_id]
input.user_id = account_id
}
`
testModuleTmpl := `package a.b.c
{{range .MissRules }}
{{ . }}
{{end}}
{{range .HitRules }}
{{ . }}
{{end}}
`
tmpl, err := template.New("Test").Parse(testModuleTmpl)
if err != nil {
panic(err)
}
var buf bytes.Buffer
var missRules []string
if numTotalRules > numHitRules {
missRules = make([]string, numTotalRules-numHitRules)
for i := range missRules {
missRules[i] = missRule
}
}
hitRules := make([]string, numHitRules)
for i := range hitRules {
hitRules[i] = hitRule
}
params := struct {
MissRules []string
HitRules []string
}{
MissRules: missRules,
HitRules: hitRules,
}
err = tmpl.Execute(&buf, params)
if err != nil {
panic(err)
}
input := map[string]interface{}{
"path": []interface{}{"accounts", "alice"},
"method": "POST",
"user_id": "alice",
}
return buf.String(), input
}