diff --git a/cmd/eval.go b/cmd/eval.go index e3e5483934..36ee306f14 100644 --- a/cmd/eval.go +++ b/cmd/eval.go @@ -62,9 +62,6 @@ const ( defaultPrettyLimit = 80 ) -// default sorting order for profiler results -var defaultSortOrder = []string{"total_time_ns", "num_eval", "num_redo", "file", "line"} - func init() { var params evalCommandParams @@ -290,7 +287,7 @@ func eval(args []string, params evalCommandParams, w io.Writer) (int, error) { } if params.profile { - var sortOrder = defaultSortOrder + var sortOrder = pr.DefaultProfileSortOrder if len(params.profileCriteria.v) != 0 { sortOrder = getProfileSortOrder(strings.Split(params.profileCriteria.String(), ",")) @@ -328,7 +325,7 @@ func getProfileSortOrder(sortOrder []string) []string { } // compare the given sort order and the default - for _, cr := range defaultSortOrder { + for _, cr := range pr.DefaultProfileSortOrder { if _, ok := sortOrderMap[cr]; !ok { sortOrder = append(sortOrder, cr) } diff --git a/internal/presentation/presentation.go b/internal/presentation/presentation.go index ce6806861c..dfeae8b862 100644 --- a/internal/presentation/presentation.go +++ b/internal/presentation/presentation.go @@ -25,6 +25,9 @@ import ( "github.com/open-policy-agent/opa/topdown" ) +// DefaultProfileSortOrder is the default ordering unless something is specified in the CLI +var DefaultProfileSortOrder = []string{"total_time_ns", "num_eval", "num_redo", "file", "line"} + // DepAnalysisOutput contains the result of dependency analysis to be presented. type DepAnalysisOutput struct { Base []ast.Ref `json:"base,omitempty"` diff --git a/repl/repl.go b/repl/repl.go index 4927fd60b7..44e4dfe825 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -12,6 +12,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/open-policy-agent/opa/profiler" "html/template" "io" "os" @@ -41,6 +42,7 @@ type REPL struct { buffer []string txn storage.Transaction metrics metrics.Metrics + profiler bool // TODO(tsandall): replace this state with rule definitions // inside the default module. @@ -259,6 +261,8 @@ func (r *REPL) OneShot(ctx context.Context, line string) error { return r.cmdMetrics() case "instrument": return r.cmdInstrument() + case "profile": + return r.cmdProfile() case "types": return r.cmdTypes() case "unknown": @@ -426,6 +430,7 @@ func (r *REPL) cmdShow(args []string) error { Trace: r.traceEnabled(), Metrics: r.metricsEnabled(), Instrument: r.instrument, + Profile: r.profilerEnabled(), } b, err := json.MarshalIndent(debug, "", "\t") if err != nil { @@ -443,6 +448,7 @@ type replDebug struct { Trace bool `json:"trace"` Metrics bool `json:"metrics"` Instrument bool `json:"instrument"` + Profile bool `json:"profile"` } func (r *REPL) traceEnabled() bool { @@ -457,6 +463,7 @@ func (r *REPL) cmdTrace() error { r.explain = explainOff } else { r.explain = explainTrace + r.profiler = false } return nil } @@ -489,6 +496,21 @@ func (r *REPL) cmdInstrument() error { return nil } +func (r *REPL) profilerEnabled() bool { + return r.profiler +} + +// This function cmdProfile will turn tracing (explain) off if profile is turned on +func (r *REPL) cmdProfile() error { + if r.profiler { + r.profiler = false + } else { + r.profiler = true + r.explain = explainOff + } + return nil +} + func (r *REPL) cmdTypes() error { r.types = !r.types return nil @@ -824,9 +846,15 @@ func (r *REPL) evalStatement(ctx context.Context, stmt interface{}) error { func (r *REPL) evalBody(ctx context.Context, compiler *ast.Compiler, input ast.Value, body ast.Body) error { var buf *topdown.BufferTracer + var bufProfiler *profiler.Profiler + var tracer topdown.Tracer if r.explain != explainOff { buf = topdown.NewBufferTracer() + tracer = buf + } else if r.profiler { + bufProfiler = profiler.New() + tracer = bufProfiler } eval := rego.New( @@ -838,7 +866,7 @@ func (r *REPL) evalBody(ctx context.Context, compiler *ast.Compiler, input ast.V rego.ParsedQuery(body), rego.ParsedInput(input), rego.Metrics(r.metrics), - rego.Tracer(buf), + rego.Tracer(tracer), rego.Instrument(r.instrument), rego.Runtime(r.runtime), ) @@ -851,15 +879,16 @@ func (r *REPL) evalBody(ctx context.Context, compiler *ast.Compiler, input ast.V Metrics: r.metrics, } + if r.profiler { + output.Profile = bufProfiler.ReportTopNResults(-1, pr.DefaultProfileSortOrder) + } output = output.WithLimit(r.prettyLimit) - if buf != nil { + if r.explain != explainOff { mangleTrace(ctx, r.store, r.txn, *buf) output.Explanation = *buf } - // TODO(tsandall): add profiler output - switch r.outputFormat { case "json": return pr.JSON(r.output, output) @@ -1044,9 +1073,10 @@ var builtin = [...]commandDesc{ {"json", []string{}, "set output format to JSON"}, {"pretty", []string{}, "set output format to pretty"}, {"pretty-limit", []string{}, "set pretty value output limit"}, - {"trace", []string{}, "toggle full trace"}, + {"trace", []string{}, "toggle full trace and turns off profiler"}, {"metrics", []string{}, "toggle metrics"}, {"instrument", []string{}, "toggle instrumentation"}, + {"profile", []string{}, "toggle profiler and turns off trace"}, {"types", []string{}, "toggle type information"}, {"unknown", []string{"[ref-1 [ref-2 [...]]]"}, "toggle partial evaluation mode"}, {"dump", []string{"[path]"}, "dump raw data in storage"}, diff --git a/repl/repl_test.go b/repl/repl_test.go index 4eab646a87..00e49ff9ad 100644 --- a/repl/repl_test.go +++ b/repl/repl_test.go @@ -323,7 +323,8 @@ func TestShowDebug(t *testing.T) { expected := `{ "trace": false, "metrics": false, - "instrument": false + "instrument": false, + "profile": false } ` assertREPLText(t, buffer, expected) @@ -331,14 +332,29 @@ func TestShowDebug(t *testing.T) { repl.OneShot(ctx, "trace") repl.OneShot(ctx, "metrics") repl.OneShot(ctx, "instrument") + repl.OneShot(ctx, "profile") + repl.OneShot(ctx, "show debug") + expected = `{ + "trace": false, + "metrics": true, + "instrument": true, + "profile": true +} +` + assertREPLText(t, buffer, expected) + buffer.Reset() + repl.OneShot(ctx, "metrics") + repl.OneShot(ctx, "instrument") + repl.OneShot(ctx, "profile") + repl.OneShot(ctx, "trace") repl.OneShot(ctx, "show debug") expected = `{ "trace": true, "metrics": true, - "instrument": true + "instrument": true, + "profile": false } ` - assertREPLText(t, buffer, expected) buffer.Reset() } @@ -1745,6 +1761,84 @@ func TestMetrics(t *testing.T) { } } +func TestProfile(t *testing.T) { + store := newTestStore() + ctx := context.Background() + txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams) + const numLines = 21 + + mod2 := []byte(`package rbac + + input = { + "subject": "bob", + "resource": "foo123", + "action": "write", + } + bindings = [ + { + "user": "alice", + "roles": ["dev", "test"], + }, + { + "user": "bob", + "roles": ["test"], + }, +] + + roles = [ + { + "name": "dev", + "permissions": [ + {"resource": "foo123", "action": "write"}, + {"resource": "foo123", "action": "read"}, + ], + }, + { + "name": "test", + "permissions": [{"resource": "foo123", "action": "read"}], + }, +] + +default allow = false + + allow { + user_has_role[role_name] + role_has_permission[role_name] + } + + user_has_role[role_name] { + binding := bindings[_] + binding.user = input.subject + role_name := binding.roles[_] + } + + role_has_permission[role_name] { + role := roles[_] + role_name := role.name + perm := role.permissions[_] + perm.resource = input.resource + perm.action = input.action + }`) + + if err := store.UpsertPolicy(ctx, txn, "mod2", mod2); err != nil { + panic(err) + } + if err := store.Commit(ctx, txn); err != nil { + panic(err) + } + + var buffer bytes.Buffer + repl := newRepl(store, &buffer) + repl.OneShot(ctx, "profile") + repl.OneShot(ctx, "data.rbac.allow") + result := buffer.String() + lines := strings.Split(result, "\n") + if len(lines) != numLines { + t.Fatal("Expected 21 lines, got :", len(lines)) + } + buffer.Reset() +} + func TestInstrument(t *testing.T) { ctx := context.Background() store := newTestStore()