| // 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 [-v] [-header file] [-server url] file... | |
| // | |
| // 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 ( | |
| "bytes" | |
| "context" | |
| "flag" | |
| "fmt" | |
| "io" | |
| "log" | |
| "mime" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "time" | |
| "golang.org/x/oauth2" | |
| "golang.org/x/perf/storage" | |
| ) | |
| var ( | |
| server = flag.String("server", "https://perfdata.golang.org", "upload benchmarks to server at `url`") | |
| verbose = flag.Bool("v", false, "print verbose log messages") | |
| header = flag.String("header", "", "insert `file` at the beginning of each uploaded file") | |
| ) | |
| const userAgent = "Benchsave/1.0" | |
| // writeOneFile reads name and writes it to u. | |
| func writeOneFile(u *storage.Upload, name string, header []byte) error { | |
| w, err := u.CreateFile(filepath.Base(name)) | |
| if err != nil { | |
| return err | |
| } | |
| if len(header) > 0 { | |
| if _, err := w.Write(header); err != nil { | |
| return err | |
| } | |
| } | |
| f, err := os.Open(name) | |
| if err != nil { | |
| return err | |
| } | |
| defer f.Close() | |
| if _, err := io.Copy(w, f); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| func usage() { | |
| fmt.Fprintf(os.Stderr, `Usage of benchsave: | |
| benchsave [flags] file... | |
| `) | |
| flag.PrintDefaults() | |
| os.Exit(2) | |
| } | |
| func main() { | |
| log.SetPrefix("benchsave: ") | |
| log.SetFlags(0) | |
| flag.Usage = usage | |
| flag.Parse() | |
| files := flag.Args() | |
| if len(files) == 0 { | |
| log.Fatal("no files to upload") | |
| } | |
| var headerData []byte | |
| if *header != "" { | |
| var err error | |
| headerData, err = os.ReadFile(*header) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| headerData = append(bytes.TrimRight(headerData, "\n"), '\n', '\n') | |
| } | |
| // TODO(quentin): Some servers might not need authentication. | |
| // We should somehow detect this and not force the user to get a token. | |
| // Or they might need non-Google authentication. | |
| hc := oauth2.NewClient(context.Background(), newTokenSource()) | |
| client := &storage.Client{BaseURL: *server, HTTPClient: hc} | |
| start := time.Now() | |
| u := client.NewUpload(context.Background()) | |
| for _, name := range files { | |
| if err := writeOneFile(u, name, headerData); err != nil { | |
| log.Print(err) | |
| u.Abort() | |
| return | |
| } | |
| } | |
| status, err := u.Commit() | |
| if err != nil { | |
| log.Fatalf("upload failed: %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 != "" { | |
| // 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) | |
| } | |
| } |