mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
d3f34a3387
Have done this some time in the past, but there was a few new issues this would highlight now that we're on Go 1.24. Mostly: - Use `b.Loop()` in benchmarks - Use `strings.SplitSeq` where possible - Remove `omitempty` tag for types that can't be empty Signed-off-by: Anders Eknert <anders@eknert.com>
699 lines
18 KiB
Go
699 lines
18 KiB
Go
// Copyright 2020 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
|
|
"github.com/open-policy-agent/opa/v1/server/types"
|
|
|
|
"github.com/open-policy-agent/opa/v1/logging"
|
|
"github.com/open-policy-agent/opa/v1/runtime"
|
|
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/open-policy-agent/opa/cmd/formats"
|
|
"github.com/open-policy-agent/opa/cmd/internal/env"
|
|
"github.com/open-policy-agent/opa/internal/presentation"
|
|
"github.com/open-policy-agent/opa/v1/compile"
|
|
"github.com/open-policy-agent/opa/v1/metrics"
|
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
// benchmarkCommandParams are a superset of evalCommandParams
|
|
// but not all eval options are exposed with flags. Only the
|
|
// ones compatible with running a benchmark.
|
|
type benchmarkCommandParams struct {
|
|
evalCommandParams
|
|
benchMem bool
|
|
count int
|
|
e2e bool
|
|
gracefulShutdownPeriod int
|
|
shutdownWaitPeriod int
|
|
configFile string
|
|
}
|
|
|
|
func newBenchmarkEvalParams() benchmarkCommandParams {
|
|
return benchmarkCommandParams{
|
|
evalCommandParams: evalCommandParams{
|
|
outputFormat: formats.Flag(formats.Pretty, formats.JSON, formats.GoBench),
|
|
target: util.NewEnumFlag(compile.TargetRego, []string{compile.TargetRego, compile.TargetWasm}),
|
|
schema: &schemaFlags{},
|
|
capabilities: newCapabilitiesFlag(),
|
|
},
|
|
gracefulShutdownPeriod: 10,
|
|
}
|
|
}
|
|
|
|
func initBench(root *cobra.Command, brand string) {
|
|
executable := root.Name()
|
|
|
|
params := newBenchmarkEvalParams()
|
|
|
|
benchCommand := &cobra.Command{
|
|
Use: "bench <query>",
|
|
Short: "Benchmark a Rego query",
|
|
Long: `Benchmark a Rego query and print the results.
|
|
|
|
The benchmark command works very similar to 'eval' and will evaluate the query in the same fashion. The
|
|
evaluation will be repeated a number of times and performance results will be returned.
|
|
|
|
Example with bundle and input data:
|
|
|
|
` + executable + ` bench -b ./policy-bundle -i input.json 'data.authz.allow'
|
|
|
|
To run benchmarks against a running ` + brand + ` server to evaluate server overhead use the --e2e flag.
|
|
To enable more detailed analysis use the --metrics and --benchmem flags.
|
|
|
|
The optional "gobench" output format conforms to the Go Benchmark Data Format.
|
|
`,
|
|
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := env.CmdFlags.CheckEnvironmentVariables(cmd); err != nil {
|
|
return err
|
|
}
|
|
// Initialize testing package for benchmarking. This is needed to set default values for some flags that may
|
|
// otherwise be dereferenced on some code paths causing panics, as reported in:
|
|
// https://github.com/open-policy-agent/opa/issues/7205
|
|
testing.Init()
|
|
|
|
return validateEvalParams(¶ms.evalCommandParams, args)
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
|
|
exit, err := benchMain(args, params, os.Stdout, os.Stderr, &goBenchRunner{})
|
|
if err != nil {
|
|
// NOTE: err should only be non-nil if a (highly unlikely)
|
|
// presentation error occurs.
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
}
|
|
if exit != 0 {
|
|
return newExitError(exit)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// Sub-set of the standard `opa eval ..` flags
|
|
addPartialFlag(benchCommand.Flags(), ¶ms.partial, false)
|
|
addUnknownsFlag(benchCommand.Flags(), ¶ms.unknowns, []string{"input"})
|
|
addFailFlag(benchCommand.Flags(), ¶ms.fail, true)
|
|
addDataFlag(benchCommand.Flags(), ¶ms.dataPaths)
|
|
addBundleFlag(benchCommand.Flags(), ¶ms.bundlePaths)
|
|
addInputFlag(benchCommand.Flags(), ¶ms.inputPath)
|
|
addImportFlag(benchCommand.Flags(), ¶ms.imports)
|
|
addPackageFlag(benchCommand.Flags(), ¶ms.pkg)
|
|
addQueryStdinFlag(benchCommand.Flags(), ¶ms.stdin)
|
|
addInputStdinFlag(benchCommand.Flags(), ¶ms.stdinInput)
|
|
addMetricsFlag(benchCommand.Flags(), ¶ms.metrics, true)
|
|
addOutputFormat(benchCommand.Flags(), params.outputFormat)
|
|
addIgnoreFlag(benchCommand.Flags(), ¶ms.ignore)
|
|
addSchemaFlags(benchCommand.Flags(), params.schema)
|
|
addTargetFlag(benchCommand.Flags(), params.target)
|
|
addV0CompatibleFlag(benchCommand.Flags(), ¶ms.v0Compatible, false)
|
|
addV1CompatibleFlag(benchCommand.Flags(), ¶ms.v1Compatible, false)
|
|
addReadAstValuesFromStoreFlag(benchCommand.Flags(), ¶ms.ReadAstValuesFromStore, false)
|
|
|
|
// Shared benchmark flags
|
|
addCountFlag(benchCommand.Flags(), ¶ms.count, "benchmark")
|
|
addBenchmemFlag(benchCommand.Flags(), ¶ms.benchMem, true)
|
|
|
|
addE2EFlag(benchCommand.Flags(), ¶ms.e2e, false, brand)
|
|
addConfigFileFlag(benchCommand.Flags(), ¶ms.configFile)
|
|
|
|
benchCommand.Flags().IntVar(¶ms.gracefulShutdownPeriod, "shutdown-grace-period", 10, "set the time (in seconds) that the server will wait to gracefully shut down. This flag is valid in 'e2e' mode only.")
|
|
benchCommand.Flags().IntVar(¶ms.shutdownWaitPeriod, "shutdown-wait-period", 0, "set the time (in seconds) that the server will wait before initiating shutdown. This flag is valid in 'e2e' mode only.")
|
|
|
|
root.AddCommand(benchCommand)
|
|
}
|
|
|
|
type benchRunner interface {
|
|
run(ctx context.Context, ectx *evalContext, params benchmarkCommandParams, f func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error)
|
|
}
|
|
|
|
func benchMain(args []string, params benchmarkCommandParams, w io.Writer, stderr io.Writer, r benchRunner) (int, error) {
|
|
|
|
ctx := context.Background()
|
|
|
|
if params.e2e {
|
|
err := benchE2E(ctx, args, params, w)
|
|
if err != nil {
|
|
errRender := renderBenchmarkError(params, err, w, stderr)
|
|
return 1, errRender
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
ectx, err := setupEval(args, params.evalCommandParams)
|
|
if err != nil {
|
|
errRender := renderBenchmarkError(params, err, w, stderr)
|
|
return 1, errRender
|
|
}
|
|
|
|
resultHandler := rego.GenerateJSON(func(*ast.Term, *rego.EvalContext) (any, error) {
|
|
// Do nothing with the result, as we are only interested in benchmarking evaluation —
|
|
// not the potentially slow process of rendering the result.
|
|
// Undefined / empty results will still be handled normally (fail the benchmark unless --fail
|
|
// is set to false).
|
|
return nil, nil
|
|
})
|
|
|
|
ectx.regoArgs = append(ectx.regoArgs, resultHandler)
|
|
|
|
var benchFunc func(context.Context, ...rego.EvalOption) error
|
|
rg := rego.New(ectx.regoArgs...)
|
|
|
|
if !params.partial {
|
|
// Take the eval context and prepare anything else we possible can before benchmarking the evaluation
|
|
pq, err := rg.PrepareForEval(ctx)
|
|
if err != nil {
|
|
errRender := renderBenchmarkError(params, err, w, stderr)
|
|
return 1, errRender
|
|
}
|
|
|
|
benchFunc = func(ctx context.Context, opts ...rego.EvalOption) error {
|
|
result, err := pq.Eval(ctx, opts...)
|
|
if err != nil {
|
|
return err
|
|
} else if len(result) == 0 && params.fail {
|
|
return errors.New("undefined result")
|
|
}
|
|
return nil
|
|
}
|
|
} else {
|
|
// As with normal evaluation, prepare as much as possible up front.
|
|
pq, err := rg.PrepareForPartial(ctx)
|
|
if err != nil {
|
|
errRender := renderBenchmarkError(params, err, w, stderr)
|
|
return 1, errRender
|
|
}
|
|
|
|
benchFunc = func(ctx context.Context, opts ...rego.EvalOption) error {
|
|
result, err := pq.Partial(ctx, opts...)
|
|
if err != nil {
|
|
return err
|
|
} else if len(result.Queries) == 0 && params.fail {
|
|
return errors.New("undefined result")
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Run the benchmark as many times as specified, re-use the prepared objects for each
|
|
for range params.count {
|
|
br, err := r.run(ctx, ectx, params, benchFunc)
|
|
if err != nil {
|
|
errRender := renderBenchmarkError(params, err, w, stderr)
|
|
return 1, errRender
|
|
}
|
|
renderBenchmarkResult(params, br, w)
|
|
}
|
|
|
|
return 0, nil
|
|
}
|
|
|
|
type goBenchRunner struct {
|
|
}
|
|
|
|
func (*goBenchRunner) run(ctx context.Context, ectx *evalContext, params benchmarkCommandParams, f func(context.Context, ...rego.EvalOption) error) (testing.BenchmarkResult, error) {
|
|
|
|
var hist, m metrics.Metrics
|
|
if params.metrics {
|
|
hist = metrics.New()
|
|
m = metrics.New()
|
|
}
|
|
|
|
ectx.evalArgs = append(ectx.evalArgs, rego.EvalMetrics(m))
|
|
|
|
var benchErr error
|
|
|
|
br := testing.Benchmark(func(b *testing.B) {
|
|
// Track memory allocations, if enabled
|
|
if params.benchMem {
|
|
b.ReportAllocs()
|
|
}
|
|
|
|
// Reset the histogram for each invocation of the bench function
|
|
if params.metrics {
|
|
hist.Clear()
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for range b.N {
|
|
|
|
// Start the timer (might already be started, but that's ok)
|
|
b.StartTimer()
|
|
|
|
// Perform the evaluation
|
|
err := f(ctx, ectx.evalArgs...)
|
|
|
|
// Stop the timer while processing the results
|
|
b.StopTimer()
|
|
if err != nil {
|
|
benchErr = err
|
|
b.FailNow()
|
|
}
|
|
|
|
// Add metrics for that evaluation into the top level histogram
|
|
if params.metrics {
|
|
for name, metric := range m.All() {
|
|
// Note: We only support int64 metrics right now, this should cover pretty
|
|
// much all the ones we would care about (timers and counters).
|
|
switch v := metric.(type) {
|
|
case int64:
|
|
hist.Histogram(name).Update(v)
|
|
}
|
|
}
|
|
m.Clear()
|
|
}
|
|
}
|
|
|
|
if params.metrics {
|
|
reportMetrics(b, hist.All())
|
|
}
|
|
})
|
|
|
|
return br, benchErr
|
|
}
|
|
|
|
func benchE2E(ctx context.Context, args []string, params benchmarkCommandParams, w io.Writer) error {
|
|
host := "localhost"
|
|
port := 0
|
|
|
|
logger := logging.New()
|
|
logger.SetLevel(logging.Error)
|
|
|
|
paths := params.dataPaths.v
|
|
if len(params.bundlePaths.v) > 0 {
|
|
paths = append(paths, params.bundlePaths.v...)
|
|
}
|
|
|
|
// Because of test concurrency, several instances of this function can be
|
|
// running simultaneously, which will result in occasional collisions when
|
|
// two goroutines wish to bind the same port for the runtime.
|
|
// We fix the issue here by binding port 0; this will result in the OS
|
|
// allocating us an open port.
|
|
rtParams := runtime.Params{
|
|
Addrs: &[]string{host + ":0"},
|
|
Paths: paths,
|
|
Logger: logger,
|
|
BundleMode: params.bundlePaths.isFlagSet(),
|
|
SkipBundleVerification: true,
|
|
EnableVersionCheck: false,
|
|
GracefulShutdownPeriod: params.gracefulShutdownPeriod,
|
|
ShutdownWaitPeriod: params.shutdownWaitPeriod,
|
|
ConfigFile: params.configFile,
|
|
V0Compatible: params.v0Compatible,
|
|
V1Compatible: params.v1Compatible,
|
|
}
|
|
|
|
rt, err := runtime.NewRuntime(ctx, rtParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
initChannel := rt.Manager.ServerInitializedChannel()
|
|
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- rt.Serve(cctx)
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case <-initChannel:
|
|
break
|
|
}
|
|
|
|
// Busy loop until server has truly come online to recover the bound port.
|
|
// We do this with exponential backoff for wait times, since the server
|
|
// typically comes online very quickly.
|
|
baseDelay := time.Duration(100) * time.Millisecond
|
|
maxDelay := time.Duration(60) * time.Second
|
|
retries := 3 // Max of around 1 minute total wait time.
|
|
for i := range retries {
|
|
if len(rt.Addrs()) == 0 {
|
|
delay := util.DefaultBackoff(float64(baseDelay), float64(maxDelay), i)
|
|
time.Sleep(delay)
|
|
continue
|
|
}
|
|
// We have an address to parse the port from.
|
|
port, err = strconv.Atoi(strings.Split(rt.Addrs()[0], ":")[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
// Check for port still being unbound after retry loop.
|
|
if port == 0 {
|
|
return errors.New("unable to bind a port for bench testing")
|
|
}
|
|
|
|
query, err := readQuery(params, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
input, err := readInputBytes(params.evalCommandParams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wrap input in "input" attribute
|
|
inp := make(map[string]any)
|
|
|
|
if input != nil {
|
|
if err = util.Unmarshal(input, &inp); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
body := map[string]any{"input": inp}
|
|
|
|
var path string
|
|
if params.partial {
|
|
path = "compile"
|
|
body["query"] = query
|
|
if len(params.unknowns) > 0 {
|
|
body["unknowns"] = params.unknowns
|
|
}
|
|
} else {
|
|
_, err := ast.ParseBody(query)
|
|
if err != nil {
|
|
return errors.New("error occurred while parsing query")
|
|
}
|
|
|
|
if strings.HasPrefix(query, "data.") {
|
|
path = strings.ReplaceAll(query, ".", "/")
|
|
} else {
|
|
path = "query"
|
|
body["query"] = query
|
|
}
|
|
}
|
|
|
|
url := fmt.Sprintf("http://%s:%d/v1/%v", host, port, path)
|
|
if params.metrics {
|
|
url += "?metrics=true"
|
|
}
|
|
|
|
for range params.count {
|
|
br, err := runE2E(params, url, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
renderBenchmarkResult(params, br, w)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runE2E(params benchmarkCommandParams, url string, input map[string]any) (testing.BenchmarkResult, error) {
|
|
hist := metrics.New()
|
|
|
|
var benchErr error
|
|
|
|
br := testing.Benchmark(func(b *testing.B) {
|
|
|
|
// Track memory allocations, if enabled
|
|
if params.benchMem {
|
|
b.ReportAllocs()
|
|
}
|
|
|
|
// Reset the histogram for each invocation of the bench function
|
|
hist.Clear()
|
|
|
|
b.ResetTimer()
|
|
for range b.N {
|
|
|
|
// Start the timer
|
|
b.StartTimer()
|
|
|
|
// Execute the query API call
|
|
m, err := e2eQuery(params, url, input)
|
|
|
|
// Stop the timer while processing the results
|
|
b.StopTimer()
|
|
if err != nil {
|
|
benchErr = err
|
|
b.FailNow()
|
|
}
|
|
|
|
// Add metrics for that evaluation into the top level histogram
|
|
if params.metrics {
|
|
for name, metric := range m {
|
|
switch v := metric.(type) {
|
|
case json.Number:
|
|
num, err := v.Int64()
|
|
if err != nil {
|
|
benchErr = err
|
|
b.FailNow()
|
|
}
|
|
hist.Histogram(name).Update(num)
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.metrics {
|
|
reportMetrics(b, hist.All())
|
|
}
|
|
})
|
|
|
|
return br, benchErr
|
|
}
|
|
|
|
func e2eQuery(params benchmarkCommandParams, url string, input map[string]any) (types.MetricsV1, error) {
|
|
|
|
reqBody, err := json.Marshal(input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unexpected error: %w", err)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
var e map[string]any
|
|
if err = util.Unmarshal(body, &e); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, ok := e["errors"]; !ok {
|
|
return nil, fmt.Errorf("request failed, OPA server replied with HTTP %v: %v", resp.StatusCode, e["message"])
|
|
}
|
|
|
|
bs, err := json.Marshal(e["errors"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var astErrs ast.Errors
|
|
if err = util.Unmarshal(bs, &astErrs); err != nil {
|
|
// ignore err
|
|
return nil, fmt.Errorf("request failed, OPA server replied with HTTP %v: %v", resp.StatusCode, e["message"])
|
|
}
|
|
|
|
return nil, astErrs
|
|
}
|
|
|
|
if !params.partial {
|
|
var result types.DataResponseV1
|
|
if err = util.Unmarshal(body, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if result.Result == nil && params.fail {
|
|
return nil, errors.New("undefined result")
|
|
}
|
|
|
|
return result.Metrics, nil
|
|
}
|
|
|
|
var result types.CompileResponseV1
|
|
if err = util.Unmarshal(body, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if params.fail {
|
|
if result.Result == nil {
|
|
return nil, errors.New("undefined result")
|
|
}
|
|
|
|
i := *result.Result
|
|
|
|
peResult, ok := i.(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("invalid result for compile response")
|
|
}
|
|
|
|
if len(peResult) == 0 {
|
|
return nil, errors.New("undefined result")
|
|
}
|
|
|
|
if val, ok := peResult["queries"]; ok {
|
|
queries, ok := val.([]any)
|
|
if !ok {
|
|
return nil, errors.New("invalid result for output of partial evaluation")
|
|
}
|
|
|
|
if len(queries) == 0 {
|
|
return nil, errors.New("undefined result")
|
|
}
|
|
} else {
|
|
return nil, errors.New("invalid result for output of partial evaluation")
|
|
}
|
|
}
|
|
|
|
return result.Metrics, nil
|
|
}
|
|
|
|
func readQuery(params benchmarkCommandParams, args []string) (string, error) {
|
|
var query string
|
|
if params.stdin {
|
|
bs, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
query = string(bs)
|
|
} else {
|
|
query = args[0]
|
|
}
|
|
return query, nil
|
|
}
|
|
|
|
func renderBenchmarkResult(params benchmarkCommandParams, br testing.BenchmarkResult, w io.Writer) {
|
|
switch params.outputFormat.String() {
|
|
case formats.JSON:
|
|
_ = presentation.JSON(w, br)
|
|
case formats.GoBench:
|
|
fmt.Fprintf(w, "BenchmarkOPAEval\t%s", br.String())
|
|
if params.benchMem {
|
|
fmt.Fprintf(w, "\t%s", br.MemString())
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
default:
|
|
data := [][]string{
|
|
{"samples", strconv.Itoa(br.N)},
|
|
{"ns/op", prettyFormatFloat(float64(br.T.Nanoseconds()) / float64(br.N))},
|
|
}
|
|
if params.benchMem {
|
|
data = append(data, []string{
|
|
"B/op", strconv.FormatInt(br.AllocedBytesPerOp(), 10),
|
|
}, []string{
|
|
"allocs/op", strconv.FormatInt(br.AllocsPerOp(), 10),
|
|
})
|
|
}
|
|
|
|
for _, k := range util.KeysSorted(br.Extra) {
|
|
data = append(data, []string{k, prettyFormatFloat(br.Extra[k])})
|
|
}
|
|
|
|
table := tablewriter.NewTable(w, tablewriter.WithAlignment(tw.Alignment{tw.AlignLeft, tw.AlignRight}))
|
|
_ = table.Bulk(data)
|
|
_ = table.Render()
|
|
}
|
|
}
|
|
|
|
func renderBenchmarkError(params benchmarkCommandParams, err error, w io.Writer, stderr io.Writer) error {
|
|
o := presentation.Output{
|
|
Errors: presentation.NewOutputErrors(err),
|
|
}
|
|
|
|
switch params.outputFormat.String() {
|
|
case formats.JSON:
|
|
return presentation.JSON(w, o)
|
|
default:
|
|
return presentation.Pretty(w, stderr, o)
|
|
}
|
|
}
|
|
|
|
// Same format used by testing/benchmark.go to format floating point output strings
|
|
// Using this keeps the results consistent between the "pretty" and "gobench" outputs.
|
|
func prettyFormatFloat(x float64) string {
|
|
// Print all numbers with 10 places before the decimal point
|
|
// and small numbers with three sig figs.
|
|
var format string
|
|
switch y := math.Abs(x); {
|
|
case y == 0 || y >= 99.95:
|
|
format = "%10.0f"
|
|
case y >= 9.995:
|
|
format = "%12.1f"
|
|
case y >= 0.9995:
|
|
format = "%13.2f"
|
|
case y >= 0.09995:
|
|
format = "%14.3f"
|
|
case y >= 0.009995:
|
|
format = "%15.4f"
|
|
case y >= 0.0009995:
|
|
format = "%16.5f"
|
|
default:
|
|
format = "%17.6f"
|
|
}
|
|
return fmt.Sprintf(format, x)
|
|
}
|
|
|
|
func reportMetrics(b *testing.B, m map[string]any) {
|
|
// For each histogram add their values to the benchmark results.
|
|
// Note: If there are many metrics this gets super verbose.
|
|
for histName, metric := range m {
|
|
histValues, ok := metric.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for metricName, rawValue := range histValues {
|
|
unit := fmt.Sprintf("%s_%s", histName, metricName)
|
|
|
|
// Only support histogram metrics that are a float64 or int64,
|
|
// this covers the stock implementation in metrics.Metrics
|
|
switch v := rawValue.(type) {
|
|
case int64:
|
|
b.ReportMetric(float64(v), unit)
|
|
case float64:
|
|
b.ReportMetric(v, unit)
|
|
}
|
|
}
|
|
}
|
|
}
|