blob: 145575f1b5b935cef53aa8660d8e0845521eed56 [file] [log] [blame]
package cli
import (
"context"
"fmt"
"os"
"strings"
"time"
"go.chromium.org/luci/common/api/swarming/swarming/v1"
"go.skia.org/infra/cabe/go/analyzer"
"go.skia.org/infra/cabe/go/backends"
"go.skia.org/infra/cabe/go/perfresults"
"go.skia.org/infra/cabe/go/replaybackends"
"go.skia.org/infra/go/sklog"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v2"
)
// analyzeCmd holds the flag values and any internal state necessary for
// executing the `analyze` subcommand.
type analyzeCmd struct {
commonCmd
}
// AnalyzeCommand returns a [*cli.Command] for running the analyzer process locally.
func AnalyzeCommand() *cli.Command {
cmd := &analyzeCmd{}
return &cli.Command{
Name: "analyze",
Description: "analyze runs the analyzer process locally.",
Usage: "cabe analyze -- --pinpoint-job <pinpoint-job>",
Flags: cmd.flags(),
Action: cmd.action,
}
}
// action runs the analyzer process locally.
func (cmd *analyzeCmd) action(cliCtx *cli.Context) error {
ctx := cliCtx.Context
rbeClients, err := backends.DialRBECAS(ctx)
if err != nil {
sklog.Fatalf("dialing RBE-CAS backends: %v", err)
return err
}
swarmingClient, err := backends.DialSwarming(ctx)
if err != nil {
sklog.Fatalf("dialing swarming: %v", err)
return err
}
var casResultReader = func(c context.Context, casInstance, digest string) (map[string]perfresults.PerfResults, error) {
rbeClient := rbeClients[casInstance]
return backends.FetchBenchmarkJSON(ctx, rbeClient, digest)
}
var swarmingTaskReader = func(ctx context.Context, pinpointJobID string) ([]*swarming.SwarmingRpcsTaskRequestMetadata, error) {
tasksResp, err := swarmingClient.ListTasks(ctx, time.Now().AddDate(0, 0, -56), time.Now(), []string{pinpointSwarmingTagName + ":" + pinpointJobID}, "")
if err != nil {
sklog.Fatalf("list task results: %v", err)
return nil, err
}
return tasksResp, nil
}
if cmd.replayFromZip != "" {
replayBackends := replaybackends.FromZipFile(cmd.replayFromZip, "blank")
casResultReader = replayBackends.CASResultReader
swarmingTaskReader = replayBackends.SwarmingTaskReader
} else if cmd.recordToZip != "" {
replayBackends := replaybackends.ToZipFile(cmd.recordToZip, rbeClients, swarmingClient)
defer func() {
if err := replayBackends.Close(); err != nil {
sklog.Fatalf("closing replay backends: %v", err)
}
}()
casResultReader = replayBackends.CASResultReader
swarmingTaskReader = replayBackends.SwarmingTaskReader
}
var analyzerOpts = []analyzer.Options{
analyzer.WithCASResultReader(casResultReader),
analyzer.WithSwarmingTaskReader(swarmingTaskReader),
}
a := analyzer.New(cmd.pinpointJobID, analyzerOpts...)
results, err := a.Run(ctx)
if err != nil {
sklog.Fatalf("failed to analyze %s: %+v", cmd.pinpointJobID, err)
return err
}
printAnalysisResultsTable(results)
return nil
}
var (
defaultRowColor = []int{tablewriter.Normal, tablewriter.FgWhiteColor, tablewriter.BgBlackColor}
rowColorPos = []int{tablewriter.Bold, tablewriter.FgGreenColor, tablewriter.BgBlackColor}
rowColorNeg = []int{tablewriter.Bold, tablewriter.FgRedColor, tablewriter.BgBlackColor}
headerColor = []int{tablewriter.Bold, tablewriter.FgWhiteColor, tablewriter.BgBlackColor}
)
func printAnalysisResultsTable(a []analyzer.Results) {
w := tablewriter.NewWriter(os.Stdout)
headers := []string{"Benchmark", "Workload", "CI Lower", "CI Upper", "P Value"}
w.SetHeader(headers)
headerColors := []tablewriter.Colors{}
for range headers {
headerColors = append(headerColors, headerColor)
}
w.SetHeaderColor(headerColors...)
w.SetAutoFormatHeaders(false)
w.SetCenterSeparator("")
w.SetColumnSeparator("")
w.SetRowSeparator("")
w.SetBorder(false)
w.SetTablePadding("\t")
w.SetNoWhiteSpace(false)
for _, ar := range a {
bmark := ar.Benchmark
workload := ar.WorkLoad
s := ar.Statistics
rc := defaultRowColor
if s.PValue <= 0.05 {
if s.LowerCi < 0 && s.UpperCi < 0 {
if upIsBetter(bmark, workload) {
rc = rowColorNeg
} else {
rc = rowColorPos
}
} else if s.LowerCi > 0 && s.UpperCi > 0 {
if upIsBetter(bmark, workload) {
rc = rowColorPos
} else {
rc = rowColorNeg
}
}
}
row := []string{
bmark,
workload,
fmt.Sprintf("%10.6f", s.LowerCi),
fmt.Sprintf("%10.6f", s.UpperCi),
fmt.Sprintf("%10.6f", s.PValue),
}
rowColors := []tablewriter.Colors{}
for range row {
rowColors = append(rowColors, rc)
}
w.Rich(row, rowColors)
}
w.Render()
}
/*
Ugly hack: Allows us to customize the color-coding for significant changes, which depends on
external client-specific details that we don't and shouldn't know about in shared code like this.
Until we have some kind of central store for authoritative metrics metadata, we'll have
to continue hard-coding special cases that would be better managed as configuration/data than code.
*/
func upIsBetter(benchmark, workload string) bool {
if benchmark == "motionmark" || benchmark == "jetstream2" || strings.Contains(workload, "RunsPerMinute") {
return true
}
return false
}