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