blob: 846ca61d630c4a436605e2ab5e84780057282b68 [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 (
"fmt"
"strings"
"testing"
"golang.org/x/perf/benchfmt"
"golang.org/x/perf/benchproc/internal/parse"
)
// mustParse parses a single projection.
func mustParse(t *testing.T, proj string) (*Projection, *Filter) {
f, err := NewFilter("*")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
s, err := (&ProjectionParser{}).Parse(proj, f)
if err != nil {
t.Fatalf("unexpected error parsing %q: %v", proj, err)
}
return s, f
}
// r constructs a benchfmt.Result with the given full name and file
// config, which is specified as alternating key/value pairs. The
// result has 1 iteration and no values.
func r(t *testing.T, fullName string, fileConfig ...string) *benchfmt.Result {
res := &benchfmt.Result{
Name: benchfmt.Name(fullName),
Iters: 1,
}
if len(fileConfig)%2 != 0 {
t.Fatal("fileConfig must be alternating key/value pairs")
}
for i := 0; i < len(fileConfig); i += 2 {
cfg := benchfmt.Config{Key: fileConfig[i], Value: []byte(fileConfig[i+1]), File: true}
res.Config = append(res.Config, cfg)
}
return res
}
// p constructs a benchfmt.Result like r, then projects it using p.
func p(t *testing.T, p *Projection, fullName string, fileConfig ...string) Key {
res := r(t, fullName, fileConfig...)
return p.Project(res)
}
func TestProjectionBasic(t *testing.T) {
check := func(key Key, want string) {
t.Helper()
got := key.String()
if got != want {
t.Errorf("got %s, want %s", got, want)
}
}
var s *Projection
// Sub-name config.
s, _ = mustParse(t, ".fullname")
check(p(t, s, "Name/a=1"), ".fullname:Name/a=1")
s, _ = mustParse(t, "/a")
check(p(t, s, "Name/a=1"), "/a:1")
s, _ = mustParse(t, ".name")
check(p(t, s, "Name/a=1"), ".name:Name")
// Fixed file config.
s, _ = mustParse(t, "a")
check(p(t, s, "", "a", "1", "b", "2"), "a:1")
check(p(t, s, "", "b", "2"), "") // Missing values are omitted
check(p(t, s, "", "a", "", "b", "2"), "")
// Variable file config.
s, _ = mustParse(t, ".config")
check(p(t, s, "", "a", "1", "b", "2"), "a:1 b:2")
check(p(t, s, "", "c", "3"), "c:3")
check(p(t, s, "", "c", "3", "a", "2"), "a:2 c:3")
}
func TestProjectionIntern(t *testing.T) {
s, _ := mustParse(t, "a,b")
c12 := p(t, s, "", "a", "1", "b", "2")
if c12 != p(t, s, "", "a", "1", "b", "2") {
t.Errorf("Keys should be equal")
}
if c12 == p(t, s, "", "a", "1", "b", "3") {
t.Errorf("Keys should not be equal")
}
if c12 != p(t, s, "", "a", "1", "b", "2", "c", "3") {
t.Errorf("Keys should be equal")
}
}
func fieldNames(fields []*Field) string {
names := new(strings.Builder)
for i, f := range fields {
if i > 0 {
names.WriteByte(' ')
}
names.WriteString(f.Name)
}
return names.String()
}
func TestProjectionParsing(t *testing.T) {
// Basic parsing is tested by the syntax package. Here we test
// additional processing done by this package.
check := func(proj string, want, wantFlat string) {
t.Helper()
s, _ := mustParse(t, proj)
got := fieldNames(s.Fields())
if got != want {
t.Errorf("%s: got fields %v, want %v", proj, got, want)
}
gotFlat := fieldNames(s.FlattenedFields())
if gotFlat != wantFlat {
t.Errorf("%s: got flat fields %v, want %v", proj, gotFlat, wantFlat)
}
}
checkErr := func(proj, error string, pos int) {
t.Helper()
f, _ := NewFilter("*")
_, err := (&ProjectionParser{}).Parse(proj, f)
if se, _ := err.(*parse.SyntaxError); se == nil || se.Msg != error || se.Off != pos {
t.Errorf("%s: want error %s at %d; got %s", proj, error, pos, err)
}
}
check("a,b,c", "a b c", "a b c")
check("a,.config,c", "a .config c", "a c") // .config hasn't been populated with anything yet
check("a,.fullname,c", "a .fullname c", "a .fullname c")
check("a,.name,c", "a .name c", "a .name c")
check("a,/b,c", "a /b c", "a /b c")
checkErr("a@foo", "unknown order \"foo\"", 2)
checkErr(".config@(1 2)", "fixed order not allowed for .config", 8)
}
func TestProjectionFiltering(t *testing.T) {
_, f := mustParse(t, "a@(a b c)")
check := func(val string, want bool) {
t.Helper()
res := r(t, "", "a", val)
got, _ := f.Apply(res)
if want != got {
t.Errorf("%s: want %v, got %v", val, want, got)
}
}
check("a", true)
check("b", true)
check("aa", false)
check("z", false)
}
func TestProjectionExclusion(t *testing.T) {
// The underlying name normalization has already been tested
// thoroughly in benchfmt/extract_test.go, so here we just
// have to test that it's being plumbed right.
check := func(key Key, want string) {
t.Helper()
got := key.String()
if got != want {
t.Errorf("got %s, want %s", got, want)
}
}
// Create the main projection.
var pp ProjectionParser
f, _ := NewFilter("*")
s, err := pp.Parse(".fullname,.config", f)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
// Parse specific keys that should be excluded from fullname
// and config.
_, err = pp.Parse(".name,/a,exc", f)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
check(p(t, s, "Name"), ".fullname:*")
check(p(t, s, "Name/a=1"), ".fullname:*")
check(p(t, s, "Name/a=1/b=2"), ".fullname:*/b=2")
check(p(t, s, "Name", "exc", "1"), ".fullname:*")
check(p(t, s, "Name", "exc", "1", "abc", "2"), ".fullname:* abc:2")
check(p(t, s, "Name", "abc", "2"), ".fullname:* abc:2")
}
func TestProjectionResidue(t *testing.T) {
check := func(mainProj string, want string) {
t.Helper()
// Get the residue of mainProj.
var pp ProjectionParser
f, _ := NewFilter("*")
_, err := pp.Parse(mainProj, f)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
s := pp.Residue()
// Project a test result.
key := p(t, s, "Name/a=1/b=2", "x", "3", "y", "4")
got := key.String()
if got != want {
t.Errorf("got %s, want %s", got, want)
}
}
// Full residue.
check("", "x:3 y:4 .fullname:Name/a=1/b=2")
// Empty residue.
check(".config,.fullname", "")
// Partial residues.
check("x,/a", "y:4 .fullname:Name/b=2")
check(".config", ".fullname:Name/a=1/b=2")
check(".fullname", "x:3 y:4")
check(".name", "x:3 y:4 .fullname:*/a=1/b=2")
}
// ExampleProjectionParser_Residue demonstrates residue projections.
//
// This example groups a set of results by .fullname and goos, but the
// two results for goos:linux have two different goarch values,
// indicating the user probably unintentionally grouped uncomparable
// results together. The example uses ProjectionParser.Residue and
// NonSingularFields to warn the user about this.
func ExampleProjectionParser_Residue() {
var pp ProjectionParser
p, _ := pp.Parse(".fullname,goos", nil)
residue := pp.Residue()
// Aggregate each result by p and track the residue of each group.
type group struct {
values []float64
residues []Key
}
groups := make(map[Key]*group)
var keys []Key
for _, result := range results(`
goos: linux
goarch: amd64
BenchmarkAlloc 1 128 ns/op
goos: linux
goarch: arm64
BenchmarkAlloc 1 137 ns/op
goos: darwin
goarch: amd64
BenchmarkAlloc 1 130 ns/op`) {
// Map result to a group.
key := p.Project(result)
g, ok := groups[key]
if !ok {
g = new(group)
groups[key] = g
keys = append(keys, key)
}
// Add value to the group.
speed, _ := result.Value("sec/op")
g.values = append(g.values, speed)
// Add residue to the group.
g.residues = append(g.residues, residue.Project(result))
}
// Report aggregated results.
SortKeys(keys)
for _, k := range keys {
g := groups[k]
// Report the result.
fmt.Println(k, mean(g.values), "sec/op")
// Check if the grouped results vary in some unexpected way.
nonsingular := NonSingularFields(g.residues)
if len(nonsingular) > 0 {
// Report a potential issue.
fmt.Printf("warning: results vary in %s and may be uncomparable\n", nonsingular)
}
}
// Output:
// .fullname:Alloc goos:linux 1.325e-07 sec/op
// warning: results vary in [goarch] and may be uncomparable
// .fullname:Alloc goos:darwin 1.3e-07 sec/op
}
func results(data string) []*benchfmt.Result {
var out []*benchfmt.Result
r := benchfmt.NewReader(strings.NewReader(data), "<string>")
for r.Scan() {
switch rec := r.Result(); rec := rec.(type) {
case *benchfmt.Result:
out = append(out, rec.Clone())
case *benchfmt.SyntaxError:
panic("unexpected error in test data: " + rec.Error())
}
}
return out
}
func mean(xs []float64) float64 {
var sum float64
for _, x := range xs {
sum += x
}
return sum / float64(len(xs))
}
func TestProjectionValues(t *testing.T) {
s, unit, err := (&ProjectionParser{}).ParseWithUnit("x", nil)
if err != nil {
t.Fatalf("unexpected error parsing %q: %v", "x", err)
}
check := func(key Key, want, wantUnit string) {
t.Helper()
got := key.String()
if got != want {
t.Errorf("got %s, want %s", got, want)
}
gotUnit := key.Get(unit)
if gotUnit != wantUnit {
t.Errorf("got unit %s, want %s", gotUnit, wantUnit)
}
}
res := r(t, "Name", "x", "1")
res.Values = []benchfmt.Value{{Value: 100, Unit: "ns/op"}, {Value: 1.21, Unit: "gigawatts"}}
keys := s.ProjectValues(res)
if len(keys) != len(res.Values) {
t.Fatalf("got %d Keys, want %d", len(keys), len(res.Values))
}
check(keys[0], "x:1 .unit:ns/op", "ns/op")
check(keys[1], "x:1 .unit:gigawatts", "gigawatts")
}