storage: coalesce results with the same labels into one row

Change-Id: Ia9d62651ba23794cd64dfcf2ea2e529914f3b100
Reviewed-on: https://go-review.googlesource.com/35674
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/benchfmt/benchfmt.go b/storage/benchfmt/benchfmt.go
index 37f33b7..e43e48d 100644
--- a/storage/benchfmt/benchfmt.go
+++ b/storage/benchfmt/benchfmt.go
@@ -9,6 +9,7 @@
 
 import (
 	"bufio"
+	"bytes"
 	"fmt"
 	"io"
 	"sort"
@@ -53,7 +54,7 @@
 // AddLabels adds additional labels as if they had been read from the header of a file.
 // It must be called before the first call to r.Next.
 func (r *Reader) AddLabels(labels Labels) {
-	r.permLabels = labels.copy()
+	r.permLabels = labels.Copy()
 	for k, v := range labels {
 		r.labels[k] = v
 	}
@@ -61,6 +62,7 @@
 
 // Result represents a single line from a benchmark file.
 // All information about that line is self-contained in the Result.
+// A Result is immutable once created.
 type Result struct {
 	// Labels is the set of persistent labels that apply to the result.
 	// Labels must not be modified.
@@ -75,10 +77,29 @@
 	Content string
 }
 
+// SameLabels reports whether r and b have the same labels.
+func (r *Result) SameLabels(b *Result) bool {
+	return r.Labels.Equal(b.Labels) && r.NameLabels.Equal(b.NameLabels)
+}
+
 // Labels is a set of key-value strings.
 type Labels map[string]string
 
-// TODO(quentin): Add String and Equal methods to Labels?
+// String returns the labels formatted as a comma-separated
+// list enclosed in braces.
+func (l Labels) String() string {
+	var out bytes.Buffer
+	out.WriteString("{")
+	for k, v := range l {
+		fmt.Fprintf(&out, "%q: %q, ", k, v)
+	}
+	if out.Len() > 1 {
+		// Remove extra ", "
+		out.Truncate(out.Len() - 2)
+	}
+	out.WriteString("}")
+	return out.String()
+}
 
 // Keys returns a sorted list of the keys in l.
 func (l Labels) Keys() []string {
@@ -90,6 +111,19 @@
 	return out
 }
 
+// Equal reports whether l and b have the same keys and values.
+func (l Labels) Equal(b Labels) bool {
+	if len(l) != len(b) {
+		return false
+	}
+	for k := range l {
+		if l[k] != b[k] {
+			return false
+		}
+	}
+	return true
+}
+
 // A Printer prints a sequence of benchmark results.
 type Printer struct {
 	w      io.Writer
@@ -178,9 +212,9 @@
 	return res
 }
 
-// copy returns a new copy of the labels map, to protect against
+// Copy returns a new copy of the labels map, to protect against
 // future modifications to labels.
-func (l Labels) copy() Labels {
+func (l Labels) Copy() Labels {
 	new := make(Labels)
 	for k, v := range l {
 		new[k] = v
@@ -207,7 +241,7 @@
 			}
 			if !copied {
 				copied = true
-				r.labels = r.labels.copy()
+				r.labels = r.labels.Copy()
 			}
 			// TODO(quentin): Spec says empty value is valid, but
 			// we need a way to cancel previous labels, so we'll
@@ -222,7 +256,7 @@
 		// Blank line delimits the header. If we find anything else, the file must not have a header.
 		if !havePerm {
 			if line == "" {
-				r.permLabels = r.labels.copy()
+				r.permLabels = r.labels.Copy()
 			} else {
 				r.permLabels = Labels{}
 			}
diff --git a/storage/db/db.go b/storage/db/db.go
index decb63a..35b696f 100644
--- a/storage/db/db.go
+++ b/storage/db/db.go
@@ -167,6 +167,7 @@
 	// pending arguments for flush
 	insertRecordArgs []interface{}
 	insertLabelArgs  []interface{}
+	lastResult       *benchfmt.Result
 }
 
 // now is a hook for testing
@@ -267,11 +268,19 @@
 // InsertRecord inserts a single record in an existing upload.
 // If InsertRecord returns a non-nil error, the Upload has failed and u.Abort() must be called.
 func (u *Upload) InsertRecord(r *benchfmt.Result) error {
+	if u.lastResult != nil && u.lastResult.SameLabels(r) {
+		data := u.insertRecordArgs[len(u.insertRecordArgs)-1].([]byte)
+		data = append(data, r.Content...)
+		data = append(data, '\n')
+		u.insertRecordArgs[len(u.insertRecordArgs)-1] = data
+		return nil
+	}
 	// TODO(quentin): Support multiple lines (slice of results?)
 	var buf bytes.Buffer
 	if err := benchfmt.NewPrinter(&buf).Print(r); err != nil {
 		return err
 	}
+	u.lastResult = r
 	u.insertRecordArgs = append(u.insertRecordArgs, u.ID, u.recordid, buf.Bytes())
 	for _, k := range r.Labels.Keys() {
 		if err := u.insertLabel(k, r.Labels[k]); err != nil {
@@ -331,6 +340,7 @@
 		}
 		u.insertLabelArgs = nil
 	}
+	u.lastResult = nil
 	return nil
 }
 
diff --git a/storage/db/db_test.go b/storage/db/db_test.go
index 9b108eb..154f4b1 100644
--- a/storage/db/db_test.go
+++ b/storage/db/db_test.go
@@ -114,24 +114,25 @@
 
 	ctx := context.Background()
 
-	r := &benchfmt.Result{
-		benchfmt.Labels{"key": "value"},
-		nil,
-		1,
-		"BenchmarkName 1 ns/op",
-	}
+	labels := benchfmt.Labels{"key": "value"}
+
 	u, err := db.NewUpload(ctx)
 	if err != nil {
 		t.Fatalf("NewUpload: %v", err)
 	}
-	r.Labels["uploadid"] = u.ID
+	labels["uploadid"] = u.ID
 	for _, num := range []string{"1", "2"} {
-		r.Labels["num"] = num
+		labels["num"] = num
 		for _, num2 := range []int{1, 2} {
-			r.Content = fmt.Sprintf("BenchmarkName %d ns/op", num2)
-			if err := u.InsertRecord(r); err != nil {
+			if err := u.InsertRecord(&benchfmt.Result{
+				labels,
+				nil,
+				1,
+				fmt.Sprintf("BenchmarkName %d ns/op", num2),
+			}); err != nil {
 				t.Fatalf("InsertRecord: %v", err)
 			}
+			labels = labels.Copy()
 		}
 	}
 
@@ -150,18 +151,23 @@
 BenchmarkName 2 ns/op
 `)
 
-	r.Labels["num"] = "3"
-	r.Content = "BenchmarkName 3 ns/op"
+	labels["num"] = "3"
 
 	for _, uploadid := range []string{u.ID, "new"} {
 		u, err := db.ReplaceUpload(uploadid)
 		if err != nil {
 			t.Fatalf("ReplaceUpload: %v", err)
 		}
-		r.Labels["uploadid"] = u.ID
-		if err := u.InsertRecord(r); err != nil {
+		labels["uploadid"] = u.ID
+		if err := u.InsertRecord(&benchfmt.Result{
+			labels,
+			nil,
+			1,
+			"BenchmarkName 3 ns/op",
+		}); err != nil {
 			t.Fatalf("InsertRecord: %v", err)
 		}
+		labels = labels.Copy()
 
 		if err := u.Commit(); err != nil {
 			t.Fatalf("Commit: %v", err)
@@ -193,15 +199,16 @@
 	br := benchfmt.NewReader(strings.NewReader(`
 key: value
 BenchmarkName 1 ns/op
+BenchmarkName 2 ns/op
 `))
-	if !br.Next() {
-		t.Fatalf("unable to read test string: %v", br.Err())
+	for br.Next() {
+		if err := u.InsertRecord(br.Result()); err != nil {
+			t.Fatalf("InsertRecord: %v", err)
+		}
 	}
-
-	if err := u.InsertRecord(br.Result()); err != nil {
-		t.Fatalf("InsertRecord: %v", err)
+	if err := br.Err(); err != nil {
+		t.Fatalf("Err: %v", err)
 	}
-
 	if err := u.Commit(); err != nil {
 		t.Fatalf("Commit: %v", err)
 	}