blob: c470629218b68185a7806090d39a36fd0d5e7327 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package metricscmd
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"github.com/maruel/subcommands"
"go.chromium.org/luci/common/cli"
"infra/build/siso/build"
)
const cmpUsage = `compare siso_metrics.json.
$ siso metrics cmp -C <dir> [--format <format>] \
[--input_a siso_metrics.json] \
[--input_b siso_metrics.json.0]
compares <dir>/siso_metrics.json (--input_a) and
<dir>/siso_metrics.json.0 (--input_b).
output format can be "diff" or "join".
diff shows duration difference: input_a - input_b
to see difference for each duration per outputs,
so what outputs became slower or what step of the outputs became slower
than before.
join makes a pair of metrics for each output.
to see difference per outputs,
e.g. check cmdhash, action has been changed etc.
default output is diff.
`
// Cmd returns the Command for the `metricscmp` subcommand provided by this package.
func cmpCmd() *subcommands.Command {
return &subcommands.Command{
UsageLine: "cmp <args>...",
ShortDesc: "compare siso_metrics.json",
LongDesc: cmpUsage,
CommandRun: func() subcommands.CommandRun {
c := &cmpRun{}
c.init()
return c
},
}
}
type cmpRun struct {
subcommands.CommandRunBase
dir string
inputA, inputB string
format string
}
var formats = map[string]func([]build.StepMetric) error{
"diff": outputDiff,
"join": outputJoin,
}
var formatKeys = func() []string {
var keys []string
for k := range formats {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}()
func (c *cmpRun) init() {
c.Flags.StringVar(&c.dir, "C", ".", "ninja running directory")
c.Flags.StringVar(&c.inputA, "input_a", "siso_metrics.json", "target siso_metrics.json")
c.Flags.StringVar(&c.inputB, "input_b", "siso_metrics.json.0", "base siso_metrics.json")
c.Flags.StringVar(&c.format, "format", "diff", fmt.Sprintf("output format: %q", formatKeys))
}
func (c *cmpRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
ctx := cli.GetContext(a, c, env)
err := c.run(ctx)
if err != nil {
switch {
case errors.Is(err, flag.ErrHelp):
fmt.Fprintf(os.Stderr, "%v\n%s\n", err, cmpUsage)
default:
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
return 1
}
return 0
}
func (c *cmpRun) run(ctx context.Context) error {
output, ok := formats[c.format]
if !ok {
return fmt.Errorf("unknown format %q: known formats %q: %w", c.format, formatKeys, flag.ErrHelp)
}
x, err := loadMetrics(ctx, filepath.Join(c.dir, c.inputA))
if err != nil {
return err
}
y, err := loadMetrics(ctx, filepath.Join(c.dir, c.inputB))
if err != nil {
return err
}
z := join(x, y)
for _, m := range z {
err := output(m)
if err != nil {
return err
}
}
return nil
}
func outputJoin(m []build.StepMetric) error {
buf, err := json.Marshal(m)
if err != nil {
return err
}
fmt.Printf("%s\n", buf)
return nil
}
func outputDiff(m []build.StepMetric) error {
d := struct {
Action string `json:"action"`
Rule string `json:"rule,omitempty"`
Output string `json:"output"`
Ready build.IntervalMetric `json:"ready"`
Start build.IntervalMetric `json:"start"`
Duration build.IntervalMetric `json:"duration"`
WeightedDuration build.IntervalMetric `json:"weighted_duration"`
RunTime build.IntervalMetric `json:"run"`
QueueTime build.IntervalMetric `json:"queue"`
ExecTime build.IntervalMetric `json:"exec"`
}{
Action: m[0].Action,
Output: m[0].Output,
Ready: m[0].Ready - m[1].Ready,
Start: m[0].Start - m[1].Start,
Duration: m[0].Duration - m[1].Duration,
WeightedDuration: m[0].WeightedDuration - m[1].WeightedDuration,
RunTime: m[0].RunTime - m[1].RunTime,
QueueTime: m[0].QueueTime - m[1].QueueTime,
ExecTime: m[0].ExecTime - m[1].ExecTime,
}
buf, err := json.Marshal(d)
if err != nil {
return err
}
fmt.Printf("%s\n", buf)
return nil
}
func join(x, y []build.StepMetric) [][]build.StepMetric {
xm := indexMap(x)
ym := indexMap(y)
km := map[string]bool{}
var keys []string
for k := range xm {
keys = append(keys, k)
km[k] = true
}
for k := range ym {
if km[k] {
continue
}
keys = append(keys, k)
km[k] = true
}
sort.Strings(keys)
var r [][]build.StepMetric
for _, k := range keys {
r = append(r, []build.StepMetric{xm[k], ym[k]})
}
return r
}
func indexMap(x []build.StepMetric) map[string]build.StepMetric {
r := make(map[string]build.StepMetric)
for _, m := range x {
output := m.Output
r[output] = m
}
return r
}