Commit ade13b72 authored by Quentin Smith's avatar Quentin Smith

all: print stats on benchsave

This changes benchsave to behave like benchstat; in addition to
uploading the files and printing a URL, it also prints the text format
of benchstat. This is fetched from the ViewURL provided by the storage
server, so the analysis can be changed/improved without requiring
users to rebuild benchsave.

Change-Id: I28519a5e3cf89962bd952ff26a8a6a717b9ef636
Reviewed-on: https://go-review.googlesource.com/37532Reviewed-by: default avatarRuss Cox <rsc@golang.org>
parent 80185218
...@@ -33,6 +33,12 @@ func (a *App) search(w http.ResponseWriter, r *http.Request) { ...@@ -33,6 +33,12 @@ func (a *App) search(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
if r.Header.Get("Accept") == "text/plain" || r.Header.Get("X-Benchsave") == "1" {
// TODO(quentin): Switch to real Accept negotiation when golang/go#19307 is resolved.
// Benchsave sends both of these headers.
a.textCompare(w, r)
return
}
// TODO(quentin): Intelligently choose an analysis method // TODO(quentin): Intelligently choose an analysis method
// based on the results from the query, once there is more // based on the results from the query, once there is more
// than one analysis method. // than one analysis method.
......
...@@ -6,6 +6,7 @@ package app ...@@ -6,6 +6,7 @@ package app
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
...@@ -221,11 +222,9 @@ func elideKeyValues(content string, keys map[string]bool) string { ...@@ -221,11 +222,9 @@ func elideKeyValues(content string, keys map[string]bool) string {
return strings.Join(parts, "/") + end return strings.Join(parts, "/") + end
} }
func (a *App) compareQuery(q string) *compareData { // fetchCompareResults fetches the matching results for a given query string.
if len(q) == 0 { // The results will be grouped into one or more groups based on either the query string or heuristics.
return &compareData{} func (a *App) fetchCompareResults(q string) ([]*resultGroup, error) {
}
// Parse query // Parse query
prefix, queries := parseQueryString(q) prefix, queries := parseQueryString(q)
...@@ -250,19 +249,13 @@ func (a *App) compareQuery(q string) *compareData { ...@@ -250,19 +249,13 @@ func (a *App) compareQuery(q string) *compareData {
res.Close() res.Close()
if err != nil { if err != nil {
// TODO: If the query is invalid, surface that to the user. // TODO: If the query is invalid, surface that to the user.
return &compareData{ return nil, err
Q: q,
Error: err.Error(),
}
} }
groups = append(groups, group) groups = append(groups, group)
} }
if found == 0 { if found == 0 {
return &compareData{ return nil, errors.New("no results matched the query string")
Q: q,
Error: "No results matched the query string.",
}
} }
// Attempt to automatically split results. // Attempt to automatically split results.
...@@ -274,6 +267,22 @@ func (a *App) compareQuery(q string) *compareData { ...@@ -274,6 +267,22 @@ func (a *App) compareQuery(q string) *compareData {
} }
} }
return groups, nil
}
func (a *App) compareQuery(q string) *compareData {
if len(q) == 0 {
return &compareData{}
}
groups, err := a.fetchCompareResults(q)
if err != nil {
return &compareData{
Q: q,
Error: err.Error(),
}
}
var buf bytes.Buffer var buf bytes.Buffer
// Compute benchstat // Compute benchstat
c := new(benchstat.Collection) c := new(benchstat.Collection)
...@@ -321,3 +330,28 @@ func (a *App) compareQuery(q string) *compareData { ...@@ -321,3 +330,28 @@ func (a *App) compareQuery(q string) *compareData {
} }
return data return data
} }
// textCompare is called if benchsave is requesting a text-only analysis.
func (a *App) textCompare(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
q := r.Form.Get("q")
groups, err := a.fetchCompareResults(q)
if err != nil {
// TODO(quentin): Should we serve this with a 500 or 404? This means the query was invalid or had no results.
fmt.Fprintf(w, "unable to analyze results: %v", err)
}
// Compute benchstat
c := new(benchstat.Collection)
for _, g := range groups {
c.AddResults(g.Q, g.results)
}
benchstat.FormatText(w, c.Tables())
}
...@@ -5,13 +5,13 @@ ...@@ -5,13 +5,13 @@
package benchstat package benchstat
import ( import (
"bytes"
"fmt" "fmt"
"io"
"unicode/utf8" "unicode/utf8"
) )
// FormatText appends a fixed-width text formatting of the tables to buf. // FormatText appends a fixed-width text formatting of the tables to w.
func FormatText(buf *bytes.Buffer, tables []*Table) { func FormatText(w io.Writer, tables []*Table) {
var textTables [][]*textRow var textTables [][]*textRow
for _, t := range tables { for _, t := range tables {
textTables = append(textTables, toText(t)) textTables = append(textTables, toText(t))
...@@ -34,7 +34,7 @@ func FormatText(buf *bytes.Buffer, tables []*Table) { ...@@ -34,7 +34,7 @@ func FormatText(buf *bytes.Buffer, tables []*Table) {
for i, table := range textTables { for i, table := range textTables {
if i > 0 { if i > 0 {
fmt.Fprintf(buf, "\n") fmt.Fprintf(w, "\n")
} }
// headings // headings
...@@ -42,11 +42,11 @@ func FormatText(buf *bytes.Buffer, tables []*Table) { ...@@ -42,11 +42,11 @@ func FormatText(buf *bytes.Buffer, tables []*Table) {
for i, s := range row.cols { for i, s := range row.cols {
switch i { switch i {
case 0: case 0:
fmt.Fprintf(buf, "%-*s", max[i], s) fmt.Fprintf(w, "%-*s", max[i], s)
default: default:
fmt.Fprintf(buf, " %-*s", max[i], s) fmt.Fprintf(w, " %-*s", max[i], s)
case len(row.cols) - 1: case len(row.cols) - 1:
fmt.Fprintf(buf, " %s\n", s) fmt.Fprintf(w, " %s\n", s)
} }
} }
...@@ -55,17 +55,17 @@ func FormatText(buf *bytes.Buffer, tables []*Table) { ...@@ -55,17 +55,17 @@ func FormatText(buf *bytes.Buffer, tables []*Table) {
for i, s := range row.cols { for i, s := range row.cols {
switch i { switch i {
case 0: case 0:
fmt.Fprintf(buf, "%-*s", max[i], s) fmt.Fprintf(w, "%-*s", max[i], s)
default: default:
if i == len(row.cols)-1 && len(s) > 0 && s[0] == '(' { if i == len(row.cols)-1 && len(s) > 0 && s[0] == '(' {
// Left-align p value. // Left-align p value.
fmt.Fprintf(buf, " %s", s) fmt.Fprintf(w, " %s", s)
break break
} }
fmt.Fprintf(buf, " %*s", max[i], s) fmt.Fprintf(w, " %*s", max[i], s)
} }
} }
fmt.Fprintf(buf, "\n") fmt.Fprintf(w, "\n")
} }
} }
} }
......
...@@ -24,7 +24,9 @@ import ( ...@@ -24,7 +24,9 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"mime"
"mime/multipart" "mime/multipart"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
...@@ -38,6 +40,8 @@ var ( ...@@ -38,6 +40,8 @@ var (
header = flag.String("header", "", "insert `file` at the beginning of each uploaded file") header = flag.String("header", "", "insert `file` at the beginning of each uploaded file")
) )
const userAgent = "Benchsave/1.0"
type uploadStatus struct { type uploadStatus struct {
// UploadID is the upload ID assigned to the upload. // UploadID is the upload ID assigned to the upload.
UploadID string `json:"uploadid"` UploadID string `json:"uploadid"`
...@@ -126,7 +130,13 @@ func main() { ...@@ -126,7 +130,13 @@ func main() {
start := time.Now() start := time.Now()
resp, err := hc.Post(*server+"/upload", mpw.FormDataContentType(), pr) req, err := http.NewRequest("POST", *server+"/upload", pr)
if err != nil {
log.Fatalf("NewRequest failed: %v\n", err)
}
req.Header.Set("Content-Type", mpw.FormDataContentType())
req.Header.Set("User-Agent", userAgent)
resp, err := hc.Do(req)
if err != nil { if err != nil {
log.Fatalf("upload failed: %v\n", err) log.Fatalf("upload failed: %v\n", err)
} }
...@@ -151,7 +161,23 @@ func main() { ...@@ -151,7 +161,23 @@ func main() {
log.Printf("%d file%s uploaded in %.2f seconds.\n", len(files), s, time.Since(start).Seconds()) log.Printf("%d file%s uploaded in %.2f seconds.\n", len(files), s, time.Since(start).Seconds())
} }
if status.ViewURL != "" { if status.ViewURL != "" {
// New servers will serve a text/plain response to the view URL when given these headers.
// Old servers will not, so only show the response if it is a 200 and text/plain.
req, err := http.NewRequest("GET", status.ViewURL, nil)
if err == nil {
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "text/plain")
req.Header.Set("X-Benchsave", "1")
resp, err := hc.Do(req)
if err == nil {
defer resp.Body.Close()
mt, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if resp.StatusCode == http.StatusOK && err == nil && mt == "text/plain" {
io.Copy(os.Stdout, resp.Body)
fmt.Println()
}
}
}
fmt.Printf("%s\n", status.ViewURL) fmt.Printf("%s\n", status.ViewURL)
} }
// TODO(quentin): Print benchstat-style output, either computed client-side or fetched from a server.
} }
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