| // 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 |
| } |