Commit 1449119c authored by Quentin Smith's avatar Quentin Smith

storage: coalesce results with the same labels into one row

Change-Id: Ia9d62651ba23794cd64dfcf2ea2e529914f3b100
Reviewed-on: https://go-review.googlesource.com/35674Reviewed-by: default avatarRuss Cox <rsc@golang.org>
parent 680f451e
...@@ -9,6 +9,7 @@ package benchfmt ...@@ -9,6 +9,7 @@ package benchfmt
import ( import (
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"io" "io"
"sort" "sort"
...@@ -53,7 +54,7 @@ func NewReader(r io.Reader) *Reader { ...@@ -53,7 +54,7 @@ func NewReader(r io.Reader) *Reader {
// AddLabels adds additional labels as if they had been read from the header of a file. // 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. // It must be called before the first call to r.Next.
func (r *Reader) AddLabels(labels Labels) { func (r *Reader) AddLabels(labels Labels) {
r.permLabels = labels.copy() r.permLabels = labels.Copy()
for k, v := range labels { for k, v := range labels {
r.labels[k] = v r.labels[k] = v
} }
...@@ -61,6 +62,7 @@ func (r *Reader) AddLabels(labels Labels) { ...@@ -61,6 +62,7 @@ func (r *Reader) AddLabels(labels Labels) {
// Result represents a single line from a benchmark file. // Result represents a single line from a benchmark file.
// All information about that line is self-contained in the Result. // All information about that line is self-contained in the Result.
// A Result is immutable once created.
type Result struct { type Result struct {
// Labels is the set of persistent labels that apply to the result. // Labels is the set of persistent labels that apply to the result.
// Labels must not be modified. // Labels must not be modified.
...@@ -75,10 +77,29 @@ type Result struct { ...@@ -75,10 +77,29 @@ type Result struct {
Content string 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. // Labels is a set of key-value strings.
type Labels map[string]string 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. // Keys returns a sorted list of the keys in l.
func (l Labels) Keys() []string { func (l Labels) Keys() []string {
...@@ -90,6 +111,19 @@ func (l Labels) Keys() []string { ...@@ -90,6 +111,19 @@ func (l Labels) Keys() []string {
return out 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. // A Printer prints a sequence of benchmark results.
type Printer struct { type Printer struct {
w io.Writer w io.Writer
...@@ -178,9 +212,9 @@ func (r *Reader) newResult(labels Labels, lineNum int, name, content string) *Re ...@@ -178,9 +212,9 @@ func (r *Reader) newResult(labels Labels, lineNum int, name, content string) *Re
return res 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. // future modifications to labels.
func (l Labels) copy() Labels { func (l Labels) Copy() Labels {
new := make(Labels) new := make(Labels)
for k, v := range l { for k, v := range l {
new[k] = v new[k] = v
...@@ -207,7 +241,7 @@ func (r *Reader) Next() bool { ...@@ -207,7 +241,7 @@ func (r *Reader) Next() bool {
} }
if !copied { if !copied {
copied = true copied = true
r.labels = r.labels.copy() r.labels = r.labels.Copy()
} }
// TODO(quentin): Spec says empty value is valid, but // TODO(quentin): Spec says empty value is valid, but
// we need a way to cancel previous labels, so we'll // we need a way to cancel previous labels, so we'll
...@@ -222,7 +256,7 @@ func (r *Reader) Next() bool { ...@@ -222,7 +256,7 @@ func (r *Reader) Next() bool {
// Blank line delimits the header. If we find anything else, the file must not have a header. // Blank line delimits the header. If we find anything else, the file must not have a header.
if !havePerm { if !havePerm {
if line == "" { if line == "" {
r.permLabels = r.labels.copy() r.permLabels = r.labels.Copy()
} else { } else {
r.permLabels = Labels{} r.permLabels = Labels{}
} }
......
...@@ -167,6 +167,7 @@ type Upload struct { ...@@ -167,6 +167,7 @@ type Upload struct {
// pending arguments for flush // pending arguments for flush
insertRecordArgs []interface{} insertRecordArgs []interface{}
insertLabelArgs []interface{} insertLabelArgs []interface{}
lastResult *benchfmt.Result
} }
// now is a hook for testing // now is a hook for testing
...@@ -267,11 +268,19 @@ func (db *DB) NewUpload(ctx context.Context) (*Upload, error) { ...@@ -267,11 +268,19 @@ func (db *DB) NewUpload(ctx context.Context) (*Upload, error) {
// InsertRecord inserts a single record in an existing upload. // 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. // 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 { 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?) // TODO(quentin): Support multiple lines (slice of results?)
var buf bytes.Buffer var buf bytes.Buffer
if err := benchfmt.NewPrinter(&buf).Print(r); err != nil { if err := benchfmt.NewPrinter(&buf).Print(r); err != nil {
return err return err
} }
u.lastResult = r
u.insertRecordArgs = append(u.insertRecordArgs, u.ID, u.recordid, buf.Bytes()) u.insertRecordArgs = append(u.insertRecordArgs, u.ID, u.recordid, buf.Bytes())
for _, k := range r.Labels.Keys() { for _, k := range r.Labels.Keys() {
if err := u.insertLabel(k, r.Labels[k]); err != nil { if err := u.insertLabel(k, r.Labels[k]); err != nil {
...@@ -331,6 +340,7 @@ func (u *Upload) flush() error { ...@@ -331,6 +340,7 @@ func (u *Upload) flush() error {
} }
u.insertLabelArgs = nil u.insertLabelArgs = nil
} }
u.lastResult = nil
return nil return nil
} }
......
...@@ -114,24 +114,25 @@ func TestReplaceUpload(t *testing.T) { ...@@ -114,24 +114,25 @@ func TestReplaceUpload(t *testing.T) {
ctx := context.Background() ctx := context.Background()
r := &benchfmt.Result{ labels := benchfmt.Labels{"key": "value"}
benchfmt.Labels{"key": "value"},
nil,
1,
"BenchmarkName 1 ns/op",
}
u, err := db.NewUpload(ctx) u, err := db.NewUpload(ctx)
if err != nil { if err != nil {
t.Fatalf("NewUpload: %v", err) t.Fatalf("NewUpload: %v", err)
} }
r.Labels["uploadid"] = u.ID labels["uploadid"] = u.ID
for _, num := range []string{"1", "2"} { for _, num := range []string{"1", "2"} {
r.Labels["num"] = num labels["num"] = num
for _, num2 := range []int{1, 2} { for _, num2 := range []int{1, 2} {
r.Content = fmt.Sprintf("BenchmarkName %d ns/op", num2) if err := u.InsertRecord(&benchfmt.Result{
if err := u.InsertRecord(r); err != nil { labels,
nil,
1,
fmt.Sprintf("BenchmarkName %d ns/op", num2),
}); err != nil {
t.Fatalf("InsertRecord: %v", err) t.Fatalf("InsertRecord: %v", err)
} }
labels = labels.Copy()
} }
} }
...@@ -150,18 +151,23 @@ BenchmarkName 1 ns/op ...@@ -150,18 +151,23 @@ BenchmarkName 1 ns/op
BenchmarkName 2 ns/op BenchmarkName 2 ns/op
`) `)
r.Labels["num"] = "3" labels["num"] = "3"
r.Content = "BenchmarkName 3 ns/op"
for _, uploadid := range []string{u.ID, "new"} { for _, uploadid := range []string{u.ID, "new"} {
u, err := db.ReplaceUpload(uploadid) u, err := db.ReplaceUpload(uploadid)
if err != nil { if err != nil {
t.Fatalf("ReplaceUpload: %v", err) t.Fatalf("ReplaceUpload: %v", err)
} }
r.Labels["uploadid"] = u.ID labels["uploadid"] = u.ID
if err := u.InsertRecord(r); err != nil { if err := u.InsertRecord(&benchfmt.Result{
labels,
nil,
1,
"BenchmarkName 3 ns/op",
}); err != nil {
t.Fatalf("InsertRecord: %v", err) t.Fatalf("InsertRecord: %v", err)
} }
labels = labels.Copy()
if err := u.Commit(); err != nil { if err := u.Commit(); err != nil {
t.Fatalf("Commit: %v", err) t.Fatalf("Commit: %v", err)
...@@ -193,15 +199,16 @@ func TestNewUpload(t *testing.T) { ...@@ -193,15 +199,16 @@ func TestNewUpload(t *testing.T) {
br := benchfmt.NewReader(strings.NewReader(` br := benchfmt.NewReader(strings.NewReader(`
key: value key: value
BenchmarkName 1 ns/op BenchmarkName 1 ns/op
BenchmarkName 2 ns/op
`)) `))
if !br.Next() { for br.Next() {
t.Fatalf("unable to read test string: %v", br.Err()) if err := u.InsertRecord(br.Result()); err != nil {
t.Fatalf("InsertRecord: %v", err)
}
} }
if err := br.Err(); err != nil {
if err := u.InsertRecord(br.Result()); err != nil { t.Fatalf("Err: %v", err)
t.Fatalf("InsertRecord: %v", err)
} }
if err := u.Commit(); err != nil { if err := u.Commit(); err != nil {
t.Fatalf("Commit: %v", err) t.Fatalf("Commit: %v", err)
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment