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/37532 Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/analysis/app/app.go b/analysis/app/app.go index c4d3029..d4bd819 100644 --- a/analysis/app/app.go +++ b/analysis/app/app.go
@@ -33,6 +33,12 @@ http.Error(w, err.Error(), 500) 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 // based on the results from the query, once there is more // than one analysis method.
diff --git a/analysis/app/compare.go b/analysis/app/compare.go index 93b8721..f154b1e 100644 --- a/analysis/app/compare.go +++ b/analysis/app/compare.go
@@ -6,6 +6,7 @@ import ( "bytes" + "errors" "fmt" "html/template" "io/ioutil" @@ -221,11 +222,9 @@ return strings.Join(parts, "/") + end } -func (a *App) compareQuery(q string) *compareData { - if len(q) == 0 { - return &compareData{} - } - +// fetchCompareResults fetches the matching results for a given query string. +// The results will be grouped into one or more groups based on either the query string or heuristics. +func (a *App) fetchCompareResults(q string) ([]*resultGroup, error) { // Parse query prefix, queries := parseQueryString(q) @@ -250,19 +249,13 @@ res.Close() if err != nil { // TODO: If the query is invalid, surface that to the user. - return &compareData{ - Q: q, - Error: err.Error(), - } + return nil, err } groups = append(groups, group) } if found == 0 { - return &compareData{ - Q: q, - Error: "No results matched the query string.", - } + return nil, errors.New("no results matched the query string") } // Attempt to automatically split results. @@ -274,6 +267,22 @@ } } + 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 // Compute benchstat c := new(benchstat.Collection) @@ -321,3 +330,28 @@ } 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()) +}
diff --git a/benchstat/text.go b/benchstat/text.go index 36d2d50..b8189f8 100644 --- a/benchstat/text.go +++ b/benchstat/text.go
@@ -5,13 +5,13 @@ package benchstat import ( - "bytes" "fmt" + "io" "unicode/utf8" ) -// FormatText appends a fixed-width text formatting of the tables to buf. -func FormatText(buf *bytes.Buffer, tables []*Table) { +// FormatText appends a fixed-width text formatting of the tables to w. +func FormatText(w io.Writer, tables []*Table) { var textTables [][]*textRow for _, t := range tables { textTables = append(textTables, toText(t)) @@ -34,7 +34,7 @@ for i, table := range textTables { if i > 0 { - fmt.Fprintf(buf, "\n") + fmt.Fprintf(w, "\n") } // headings @@ -42,11 +42,11 @@ for i, s := range row.cols { switch i { case 0: - fmt.Fprintf(buf, "%-*s", max[i], s) + fmt.Fprintf(w, "%-*s", max[i], s) default: - fmt.Fprintf(buf, " %-*s", max[i], s) + fmt.Fprintf(w, " %-*s", max[i], s) case len(row.cols) - 1: - fmt.Fprintf(buf, " %s\n", s) + fmt.Fprintf(w, " %s\n", s) } } @@ -55,17 +55,17 @@ for i, s := range row.cols { switch i { case 0: - fmt.Fprintf(buf, "%-*s", max[i], s) + fmt.Fprintf(w, "%-*s", max[i], s) default: if i == len(row.cols)-1 && len(s) > 0 && s[0] == '(' { // Left-align p value. - fmt.Fprintf(buf, " %s", s) + fmt.Fprintf(w, " %s", s) break } - fmt.Fprintf(buf, " %*s", max[i], s) + fmt.Fprintf(w, " %*s", max[i], s) } } - fmt.Fprintf(buf, "\n") + fmt.Fprintf(w, "\n") } } }
diff --git a/cmd/benchsave/benchsave.go b/cmd/benchsave/benchsave.go index 5913e41..50c70f0 100644 --- a/cmd/benchsave/benchsave.go +++ b/cmd/benchsave/benchsave.go
@@ -24,7 +24,9 @@ "io" "io/ioutil" "log" + "mime" "mime/multipart" + "net/http" "os" "path/filepath" "time" @@ -38,6 +40,8 @@ header = flag.String("header", "", "insert `file` at the beginning of each uploaded file") ) +const userAgent = "Benchsave/1.0" + type uploadStatus struct { // UploadID is the upload ID assigned to the upload. UploadID string `json:"uploadid"` @@ -126,7 +130,13 @@ 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 { log.Fatalf("upload failed: %v\n", err) } @@ -151,7 +161,23 @@ log.Printf("%d file%s uploaded in %.2f seconds.\n", len(files), s, time.Since(start).Seconds()) } 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) } - // TODO(quentin): Print benchstat-style output, either computed client-side or fetched from a server. }