cmd/benchsave: command-line tool for uploading benchmark results
Change-Id: I060d45ac3b487900ab8315978fe89ab576c571b7
Reviewed-on: https://go-review.googlesource.com/35053
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/cmd/benchsave/benchsave.go b/cmd/benchsave/benchsave.go
new file mode 100644
index 0000000..8247189
--- /dev/null
+++ b/cmd/benchsave/benchsave.go
@@ -0,0 +1,130 @@
+// Copyright 2017 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.
+
+// Benchsave uploads benchmark results to a storage server.
+//
+// Usage:
+//
+// benchsave [-server https://server.org] a.txt [b.txt ...]
+//
+// Each input file should contain the output from one or more runs of
+// ``go test -bench'', or another tool which uses the same format.
+//
+// benchsave will upload the input files to the specified server and
+// print a URL where they can be viewed.
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "mime/multipart"
+ "net/http"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+var (
+ server = flag.String("server", "https://perfdata.golang.org", "perfdata server to upload benchmarks to")
+ verbose = flag.Bool("v", false, "verbose")
+)
+
+type uploadStatus struct {
+ // UploadID is the upload ID assigned to the upload.
+ UploadID string `json:"uploadid"`
+ // FileIDs is the list of file IDs assigned to the files in the upload.
+ FileIDs []string `json:"fileids"`
+ // ViewURL is a server-supplied URL to view the results.
+ ViewURL string `json:"viewurl"`
+}
+
+// writeOneFile reads name and writes it to mpw.
+func writeOneFile(mpw *multipart.Writer, name string) {
+ w, err := mpw.CreateFormFile("file", filepath.Base(name))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Writing upload failed: %v\n", err)
+ os.Exit(1)
+ }
+ f, err := os.Open(name)
+ if err != nil {
+ fmt.Fprint(os.Stderr, err)
+ mpw.WriteField("abort", "1")
+ // TODO(quentin): Wait until the abort field is written before exiting.
+ os.Exit(1)
+ }
+ defer f.Close()
+
+ if _, err := io.Copy(w, f); err != nil {
+ fmt.Fprintf(os.Stderr, "Writing upload failed: %v", err)
+ os.Exit(1)
+ }
+}
+
+func usage() {
+ fmt.Fprintf(os.Stderr, `Usage of %s:
+%s [flags] file...
+`, os.Args[0], os.Args[0])
+ flag.PrintDefaults()
+ os.Exit(2)
+}
+
+func main() {
+ log.SetPrefix("benchsave: ")
+ log.SetFlags(0)
+ flag.Usage = usage
+ flag.Parse()
+
+ // TODO(quentin): Authentication
+
+ files := flag.Args()
+ if len(files) == 0 {
+ log.Fatal("no files to upload")
+ }
+
+ pr, pw := io.Pipe()
+ mpw := multipart.NewWriter(pw)
+
+ go func() {
+ defer pw.Close()
+ defer mpw.Close()
+
+ for _, name := range files {
+ writeOneFile(mpw, name)
+ }
+ }()
+
+ start := time.Now()
+
+ resp, err := http.Post(*server+"/upload", mpw.FormDataContentType(), pr)
+ if err != nil {
+ log.Fatalf("upload failed: %v\n", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ log.Printf("upload failed: %v\n", resp.Status)
+ io.Copy(os.Stderr, resp.Body)
+ os.Exit(1)
+ }
+
+ status := &uploadStatus{}
+ if err := json.NewDecoder(resp.Body).Decode(status); err != nil {
+ log.Fatalf("cannot parse upload response: %v\n", err)
+ }
+
+ if *verbose {
+ s := ""
+ if len(files) != 1 {
+ s = "s"
+ }
+ log.Printf("%d file%s uploaded in %.2f seconds.\n", len(files), s, time.Since(start).Seconds())
+ }
+ if status.ViewURL != "" {
+ fmt.Printf("%s\n", status.ViewURL)
+ }
+ // TODO(quentin): Print benchstat-style output, either computed client-side or fetched from a server.
+}