storage/app: add BenchmarkReader struct
BenchmarkReader is capable of reading standard benchmark files into
Result objects and writing those Result objects back out to a Writer.
Change-Id: I022221f53b5d3ce1de7e8e7b74d265a50ac4a0eb
Reviewed-on: https://go-review.googlesource.com/34627
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/app/benchfmt.go b/storage/app/benchfmt.go
new file mode 100644
index 0000000..b115950
--- /dev/null
+++ b/storage/app/benchfmt.go
@@ -0,0 +1,227 @@
+// Copyright 2016 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 app
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "sort"
+ "strconv"
+ "strings"
+ "unicode"
+)
+
+// BenchmarkReader reads benchmark results from an io.Reader.
+type BenchmarkReader struct {
+ s *bufio.Scanner
+ labels map[string]string
+ lineNum int
+}
+
+// NewBenchmarkReader creates a BenchmarkReader that reads from r.
+func NewBenchmarkReader(r io.Reader) *BenchmarkReader {
+ return &BenchmarkReader{
+ s: bufio.NewScanner(r),
+ labels: make(map[string]string),
+ }
+}
+
+// AddLabels adds additional labels as if they had been read from the file.
+// It must be called before the first call to r.Next.
+func (r *BenchmarkReader) AddLabels(labels map[string]string) {
+ for k, v := range labels {
+ r.labels[k] = v
+ }
+}
+
+// TODO: It would probably be helpful to add a named type for
+// map[string]string with String(), Keys(), and Equal() methods.
+
+// Result represents a single line from a benchmark file.
+// All information about that line is self-contained in the Result.
+type Result struct {
+ // Labels is the set of persistent labels that apply to the result.
+ // Labels must not be modified.
+ Labels map[string]string
+ // NameLabels is the set of ephemeral labels that were parsed
+ // from the benchmark name/line.
+ // NameLabels must not be modified.
+ NameLabels map[string]string
+ // LineNum is the line number on which the result was found
+ LineNum int
+ // Content is the verbatim input line of the benchmark file, beginning with the string "Benchmark".
+ Content string
+}
+
+// A BenchmarkPrinter prints a sequence of benchmark results.
+type BenchmarkPrinter struct {
+ w io.Writer
+ labels map[string]string
+}
+
+// NewBenchmarkPrinter constructs a BenchmarkPrinter writing to w.
+func NewBenchmarkPrinter(w io.Writer) *BenchmarkPrinter {
+ return &BenchmarkPrinter{w: w}
+}
+
+// Print writes the lines necessary to recreate r.
+func (bp *BenchmarkPrinter) Print(r *Result) error {
+ var keys []string
+ // Print removed keys first.
+ for k := range bp.labels {
+ if r.Labels[k] == "" {
+ keys = append(keys, k)
+ }
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ if _, err := fmt.Fprintf(bp.w, "%s:\n", k); err != nil {
+ return err
+ }
+ }
+ // Then print new or changed keys.
+ keys = keys[:0]
+ for k, v := range r.Labels {
+ if v != "" && bp.labels[k] != v {
+ keys = append(keys, k)
+ }
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ if _, err := fmt.Fprintf(bp.w, "%s: %s\n", k, r.Labels[k]); err != nil {
+ return err
+ }
+ }
+ // Finally print the actual line itself.
+ if _, err := fmt.Fprintf(bp.w, "%s\n", r.Content); err != nil {
+ return err
+ }
+ bp.labels = r.Labels
+ return nil
+}
+
+// parseNameLabels extracts extra labels from a benchmark name and sets them in labels.
+func parseNameLabels(name string, labels map[string]string) {
+ dash := strings.LastIndex(name, "-")
+ if dash >= 0 {
+ // Accept -N as an alias for /GOMAXPROCS=N
+ _, err := strconv.Atoi(name[dash+1:])
+ if err == nil {
+ labels["GOMAXPROCS"] = name[dash+1:]
+ name = name[:dash]
+ }
+ }
+ parts := strings.Split(name, "/")
+ labels["name"] = parts[0]
+ for i, sub := range parts[1:] {
+ equals := strings.Index(sub, "=")
+ var key string
+ if equals >= 0 {
+ key, sub = sub[:equals], sub[equals+1:]
+ } else {
+ key = fmt.Sprintf("sub%d", i+1)
+ }
+ labels[key] = sub
+ }
+}
+
+// newResult parses a line and returns a Result object for the line.
+func newResult(labels map[string]string, lineNum int, name, content string) *Result {
+ r := &Result{
+ Labels: labels,
+ NameLabels: make(map[string]string),
+ LineNum: lineNum,
+ Content: content,
+ }
+ parseNameLabels(name, r.NameLabels)
+ return r
+}
+
+// copyLabels makes a new copy of the labels map, to protect against
+// future modifications to labels.
+func copyLabels(labels map[string]string) map[string]string {
+ new := make(map[string]string)
+ for k, v := range labels {
+ new[k] = v
+ }
+ return new
+}
+
+// TODO(quentin): How to represent and efficiently group multiple lines?
+
+// Next returns the next benchmark result from the file. If there are
+// no further results, it returns nil, io.EOF.
+func (r *BenchmarkReader) Next() (*Result, error) {
+ copied := false
+ for r.s.Scan() {
+ r.lineNum++
+ line := r.s.Text()
+ if key, value, ok := parseKeyValueLine(line); ok {
+ if !copied {
+ copied = true
+ r.labels = copyLabels(r.labels)
+ }
+ // TODO(quentin): Spec says empty value is valid, but
+ // we need a way to cancel previous labels, so we'll
+ // treat an empty value as a removal.
+ if value == "" {
+ delete(r.labels, key)
+ } else {
+ r.labels[key] = value
+ }
+ continue
+ }
+ if fullName, ok := parseBenchmarkLine(line); ok {
+ return newResult(r.labels, r.lineNum, fullName, line), nil
+ }
+ }
+ if err := r.s.Err(); err != nil {
+ return nil, err
+ }
+ return nil, io.EOF
+}
+
+// parseKeyValueLine attempts to parse line as a key: value pair. ok
+// indicates whether the line could be parsed.
+func parseKeyValueLine(line string) (key, val string, ok bool) {
+ for i, c := range line {
+ if i == 0 && !unicode.IsLower(c) {
+ return
+ }
+ if unicode.IsSpace(c) || unicode.IsUpper(c) {
+ return
+ }
+ if i > 0 && c == ':' {
+ key = line[:i]
+ val = line[i+1:]
+ break
+ }
+ }
+ if val == "" {
+ ok = true
+ return
+ }
+ for len(val) > 0 && (val[0] == ' ' || val[0] == '\t') {
+ val = val[1:]
+ ok = true
+ }
+ return
+}
+
+// parseBenchmarkLine attempts to parse line as a benchmark result. If
+// successful, fullName is the name of the benchmark with the
+// "Benchmark" prefix stripped, and ok is true.
+func parseBenchmarkLine(line string) (fullName string, ok bool) {
+ space := strings.IndexFunc(line, unicode.IsSpace)
+ if space < 0 {
+ return
+ }
+ name := line[:space]
+ if !strings.HasPrefix(name, "Benchmark") {
+ return
+ }
+ return name[len("Benchmark"):], true
+}
diff --git a/storage/app/benchfmt_test.go b/storage/app/benchfmt_test.go
new file mode 100644
index 0000000..0e9d763
--- /dev/null
+++ b/storage/app/benchfmt_test.go
@@ -0,0 +1,207 @@
+// Copyright 2016 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 app
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func readAllResults(t *testing.T, r *BenchmarkReader) []*Result {
+ var out []*Result
+ for {
+ result, err := r.Next()
+ switch err {
+ case io.EOF:
+ return out
+ case nil:
+ out = append(out, result)
+ default:
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestBenchmarkReader(t *testing.T) {
+ type kv map[string]string
+ tests := []struct {
+ name, input string
+ want []*Result
+ }{
+ {
+ "basic",
+ `key: value
+BenchmarkOne 1 ns/sec
+`,
+ []*Result{{
+ kv{"key": "value"},
+ kv{"name": "One"},
+ 2,
+ "BenchmarkOne 1 ns/sec",
+ }},
+ },
+ {
+ "two results with indexed and named subnames",
+ `key: value
+BenchmarkOne/foo/bar=1-2 1 ns/sec
+BenchmarkTwo 2 ns/sec
+`,
+ []*Result{
+ {
+ kv{"key": "value"},
+ kv{"name": "One", "sub1": "foo", "bar": "1", "GOMAXPROCS": "2"},
+ 2,
+ "BenchmarkOne/foo/bar=1-2 1 ns/sec",
+ },
+ {
+ kv{"key": "value"},
+ kv{"name": "Two"},
+ 3,
+ "BenchmarkTwo 2 ns/sec",
+ },
+ },
+ },
+ {
+ "remove existing label",
+ `key: value
+key:
+BenchmarkOne 1 ns/sec
+`,
+ []*Result{
+ {
+ kv{},
+ kv{"name": "One"},
+ 3,
+ "BenchmarkOne 1 ns/sec",
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ r := NewBenchmarkReader(strings.NewReader(test.input))
+ have := readAllResults(t, r)
+ want := test.want
+ diff := ""
+ mismatch := false
+ for i := 0; i < len(have) || i < len(want); i++ {
+ if i < len(have) && i < len(want) && reflect.DeepEqual(have[i], want[i]) {
+ diff += fmt.Sprintf(" %+v\n", have[i])
+ continue
+ }
+ mismatch = true
+ if i < len(have) {
+ diff += fmt.Sprintf("-%+v\n", have[i])
+ }
+ if i < len(want) {
+ diff += fmt.Sprintf("+%+v\n", want[i])
+ }
+ }
+ if mismatch {
+ t.Errorf("wrong results: (- have/+ want)\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestBenchmarkPrinter(t *testing.T) {
+ tests := []struct {
+ name, input, want string
+ }{
+ {
+ "basic",
+ `key: value
+BenchmarkOne 1 ns/sec
+`,
+ `key: value
+BenchmarkOne 1 ns/sec
+`,
+ },
+ {
+ "missing newline",
+ `key: value
+BenchmarkOne 1 ns/sec`,
+ `key: value
+BenchmarkOne 1 ns/sec
+`,
+ },
+ {
+ "duplicate and removed fields",
+ `one: 1
+two: 2
+BenchmarkOne 1 ns/sec
+one: 1
+two: 3
+BenchmarkOne 1 ns/sec
+two:
+BenchmarkOne 1 ns/sec
+`,
+ `one: 1
+two: 2
+BenchmarkOne 1 ns/sec
+two: 3
+BenchmarkOne 1 ns/sec
+two:
+BenchmarkOne 1 ns/sec
+`,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ r := NewBenchmarkReader(strings.NewReader(test.input))
+ results := readAllResults(t, r)
+ var have bytes.Buffer
+ bp := NewBenchmarkPrinter(&have)
+ for _, result := range results {
+ if err := bp.Print(result); err != nil {
+ t.Errorf("Print returned %v", err)
+ }
+ }
+ if diff := diff(have.String(), test.want); diff != "" {
+ t.Errorf("wrong output: (- got/+ want)\n%s", diff)
+ }
+ })
+ }
+}
+
+// diff returns the output of unified diff on s1 and s2. If the result
+// is non-empty, the strings differ or the diff command failed.
+func diff(s1, s2 string) string {
+ f1, err := ioutil.TempFile("", "benchfmt_test")
+ if err != nil {
+ return err.Error()
+ }
+ defer os.Remove(f1.Name())
+ defer f1.Close()
+
+ f2, err := ioutil.TempFile("", "benchfmt_test")
+ if err != nil {
+ return err.Error()
+ }
+ defer os.Remove(f2.Name())
+ defer f2.Close()
+
+ f1.Write([]byte(s1))
+ f2.Write([]byte(s2))
+
+ data, err := exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
+ if len(data) > 0 {
+ // diff exits with a non-zero status when the files don't match.
+ // Ignore that failure as long as we get output.
+ err = nil
+ }
+ if err != nil {
+ data = append(data, []byte(err.Error())...)
+ }
+ return string(data)
+
+}
diff --git a/storage/app/upload.go b/storage/app/upload.go
index 1f968a2..7943f9e 100644
--- a/storage/app/upload.go
+++ b/storage/app/upload.go
@@ -10,6 +10,7 @@
"io"
"mime/multipart"
"net/http"
+ "sort"
"golang.org/x/net/context"
)
@@ -102,7 +103,18 @@
return nil, err
}
- // TODO(quentin): Write metadata at top of file
+ var keys []string
+ for k := range meta {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ if _, err := fmt.Fprintf(fw, "%s: %s\n", k, meta[k]); err != nil {
+ fw.CloseWithError(err)
+ return nil, err
+ }
+ }
+
if _, err := io.Copy(fw, p); err != nil {
fw.CloseWithError(err)
return nil, err