blob: 3cc49b82f744eb07ad106bf11ba5e1232034650f [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go . -help
package main
import (
"bufio"
"encoding/json"
"fmt"
"golang.org/x/tools/benchmark/parse"
"io"
"os"
"runtime"
"strconv"
"strings"
"time"
"v.io/v23/context"
"v.io/x/lib/cmdline"
"v.io/x/ref/lib/v23cmd"
_ "v.io/x/ref/runtime/factories/roaming"
"v.io/x/ref/services/ben"
"v.io/x/ref/services/ben/archive"
)
var (
flagLabel string
flagDryRun bool
flagArchiver string
cmdRoot = &cmdline.Command{
Name: "benup",
Runner: v23cmd.RunnerFunc(runUpload),
Short: "Upload microbenchmark results to an archival service",
ArgsName: "[<input file>]",
ArgsLong: `
[input file] Is the name of the file from which to extract benchmark results
(or empty for standard input).`,
Long: `
Command benup extracts microbenchmark results from the provided input (standard
input for file) and uploads them to an archival service.
For example:
go test -run X -bench crypto/sha256 | benup
will upload the benchmark results, along with a description of the scenario
(operating system, CPU, compiler etc.) under which the benchmarks were run
to the archival service.
To run tests on an Android device, use bendroid (https://godoc.org/v.io/x/devtools/bendroid):
bendroid -bench crypto/sha256 | benup
In the fullness of time, benup may support benchmark formats other than those
from "go test" (such as Java benchmarks written using Caliper).
CAVEAT: At this time, benup assumes the benchmarks were either run on the same
machine (operating system, CPU etc.) as the one where benup is being run, OR
run by bendroid and the state of the source code is the same as that on which
benup is being run.
`,
}
)
func runUpload(ctx *context.T, env *cmdline.Env, args []string) error {
code, err := detectSourceCode(env)
if err != nil {
return err
}
// Set input
var input io.Reader
switch nargs := len(args); nargs {
case 0:
if flagDryRun {
input = env.Stdin
break
}
// Make the command "transparent"
// e.g., "go test | benup" will dump test results to standard
// output just like "go test" would.
input = io.TeeReader(env.Stdin, env.Stdout)
case 1:
f, err := os.Open(args[0])
if err != nil {
return err
}
defer f.Close()
input = f
default:
return env.UsageErrorf("expected 0 or 1 arguments, got %d", nargs)
}
// Goroutines:
// (1) Parse input to extract ben.Scenario and ben.Run objects
// (2) Send ben.Run objects to destination
// (3) (This one) Wait for both to finish
var (
runs = make(chan ben.Run)
errs = make(chan error)
scn = make(chan ben.Scenario)
)
go parseRuns(scn, runs, input, errs)
if flagDryRun {
go printRuns(env, scn, code, runs, errs)
} else {
go sendRuns(env, ctx, scn, code, runs, errs)
}
// Wait for both to terminate (two sends on errch)
for i := 0; i < 2; i++ {
if err := <-errs; err != nil {
return err
}
}
return nil
}
func parseRuns(scn chan<- ben.Scenario, out chan<- ben.Run, in io.Reader, errch chan<- error) {
// Parse the input file. The ben.Scenario under which the benchmarks
// were run should be specified in the input file (bendroid takes care
// of that). If none is found, then assume that the benchmarks were run
// on the same machine as this function.
var (
scanner = bufio.NewScanner(in)
pkgRuns []ben.Run // Runs for all benchmarks in a particular package
write = func(pkg string) {
// Prepend pkg to the name of all benchmarks in pkgRuns,
// send them on put and clear the slice.
if len(pkgRuns) == 0 {
return
}
for _, r := range pkgRuns {
r.Name = pkg + "." + r.Name
out <- r
}
pkgRuns = pkgRuns[:0]
}
)
defer func() {
if err := scanner.Err(); err != nil {
errch <- fmt.Errorf("error parsing input: %v", err)
} else {
errch <- nil
}
close(out)
close(scn)
}()
// Fill in a ben.Scenario: It will be in the input before any benchmark
// runs.
var scan bool
for scan = scanner.Scan(); scan; scan = scanner.Scan() {
line := scanner.Text()
if _, err := parse.ParseLine(line); err == nil {
scn <- detectScenario()
break
}
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "BENDROID") {
continue
}
var s ben.Scenario
s.Os.Name = "android"
s.Label = flagLabel
bendroid := true
for bendroid {
// https://github.com/vanadium/go.devtools/blob/4fd6edf469d84a5be8d00a11283fe7febeafe30f/bendroid/templates.go#L149
if ok, v := trimPrefix(line, "BENDROIDOS_VERSION="); ok {
s.Os.Version = v
} else if ok, v := trimPrefix(line, "BENDROIDCPU_ARCHITECTURE="); ok {
s.Cpu.Architecture = v
} else if ok, v := trimPrefix(line, "BENDROIDCPU_DESCRIPTION="); ok {
s.Cpu.Description = v
}
if bendroid = scanner.Scan(); bendroid {
line = scanner.Text()
_, err := parse.ParseLine(line)
line = strings.TrimSpace(line)
bendroid = (err != nil) && strings.HasPrefix(line, "BENDROID")
}
}
scn <- s
break
}
for ; scan; scan = scanner.Scan() {
line := scanner.Text()
if b, err := parse.ParseLine(line); err == nil {
// Translate b into a ben.Run
parallelism := 0
name := b.Name
if idx := strings.LastIndex(name, "-"); idx > 0 {
if p, err := strconv.Atoi(name[idx+1:]); err == nil {
parallelism = p
name = name[0:idx]
}
}
pkgRuns = append(pkgRuns, ben.Run{
Name: name,
Iterations: uint64(b.N),
NanoSecsPerOp: b.NsPerOp,
AllocsPerOp: b.AllocsPerOp,
AllocedBytesPerOp: b.AllocedBytesPerOp,
MegaBytesPerSec: b.MBPerS,
Parallelism: uint32(parallelism),
})
continue
}
if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "FAIL") {
// Lines which tell us the package name, like:
// ok v.io/v23/security 24.588s
// OR
// FAIL v.io/v23/security 0.046s
if fields := strings.Fields(line); len(fields) == 3 {
write(fields[1])
}
}
}
}
func parseBendroidLine(output *string, prefix, line string) bool {
if strings.HasPrefix(line, prefix) {
*output = strings.TrimPrefix(line, prefix)
return true
}
return false
}
func printRuns(env *cmdline.Env, scn <-chan ben.Scenario, code ben.SourceCode, in <-chan ben.Run, errch chan<- error) {
fmt.Fprintln(env.Stdout, "SOURCE CODE:")
fmt.Fprintln(env.Stdout, code)
fmt.Fprintf(env.Stdout, "SCENARIO:")
prettyPrint(env, <-scn)
fmt.Fprintln(env.Stdout)
fmt.Fprintln(env.Stdout, "RUNS:")
n := 0
for r := range in {
n++
fmt.Fprintf(env.Stdout, "%d: %+v\n", n, r)
}
errch <- nil
}
func sendRuns(env *cmdline.Env, ctx *context.T, scn <-chan ben.Scenario, code ben.SourceCode, in <-chan ben.Run, errch chan<- error) {
var (
scenario = <-scn
runs []ben.Run
uploaded int
urls []string
timer = time.After(time.Minute)
archive = func() error {
if len(runs) == 0 {
return nil
}
url, err := archive.BenchmarkArchiverClient(flagArchiver).Archive(ctx, scenario, code, runs)
if err != nil {
errch <- err
return err
}
urls = append(urls, url)
uploaded += len(runs)
runs = runs[:0]
return nil
}
)
// Collect runs, every minute flush results out to the archiver.
done := false
for !done {
select {
case r, more := <-in:
if !more {
done = true
break
}
runs = append(runs, r)
case <-timer:
if archive() != nil {
return
}
timer = time.After(time.Minute)
}
}
if archive() != nil {
return
}
if uploaded == 0 {
fmt.Fprintln(env.Stdout, "No benchmarks found to upload")
errch <- nil
return
}
fmt.Fprintf(env.Stdout, "Uploaded %d benchmark(s) to %v\n", uploaded, flagArchiver)
for _, url := range urls {
fmt.Fprintln(env.Stdout, "Results at:", url)
}
errch <- nil
}
func detectScenario() ben.Scenario {
return ben.Scenario{
Cpu: ben.Cpu{
Architecture: runtime.GOARCH,
Description: detectCPUDescription(),
ClockSpeedMhz: detectCPUClockSpeedMHz(),
},
Os: ben.Os{
Name: runtime.GOOS,
Version: detectOSVersion(),
},
Label: flagLabel,
}
}
func trimPrefix(s, prefix string) (bool, string) {
return strings.HasPrefix(s, prefix), strings.TrimPrefix(s, prefix)
}
func prettyPrint(env *cmdline.Env, v interface{}) error {
// Use JSON only because json.MarshalIndent pretty-prints in multiple
// lines.
out, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
fmt.Fprintf(env.Stdout, "%s\n", out)
return nil
}
func main() {
cmdline.HideGlobalFlagsExcept()
cmdRoot.Flags.StringVar(&flagLabel, "label", "", "label to assign the uploaded benchmark results")
cmdRoot.Flags.BoolVar(&flagDryRun, "n", false, "If true, results will not be uploaded but the results that would have been uploaded will be dumped out")
cmdRoot.Flags.StringVar(&flagArchiver, "archiver", "/TODO(ashankar):Fill_this_in_when_something_is_setup", "Vanadium object name of the BenchmarkArchiver service")
cmdline.Main(cmdRoot)
}