Files
Etienne Perot 84172e4e70 Go benchstat parser: Support alternate syntax for parameters.
This supports benchmark names where parameter-value pairs are separated
by `=` rather than `.`, and benchmarks where `GOMAXPROCS` is not appended
to the name of the benchmark.

This is used in Kubernetes benchmarks where the `GOMAXPROCS` value of the
machine running the Kubernetes client has no bearing on the benchmark's
performance.

This also moves the logic for how to handle sub-test names to the caller.
Docker benchmarks continue to have the behavior of treating sub-names
as a `Condition` with key equal to its value. Kubernetes benchmarks will
instead treat sub-test names as a single `Condition` called `subtest`.

PiperOrigin-RevId: 709861103
2024-12-26 12:51:43 -08:00

199 lines
6.4 KiB
Go

// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tools
import (
"fmt"
"regexp"
"strconv"
"strings"
"testing"
)
// Parameter is a test parameter.
type Parameter struct {
Name string
Value string
}
// Output is parsed and split by these values. Make them illegal in input methods.
// We are constrained on what characters these can be by 1) docker's allowable
// container names, 2) golang allowable benchmark names, and 3) golangs allowable
// characters in b.ReportMetric calls.
var illegalChars = regexp.MustCompile(`[/\.]`)
// ParametersToName joins parameters into a string format for parsing.
// It is meant to be used for t.Run() calls in benchmark tools.
func ParametersToName(params ...Parameter) (string, error) {
var strs []string
for _, param := range params {
if illegalChars.MatchString(param.Name) || illegalChars.MatchString(param.Value) {
return "", fmt.Errorf("params Name: %q and Value: %q cannot container '.' or '/'", param.Name, param.Value)
}
strs = append(strs, strings.Join([]string{param.Name, param.Value}, "."))
}
return strings.Join(strs, "/"), nil
}
// NameToParameters parses the string created by ParametersToName and returns
// the name components and parameters contained within.
// The separator between the name and value may either be '.' or '='.
//
// Example: "BenchmarkRuby/SubTest/LevelTwo/server_threads.1/doc_size.16KB-6"
// The parameter part of this benchmark is "server_threads.1/doc_size.16KB",
// whereas "BenchmarkRuby/SubTest/LevelTwo" is the name, and the "-6" suffix is
// GOMAXPROCS (optional, may be omitted).
// This function will return a slice of the name components of the benchmark:
//
// [
// "BenchmarkRuby",
// "SubTest",
// "LevelTwo",
// ]
//
// and a slice of the parameters:
//
// [
// {Name: "server_threads", Value: "1"},
// {Name: "doc_size", Value: "16KB"},
// {Name: "GOMAXPROCS", Value: "6"},
// ]
//
// (and a nil error).
func NameToParameters(name string) ([]string, []*Parameter, error) {
var params []*Parameter
var separator string
switch {
case strings.IndexRune(name, '.') != -1 && strings.IndexRune(name, '=') != -1:
return nil, nil, fmt.Errorf("ambiguity while parsing parameters from benchmark name %q: multiple types of parameter separators are present", name)
case strings.IndexRune(name, '.') != -1:
separator = "."
case strings.IndexRune(name, '=') != -1:
separator = "="
default:
// No separator; use '=' which we know is not present in the name,
// but we still need to process the name (even if unparameterized) in
// order to possibly extract GOMAXPROCS.
separator = "="
}
var nameComponents []string
var firstParameterCond string
var goMaxProcs *Parameter
split := strings.Split(name, "/")
for i, cond := range split {
if isLast := i == len(split)-1; isLast {
// On the last component, if it contains a dash, it is a GOMAXPROCS value.
if dashSplit := strings.Split(cond, "-"); len(dashSplit) >= 2 {
goMaxProcs = &Parameter{Name: "GOMAXPROCS", Value: dashSplit[len(dashSplit)-1]}
cond = strings.Join(dashSplit[:len(dashSplit)-1], "-")
}
}
cs := strings.Split(cond, separator)
switch len(cs) {
case 1:
if firstParameterCond != "" {
return nil, nil, fmt.Errorf("failed to parse params from %q: a non-parametrized component %q was found after a parametrized one %q", name, cond, firstParameterCond)
}
nameComponents = append(nameComponents, cond)
case 2:
if firstParameterCond == "" {
firstParameterCond = cond
}
params = append(params, &Parameter{Name: cs[0], Value: cs[1]})
default:
return nil, nil, fmt.Errorf("failed to parse params from %q: %s", name, cond)
}
}
if goMaxProcs != nil {
// GOMAXPROCS should always be last in order to match the ordering of the
// benchmark name.
params = append(params, goMaxProcs)
}
return nameComponents, params, nil
}
// ReportCustomMetric reports a metric in a set format for parsing.
func ReportCustomMetric(b *testing.B, value float64, name, unit string) {
if illegalChars.MatchString(name) || illegalChars.MatchString(unit) {
b.Fatalf("name: %q and unit: %q cannot contain '/' or '.'", name, unit)
}
nameUnit := strings.Join([]string{name, unit}, ".")
b.ReportMetric(value, nameUnit)
}
// Metric holds metric data parsed from a string based on the format
// ReportMetric.
type Metric struct {
Name string
Unit string
Sample float64
}
// ParseCustomMetric parses a metric reported with ReportCustomMetric.
func ParseCustomMetric(value, metric string) (*Metric, error) {
sample, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse value: %v", err)
}
separators := []rune{'-', '.'}
var separator string
for _, sep := range separators {
if strings.ContainsRune(metric, sep) {
if separator != "" {
return nil, fmt.Errorf("failed to parse metric: ambiguous unit separator: %q (is the separator %q or %q?)", metric, separator, string(sep))
}
separator = string(sep)
}
}
var name, unit string
switch separator {
case "":
unit = metric
default:
components := strings.Split(metric, separator)
name, unit = strings.Join(components[:len(components)-1], ""), components[len(components)-1]
}
// Normalize some unit names to benchstat defaults.
switch unit {
case "":
return nil, fmt.Errorf("failed to parse metric %q: no unit specified", metric)
case "s":
unit = "sec"
case "nanos":
unit = "ns"
case "byte":
unit = "B"
case "bit":
unit = "b"
default:
// Otherwise, leave unit as-is.
}
// If the metric name is unspecified, it can sometimes be inferred from
// the unit.
if name == "" {
switch unit {
case "sec":
name = "duration"
case "req/sec", "tok/sec":
name = "throughput"
case "B/sec":
name = "bandwidth"
default:
return nil, fmt.Errorf("failed to parse metric %q: ambiguous metric name, please format the unit as 'name.unit' or 'name-unit'", metric)
}
}
return &Metric{Name: name, Unit: unit, Sample: sample}, nil
}