blob: 777fb60899ab6fc79a9ba6acf80e12ff7c7d2670 [file] [log] [blame]
// Command-line application for interacting with Perf.
package main
import (
"context"
"fmt"
"os"
cli "github.com/urfave/cli/v2"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog/nooplogging"
"go.skia.org/infra/go/sklog/sklogimpl"
"go.skia.org/infra/go/sklog/stdlogging"
"go.skia.org/infra/go/urfavecli"
"go.skia.org/infra/perf/go/builders"
"go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/perf-tool/application"
"go.skia.org/infra/perf/go/tracestore"
"go.skia.org/infra/perf/go/types"
)
// flag names
const (
backupToDateFlagName = "backup_to_date"
beginCommitFlagName = "begin"
configFilenameFlagName = "config_filename"
connectionStringFlagName = "connection_string"
dryrunFlagName = "dryrun"
endCommitFlagName = "end"
inputFilenameFlagName = "in"
localFlagName = "local"
loggingFlagName = "logging"
numTilesListFlagName = "num"
outputFilenameFlagName = "out"
queryFlagName = "query"
startTimeFlagName = "start"
stopTimeFlagName = "stop"
tileNumberFlagName = "tile"
trybotFilenameFlagName = "filename"
trybotNumCommitsFlagName = "num"
verboseFlagName = "verbose"
)
// flags
var trybotFilenameFlag = &cli.StringFlag{
Name: trybotFilenameFlagName,
Value: "",
Usage: "The full URL of a nanobench trybot results files, e.g.: 'gs://skia-perf/...foo.json'",
}
var connectionStringFlag = &cli.StringFlag{
Name: connectionStringFlagName,
Value: "",
Usage: "Override the connection string in the config file.",
EnvVars: []string{"PERF_CONNECTION_STRING"},
}
var requiredOutputFilenameFlag = &cli.StringFlag{
Name: outputFilenameFlagName,
Value: "",
Usage: "The output filename.",
Required: true,
}
var optionalOutputFilenameFlag = &cli.StringFlag{
Name: outputFilenameFlagName,
Value: "",
Usage: "The output filename.",
}
var inputFilenameFlag = &cli.StringFlag{
Name: inputFilenameFlagName,
Value: "",
Usage: "The input filename.",
Required: true,
}
var backupToDateFlag = &cli.StringFlag{
Name: backupToDateFlagName,
Value: "",
Usage: "How far back in time to back up Regressions. Defaults to four weeks.",
}
var configFilenameFlag = &cli.StringFlag{
Name: configFilenameFlagName,
Value: "",
Usage: "Load configuration from `FILE`",
EnvVars: []string{"PERF_CONFIG_FILENAME"},
Required: true,
}
var localFlag = &cli.BoolFlag{
Name: localFlagName,
Value: true,
Usage: "If true then use gcloud credentials.",
}
var queryFlag = &cli.StringFlag{
Name: queryFlagName,
Value: "",
Usage: "The query to run.",
Required: true,
}
var tileNumberFlag = &cli.Int64Flag{
Name: tileNumberFlagName,
Value: int64(types.BadTileNumber),
Usage: "The tile to query.",
}
var numTilesListFlag = &cli.IntFlag{
Name: numTilesListFlagName,
Value: 10,
Usage: "The number of tiles to display.",
EnvVars: []string{"PERF_CONFIG_FILENAME"},
}
var trybotNumCommitsFlag = &cli.IntFlag{
Name: trybotNumCommitsFlagName,
Value: 5,
Usage: "The number of ingestion files to load.",
}
var beginCommitFlag = &cli.Int64Flag{
Name: beginCommitFlagName,
Value: int64(types.BadCommitNumber),
Usage: "The commit number to start loading data from. Inclusive.",
Required: true,
}
var endCommitFlag = &cli.Int64Flag{
Name: endCommitFlagName,
Value: int64(types.BadCommitNumber),
Usage: "The commit number to load data to.",
}
var startTimeFlag = &cli.StringFlag{
Name: startTimeFlagName,
Value: "",
Usage: "Start the ingestion at this time, of the form: 2006-01-02. Default to one week ago.",
}
var stopTimeFlag = &cli.StringFlag{
Name: stopTimeFlagName,
Value: "",
Usage: "Ingest up to this time, of the form: 2006-01-02. Default to now.",
}
var dryrunFlag = &cli.BoolFlag{
Name: dryrunFlagName,
Value: false,
Usage: "Just display the list of files to send.",
}
var loggingFlag = &cli.BoolFlag{
Name: loggingFlagName,
Value: false,
Usage: "Turn on logging while running commands.",
}
var verboseFlag = &cli.BoolFlag{
Name: verboseFlagName,
Value: true,
Usage: "Verbose output.",
}
// instanceConfigFromFlags returns an InstanceConfig based
// on the flags configFilenameFlag and connectionStringFlag.
func instanceConfigFromFlags(c *cli.Context) (*config.InstanceConfig, error) {
instanceConfig, schemaViolations, err := config.InstanceConfigFromFile(c.String(configFilenameFlagName))
if err != nil {
for _, v := range schemaViolations {
fmt.Println(v)
}
return nil, skerr.Wrap(err)
}
override := c.String(connectionStringFlagName)
if override != "" {
instanceConfig.DataStoreConfig.ConnectionString = override
}
return instanceConfig, nil
}
// getStore returns a TraceStore built on the flags configFilenameFlag and
// connectionStringFlag.
func getStore(c *cli.Context) (tracestore.TraceStore, error) {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return nil, skerr.Wrap(err)
}
local := c.Bool(localFlagName)
return builders.NewTraceStoreFromConfig(context.Background(), local, instanceConfig)
}
func main() {
app := application.New()
actualMain(app)
}
func actualMain(app application.Application) {
cli.MarkdownDocTemplate = urfavecli.MarkdownDocTemplate
cliApp := &cli.App{
Name: "perf-tool",
Usage: "Command-line tool for working with Perf data.",
Flags: []cli.Flag{
loggingFlag,
},
Before: func(c *cli.Context) error {
if c.Bool(loggingFlagName) {
sklogimpl.SetLogger(stdlogging.New(os.Stderr))
} else {
sklogimpl.SetLogger(nooplogging.New())
}
return nil
},
Commands: []*cli.Command{
{
Name: "config",
Subcommands: []*cli.Command{
{
Name: "create-pubsub-topics",
Usage: "Create PubSub topics for the given big_table_config.",
Flags: []cli.Flag{
configFilenameFlag,
connectionStringFlag,
},
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.ConfigCreatePubSubTopics(instanceConfig)
},
},
{
Name: "validate",
Usage: "Validate the given config",
Flags: []cli.Flag{
configFilenameFlag,
},
Action: func(c *cli.Context) error {
_, err := instanceConfigFromFlags(c)
if err != nil {
// Unwrap the error since this gets printed as a user facing error message.
return fmt.Errorf("Validation Failed: %s", skerr.Unwrap(err))
}
return nil
},
},
},
},
{
Name: "tiles",
Subcommands: []*cli.Command{
{
Name: "last",
Usage: "Prints the index of the last (most recent) tile.",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
},
Action: func(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return skerr.Wrap(err)
}
return app.TilesLast(store)
},
},
{
Name: "list",
Usage: "Prints the last N tiles and the number of traces they contain.",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
numTilesListFlag,
},
Action: func(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return skerr.Wrap(err)
}
return app.TilesList(store, c.Int(numTilesListFlagName))
},
},
},
},
{
Name: "traces",
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "Prints the IDs of traces in the last (most recent) tile, or the tile specified by the --tile flag, that match --query.",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
queryFlag,
tileNumberFlag,
},
Action: func(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return skerr.Wrap(err)
}
return app.TracesList(
store,
c.String(queryFlagName),
types.TileNumber(c.Int64(tileNumberFlagName)))
},
},
{
Name: "export",
Usage: "Writes a JSON files with the traces that match --query for the given range of commits.",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
queryFlag,
optionalOutputFilenameFlag,
beginCommitFlag,
endCommitFlag,
},
Action: func(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return skerr.Wrap(err)
}
return app.TracesExport(
store,
c.String(queryFlagName),
types.CommitNumber(c.Int64(beginCommitFlagName)),
types.CommitNumber(c.Int64(endCommitFlagName)),
c.String(outputFilenameFlagName))
},
},
},
},
{
Name: "ingest",
Subcommands: []*cli.Command{
{
Name: "force-reingest",
Description: "Force re-ingestion of files.",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
startTimeFlag,
stopTimeFlag,
dryrunFlag,
},
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.IngestForceReingest(
c.Bool(localFlagName),
instanceConfig,
c.String(startTimeFlagName),
c.String(stopTimeFlagName),
c.Bool(dryrunFlagName))
},
},
{
Name: "validate",
Description: "Validate an ingestion file",
Flags: []cli.Flag{
inputFilenameFlag,
verboseFlag,
},
Action: func(c *cli.Context) error {
return app.IngestValidate(c.String(inputFilenameFlag.Name), c.Bool(verboseFlag.Name))
},
},
},
},
{
Name: "database",
Subcommands: []*cli.Command{
{
Name: "migrate",
Usage: "Migrate the database to the latest version of the schema.",
Flags: []cli.Flag{
configFilenameFlag,
connectionStringFlag,
},
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.DatabaseMigrate(instanceConfig)
},
},
{
Name: "backup",
Subcommands: []*cli.Command{
{
Name: "alerts",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
requiredOutputFilenameFlag,
},
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.DatabaseBackupAlerts(c.Bool(localFlagName), instanceConfig, c.String(outputFilenameFlagName))
},
},
{
Name: "shortcuts",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
requiredOutputFilenameFlag,
},
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.DatabaseBackupShortcuts(c.Bool(localFlagName), instanceConfig, c.String(outputFilenameFlagName))
},
},
{
Name: "regressions",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
requiredOutputFilenameFlag,
backupToDateFlag,
},
Description: `Backups up regressions and any shortcuts they rely on.
When restoring you must restore twice, first:
'perf-tool database restore regressions'
and then:
'perf-tool database restore shortcuts'
using the same input file for both restores.
`,
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.DatabaseBackupRegressions(c.Bool(localFlagName), instanceConfig, c.String(outputFilenameFlagName), c.String(backupToDateFlagName))
},
},
},
},
{
Name: "restore",
Subcommands: []*cli.Command{
{
Name: "alerts",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
inputFilenameFlag,
},
Description: "Restores the alerts from the given file.",
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.DatabaseRestoreAlerts(c.Bool(localFlagName), instanceConfig, c.String(inputFilenameFlagName))
},
},
{
Name: "shortcuts",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
inputFilenameFlag,
},
Description: "Restores the shortcuts from the given file.",
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.DatabaseRestoreShortcuts(c.Bool(localFlagName), instanceConfig, c.String(inputFilenameFlagName))
},
},
{
Name: "regressions",
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
inputFilenameFlag,
},
Description: "Restores from the given backup both the regressions and their associated shortcuts.",
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
return app.DatabaseRestoreRegressions(c.Bool(localFlagName), instanceConfig, c.String(inputFilenameFlagName))
},
},
},
},
},
},
{
Name: "trybot",
Subcommands: []*cli.Command{
{
Name: "reference",
Usage: "Generates a reference file to be used by nanostat for the given trybot file.",
Description: `
This command constructs a synthetic nanobench file filled with 'samples' for
each of the trace ids found in the nanobench file given in --filename.
The samples in the sythentic file are the samples found in the last --num
commits of ingested perf data for all the traces found in --filename.
This allows building a nanobench file with a large number of samples
that can be compared with a nanobench file from a trybot using the nanostat
application.
This is an experimental function that may go away in the future.
`,
Flags: []cli.Flag{
localFlag,
configFilenameFlag,
connectionStringFlag,
trybotFilenameFlag,
trybotNumCommitsFlag,
requiredOutputFilenameFlag,
},
Action: func(c *cli.Context) error {
instanceConfig, err := instanceConfigFromFlags(c)
if err != nil {
return skerr.Wrap(err)
}
store, err := getStore(c)
if err != nil {
return skerr.Wrap(err)
}
return app.TrybotReference(c.Bool(localFlagName), store, instanceConfig, c.String(trybotFilenameFlagName), c.String(outputFilenameFlagName), c.Int(trybotNumCommitsFlagName))
},
},
},
},
{
Name: "markdown",
Usage: "Generates markdown help for perf-tool.",
Action: func(c *cli.Context) error {
body, err := c.App.ToMarkdown()
if err != nil {
return skerr.Wrap(err)
}
fmt.Println(body)
return nil
},
},
},
}
cliApp.EnableBashCompletion = true
err := cliApp.Run(os.Args)
if err != nil {
fmt.Printf("\nError: %s\n", err.Error())
os.Exit(2)
}
}