| // 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 benchfmt |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "reflect" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| func parseAll(t *testing.T, data string, setup ...func(r *Reader, sr io.Reader)) ([]Record, *Reader) { |
| sr := strings.NewReader(data) |
| r := NewReader(sr, "test") |
| for _, f := range setup { |
| f(r, sr) |
| } |
| var out []Record |
| for r.Scan() { |
| switch rec := r.Result(); rec := rec.(type) { |
| case *Result: |
| res := rec.Clone() |
| // Wipe position information for comparisons. |
| res.fileName = "" |
| res.line = 0 |
| out = append(out, res) |
| case *SyntaxError, *UnitMetadata: |
| out = append(out, rec) |
| default: |
| t.Fatalf("unexpected result type %T", rec) |
| } |
| } |
| if err := r.Err(); err != nil { |
| t.Fatal("parsing failed: ", err) |
| } |
| return out, r |
| } |
| |
| func printRecord(w io.Writer, r Record) { |
| switch r := r.(type) { |
| case *Result: |
| for _, fc := range r.Config { |
| fmt.Fprintf(w, "{%s: %s} ", fc.Key, fc.Value) |
| } |
| fmt.Fprintf(w, "%s %d", r.Name.Full(), r.Iters) |
| for _, val := range r.Values { |
| fmt.Fprintf(w, " %v %s", val.Value, val.Unit) |
| } |
| fmt.Fprintf(w, "\n") |
| case *UnitMetadata: |
| fmt.Fprintf(w, "Unit: %+v\n", r) |
| case *SyntaxError: |
| fmt.Fprintf(w, "SyntaxError: %s\n", r) |
| default: |
| panic(fmt.Sprintf("unknown record type %T", r)) |
| } |
| } |
| |
| type resultBuilder struct { |
| res *Result |
| } |
| |
| func r(fullName string, iters int) *resultBuilder { |
| return &resultBuilder{ |
| &Result{ |
| Config: []Config{}, |
| Name: Name(fullName), |
| Iters: iters, |
| }, |
| } |
| } |
| |
| func (b *resultBuilder) config(keyVals ...string) *resultBuilder { |
| for i := 0; i < len(keyVals); i += 2 { |
| key, val := keyVals[i], keyVals[i+1] |
| file := true |
| if val[0] == '*' { |
| file = false |
| val = val[1:] |
| } |
| b.res.Config = append(b.res.Config, Config{key, []byte(val), file}) |
| } |
| return b |
| } |
| |
| func (b *resultBuilder) v(value float64, unit string) *resultBuilder { |
| var v Value |
| if unit == "ns/op" { |
| v = Value{Value: value * 1e-9, Unit: "sec/op", OrigValue: value, OrigUnit: unit} |
| } else { |
| v = Value{Value: value, Unit: unit} |
| } |
| b.res.Values = append(b.res.Values, v) |
| return b |
| } |
| |
| func compareRecords(t *testing.T, got, want []Record) { |
| t.Helper() |
| var diff bytes.Buffer |
| for i := 0; i < len(got) || i < len(want); i++ { |
| if i >= len(got) { |
| fmt.Fprintf(&diff, "[%d] got: none, want:\n", i) |
| printRecord(&diff, want[i]) |
| } else if i >= len(want) { |
| fmt.Fprintf(&diff, "[%d] want: none, got:\n", i) |
| printRecord(&diff, got[i]) |
| } else if !reflect.DeepEqual(got[i], want[i]) { |
| fmt.Fprintf(&diff, "[%d] got:\n", i) |
| printRecord(&diff, got[i]) |
| fmt.Fprintf(&diff, "[%d] want:\n", i) |
| printRecord(&diff, want[i]) |
| } |
| } |
| if diff.Len() != 0 { |
| t.Error(diff.String()) |
| } |
| } |
| |
| func TestReader(t *testing.T) { |
| type testCase struct { |
| name, input string |
| want []Record |
| } |
| for _, test := range []testCase{ |
| { |
| "basic", |
| `key: value |
| BenchmarkOne 100 1 ns/op 2 B/op |
| BenchmarkTwo 300 4.5 ns/op |
| `, |
| []Record{ |
| r("One", 100). |
| config("key", "value"). |
| v(1, "ns/op").v(2, "B/op").res, |
| r("Two", 300). |
| config("key", "value"). |
| v(4.5, "ns/op").res, |
| }, |
| }, |
| { |
| "weird", |
| ` |
| BenchmarkSpaces 1 1 ns/op |
| BenchmarkHugeVal 1 9999999999999999999999999999999 ns/op |
| BenchmarkEmSpace 1 1 ns/op |
| `, |
| []Record{ |
| r("Spaces", 1). |
| v(1, "ns/op").res, |
| r("HugeVal", 1). |
| v(9999999999999999999999999999999, "ns/op").res, |
| r("EmSpace", 1). |
| v(1, "ns/op").res, |
| }, |
| }, |
| { |
| "basic file keys", |
| `key1: value |
| : not a key |
| ab:not a key |
| a b: also not a key |
| key2: value |
| |
| BenchmarkOne 100 1 ns/op |
| `, |
| []Record{ |
| r("One", 100). |
| config("key1", "value", "key2", "value"). |
| v(1, "ns/op").res, |
| }, |
| }, |
| { |
| "bad lines", |
| `not a benchmark |
| BenchmarkMissingIter |
| BenchmarkBadIter abc |
| BenchmarkHugeIter 9999999999999999999999999999999 |
| BenchmarkMissingVal 100 |
| BenchmarkBadVal 100 abc |
| BenchmarkMissingUnit 100 1 |
| BenchmarkMissingUnit2 100 1 ns/op 2 |
| also not a benchmark |
| Unit |
| Unit ns/op blah |
| Unit ns/op a=1 |
| Unit ns/op a=2 |
| `, |
| []Record{ |
| &SyntaxError{"test", 2, "missing iteration count"}, |
| &SyntaxError{"test", 3, "parsing iteration count: invalid syntax"}, |
| &SyntaxError{"test", 4, "parsing iteration count: value out of range"}, |
| &SyntaxError{"test", 5, "missing measurements"}, |
| &SyntaxError{"test", 6, "parsing measurement: invalid syntax"}, |
| &SyntaxError{"test", 7, "missing units"}, |
| &SyntaxError{"test", 8, "missing units"}, |
| &SyntaxError{"test", 10, "missing unit"}, |
| &SyntaxError{"test", 11, "expected key=value"}, |
| &UnitMetadata{UnitMetadataKey{"sec/op", "a"}, "ns/op", "1", "test", 12}, |
| &SyntaxError{"test", 13, "metadata a of unit ns/op already set to 1"}, |
| }, |
| }, |
| { |
| "remove existing label", |
| `key: value |
| key: |
| BenchmarkOne 100 1 ns/op |
| `, |
| []Record{ |
| r("One", 100). |
| v(1, "ns/op").res, |
| }, |
| }, |
| { |
| "overwrite exiting label", |
| `key1: first |
| key2: second |
| key1: third |
| BenchmarkOne 100 1 ns/op |
| `, |
| []Record{ |
| r("One", 100). |
| config("key1", "third", "key2", "second"). |
| v(1, "ns/op").res, |
| }, |
| }, |
| { |
| "unit metadata", |
| `Unit ns/op a=1 b=2 |
| Unit ns/op c=3 error d=4 |
| # Repeated unit should report nothing |
| Unit ns/op d=4 |
| # Starts like a unit line but actually isn't |
| Unitx |
| BenchmarkOne 100 1 ns/op |
| `, |
| []Record{ |
| &UnitMetadata{UnitMetadataKey{"sec/op", "a"}, "ns/op", "1", "test", 1}, |
| &UnitMetadata{UnitMetadataKey{"sec/op", "b"}, "ns/op", "2", "test", 1}, |
| &UnitMetadata{UnitMetadataKey{"sec/op", "c"}, "ns/op", "3", "test", 2}, |
| &SyntaxError{"test", 2, "expected key=value"}, |
| &UnitMetadata{UnitMetadataKey{"sec/op", "d"}, "ns/op", "4", "test", 2}, |
| r("One", 100). |
| v(1, "ns/op").res, |
| }, |
| }, |
| } { |
| t.Run(test.name, func(t *testing.T) { |
| got, _ := parseAll(t, test.input) |
| compareRecords(t, got, test.want) |
| }) |
| } |
| } |
| |
| func TestReaderInternalConfig(t *testing.T) { |
| got, _ := parseAll(t, ` |
| # Test initial internal config |
| Benchmark1 100 1 ns/op |
| # Overwrite internal config with file config |
| key1: file1 |
| key3: file3 |
| Benchmark2 100 1 ns/op |
| # Delete internal config, check that file config is right |
| key2: |
| Benchmark3 100 1 ns/op |
| `, func(r *Reader, sr io.Reader) { |
| r.Reset(sr, "test", "key1", "internal1", "key2", "internal2") |
| }) |
| want := []Record{ |
| r("1", 100).v(1, "ns/op").config("key1", "*internal1", "key2", "*internal2").res, |
| r("2", 100).v(1, "ns/op").config("key1", "file1", "key2", "*internal2", "key3", "file3").res, |
| r("3", 100).v(1, "ns/op").config("key1", "file1", "key3", "file3").res, |
| } |
| compareRecords(t, got, want) |
| } |
| |
| func BenchmarkReader(b *testing.B) { |
| path := "testdata/bent" |
| fileInfos, err := ioutil.ReadDir(path) |
| if err != nil { |
| b.Fatal("reading test data directory: ", err) |
| } |
| |
| var files []*os.File |
| for _, info := range fileInfos { |
| f, err := os.Open(filepath.Join(path, info.Name())) |
| if err != nil { |
| b.Fatal(err) |
| } |
| defer f.Close() |
| files = append(files, f) |
| } |
| |
| b.ResetTimer() |
| |
| start := time.Now() |
| var n int |
| for i := 0; i < b.N; i++ { |
| r := new(Reader) |
| for _, f := range files { |
| if _, err := f.Seek(0, 0); err != nil { |
| b.Fatal("seeking to 0: ", err) |
| } |
| r.Reset(f, f.Name()) |
| for r.Scan() { |
| n++ |
| if err, ok := r.Result().(error); ok { |
| b.Fatal("malformed record: ", err) |
| } |
| } |
| if err := r.Err(); err != nil { |
| b.Fatal(err) |
| } |
| } |
| } |
| dur := time.Since(start) |
| b.Logf("read %d records", n) |
| |
| b.StopTimer() |
| b.ReportMetric(float64(n/b.N), "records/op") |
| b.ReportMetric(float64(n)*float64(time.Second)/float64(dur), "records/sec") |
| } |