blob: 1722258f4a5f0928aedbf3fe180c35eb2fbd295f [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package benchproc
import (
"bytes"
"fmt"
"strings"
"golang.org/x/perf/benchfmt"
)
// An extractor returns some field of a benchmark result. The
// result may be a view into a mutable []byte in *benchfmt.Result, so
// it may change if the Result is modified.
type extractor func(*benchfmt.Result) []byte
// newExtractor returns a function that extracts some field of a
// benchmark result.
//
// The key must be one of the following:
//
// - ".name" for the benchmark name (excluding per-benchmark
// configuration).
//
// - ".fullname" for the full benchmark name (including per-benchmark
// configuration).
//
// - "/{key}" for a benchmark sub-name key. This may be "/gomaxprocs"
// and the extractor will normalize the name as needed.
//
// - Any other string is a file configuration key.
func newExtractor(key string) (extractor, error) {
if len(key) == 0 {
return nil, fmt.Errorf("key must not be empty")
}
switch {
case key == ".config", key == ".unit":
// The caller should already have handled this more gracefully.
panic(key + " is not an extractor")
case key == ".name":
return extractName, nil
case key == ".fullname":
return extractFull, nil
case strings.HasPrefix(key, "/"):
// Construct the byte prefix to search for.
prefix := make([]byte, len(key)+1)
copy(prefix, key)
prefix[len(prefix)-1] = '='
isGomaxprocs := key == "/gomaxprocs"
return func(res *benchfmt.Result) []byte {
return extractNamePart(res, prefix, isGomaxprocs)
}, nil
}
return func(res *benchfmt.Result) []byte {
return extractConfig(res, key)
}, nil
}
// newExtractorFullName returns an extractor for the full name of a
// benchmark, but optionally with the base name or sub-name
// configuration keys excluded. Any excluded sub-name keys will be
// deleted from the name. If ".name" is excluded, the name will be
// normalized to "*". This will ignore anything in the exclude list that
// isn't in the form of a /-prefixed sub-name key or ".name".
func newExtractorFullName(exclude []string) extractor {
// Extract the sub-name keys, turn them into substrings and
// construct their normalized replacement.
//
// It's important that we fully delete excluded sub-name keys rather
// than, say, normalizing them to "*". Simply normalizing them will
// leak whether or not they are present into the result, resulting
// in different strings. This is most noticeable when excluding
// /gomaxprocs: since this is already omitted if it's 1, benchmarks
// running at /gomaxprocs of 1 won't merge with benchmarks at higher
// /gomaxprocs.
var delete [][]byte
excName := false
excGomaxprocs := false
for _, k := range exclude {
if k == ".name" {
excName = true
}
if !strings.HasPrefix(k, "/") {
continue
}
delete = append(delete, append([]byte(k), '='))
if k == "/gomaxprocs" {
excGomaxprocs = true
}
}
if len(delete) == 0 && !excName && !excGomaxprocs {
return extractFull
}
return func(res *benchfmt.Result) []byte {
return extractFullExcluded(res, delete, excName, excGomaxprocs)
}
}
func extractName(res *benchfmt.Result) []byte {
return res.Name.Base()
}
func extractFull(res *benchfmt.Result) []byte {
return res.Name.Full()
}
func extractFullExcluded(res *benchfmt.Result, delete [][]byte, excName, excGomaxprocs bool) []byte {
name := res.Name.Full()
found := false
if excName {
found = true
}
if !found {
for _, k := range delete {
if bytes.Contains(name, k) {
found = true
break
}
}
}
if !found && excGomaxprocs && bytes.IndexByte(name, '-') >= 0 {
found = true
}
if !found {
// No need to transform name.
return name
}
// Delete excluded keys from the name.
base, parts := res.Name.Parts()
var newName []byte
if excName {
newName = append(newName, '*')
} else {
newName = append(newName, base...)
}
outer:
for _, part := range parts {
for _, k := range delete {
if bytes.HasPrefix(part, k) {
// Skip this part.
continue outer
}
}
if excGomaxprocs && part[0] == '-' {
// Skip gomaxprocs.
continue outer
}
newName = append(newName, part...)
}
return newName
}
func extractNamePart(res *benchfmt.Result, prefix []byte, isGomaxprocs bool) []byte {
_, parts := res.Name.Parts()
if isGomaxprocs && len(parts) > 0 {
last := parts[len(parts)-1]
if last[0] == '-' {
// GOMAXPROCS specified as "-N" suffix.
return last[1:]
}
}
// Search for the prefix.
for _, part := range parts {
if bytes.HasPrefix(part, prefix) {
return part[len(prefix):]
}
}
// Not found.
return nil
}
func extractConfig(res *benchfmt.Result, key string) []byte {
pos, ok := res.ConfigIndex(key)
if !ok {
return nil
}
return res.Config[pos].Value
}