blob: 3602726f1f72e97b21eccb76f2ca56750d5b2349 [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"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/maruel/subcommands"
"go.chromium.org/luci/common/cli"
"infra/build/siso/build"
)
const summaryUsage = `summarize siso_metrics.json
$ siso metrics summary -C <dir> \
[--step_types <types>] \
[--elapsed_time_sorting] \
[--elapsed_time=run|step] \
[--input siso_metrics.json]
summarize <dir>/.siso_metrics.json (--input)
as depot_tools/post_ninja_build_summary.py does.
`
// summaryCmd returns the Command for the `metricssummary` subcommand provided by this package.
func summaryCmd() *subcommands.Command {
return &subcommands.Command{
UsageLine: "summary <args>...",
ShortDesc: "summarize siso_metrics.json",
LongDesc: summaryUsage,
CommandRun: func() subcommands.CommandRun {
c := &summaryRun{}
c.init()
return c
},
}
}
type summaryRun struct {
subcommands.CommandRunBase
dir string
input string
stepTypes string
elapsedTime string
elapsedTimeSorting bool
}
func (c *summaryRun) init() {
c.Flags.StringVar(&c.dir, "C", ".", "ninja running directory, where siso_metrics.json exists")
c.Flags.StringVar(&c.input, "input", "siso_metrics.json", "filename of siso_metrics.json to summarize")
c.Flags.StringVar(&c.stepTypes, "step_types", "", "semicolon separated glob patterns (go filepath.Match) for build-step grouping")
c.Flags.StringVar(&c.elapsedTime, "elapsed_time", "run", `metrics to use for elapsed time. "run" or "step". "run": time to run local command or call remote execution. "step": full duration for the step, including preproc, waiting resource to run command etc.`)
c.Flags.BoolVar(&c.elapsedTimeSorting, "elapsed_time_sorting", false, "Sort output by elapsed time instead of weighted time")
}
func (c *summaryRun) 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, summaryUsage)
default:
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
return 1
}
return 0
}
func (c *summaryRun) run(ctx context.Context) error {
switch c.elapsedTime {
case "run", "step":
default:
return fmt.Errorf(`wrong --elapsed_time=%s "run" or "step". %w`, c.elapsedTime, flag.ErrHelp)
}
err := os.Chdir(c.dir)
if err != nil {
return err
}
metrics, err := loadMetrics(ctx, c.input)
if err != nil {
return err
}
var filteredMetrics []build.StepMetric
for _, s := range metrics {
if s.StepID == "" {
// this is special entry for build metrics, not per step metrics.
continue
}
filteredMetrics = append(filteredMetrics, s)
}
metrics = filteredMetrics
if len(metrics) == 0 {
return nil
}
// TODO(ukai): deduce wait time from the duration?
// same algorithm with post_build_ninja_summary.py
// https://chromium.googlesource.com/chromium/tools/depot_tools/+/80226254ea024e756b9ad5e8a39160405880cbb1/post_build_ninja_summary.py#214
var accumulatedDuration time.Duration
var m []*targetMetric
var events []buildEvent
var earliest, latest time.Duration
for _, s := range metrics {
var start, end time.Duration
switch c.elapsedTime {
case "run":
start = time.Duration(s.ActionStartTime)
end = start + time.Duration(s.RunTime)
case "step":
start = time.Duration(s.Ready) + time.Duration(s.Start)
end = start + time.Duration(s.Duration)
}
if start == 0 && end == 0 {
// handler only step (i.e. stamp, copy) doesn't have ActionStartTime/RunTime, so ignore such steps.
continue
}
if earliest == 0 || start < earliest {
earliest = start
}
if end > latest {
latest = end
}
target := &targetMetric{
Output: s.Output,
Start: start,
End: end,
}
m = append(m, target)
accumulatedDuration += target.Duration()
events = append(events, buildEvent{
ts: target.Start,
event: eventStart,
target: target,
}, buildEvent{
ts: target.End,
event: eventStop,
target: target,
})
}
if len(events) == 0 {
return nil
}
length := latest - earliest
// sort by time/event records
sort.Slice(events, func(i, j int) bool {
if events[i].ts < events[j].ts {
return true
}
return events[i].event < events[j].event
})
// current running task -> weighted time when the task started.
runningTasks := make(map[*targetMetric]time.Duration)
lastTime := events[0].ts
var lastWeightedTime time.Duration
var accumulatedWeightedDuration time.Duration
for _, ev := range events {
numRunning := len(runningTasks)
if numRunning > 0 {
lastWeightedTime += time.Duration(float64(ev.ts-lastTime) / float64(numRunning))
}
switch ev.event {
case eventStart:
runningTasks[ev.target] = lastWeightedTime
case eventStop:
ev.target.weighted = lastWeightedTime - runningTasks[ev.target]
accumulatedWeightedDuration += ev.target.weighted
delete(runningTasks, ev.target)
}
lastTime = ev.ts
}
// Warn if the sum of weighted times is off by more than half a second.
wdiff := length - accumulatedWeightedDuration
if wdiff.Abs() > 500*time.Millisecond {
fmt.Printf("Warning: Possible corrupt siso_metrics.json, result may be untrustworthy. length = %s, weighted total = %s\n", formatDuration(length), formatDuration(accumulatedWeightedDuration))
}
// Print the slowest build steps:
fmt.Println(" Longest build steps:")
if c.elapsedTimeSorting {
sort.Slice(m, func(i, j int) bool {
return m[i].Duration() > m[j].Duration()
})
} else {
sort.Slice(m, func(i, j int) bool {
return m[i].WeightedDuration() > m[j].WeightedDuration()
})
}
longCount := 10
topMetrics := m
if len(topMetrics) > longCount {
topMetrics = topMetrics[:longCount]
}
for i := len(topMetrics) - 1; i >= 0; i-- {
tm := topMetrics[i]
fmt.Printf(" %8s weighted to build %s (%s elapsed time)\n",
formatDuration(time.Duration(tm.WeightedDuration())),
relPath(c.dir, tm.Output),
formatDuration(tm.Duration()))
}
// Sum up the time by file extension/type of the output file
am, err := c.aggregate(m)
if err != nil {
return err
}
fmt.Println(" Time by build-step type:")
if c.elapsedTimeSorting {
sort.Slice(am, func(i, j int) bool {
return am[i].Duration > am[j].Duration
})
} else {
sort.Slice(am, func(i, j int) bool {
return am[i].WeightedDuration > am[j].WeightedDuration
})
}
// Print the slowest build target types:
longCount += len(strings.Split(c.stepTypes, ";"))
if len(am) > longCount {
am = am[:longCount]
}
for i := len(am) - 1; i >= 0; i-- {
s := am[i]
fmt.Printf(" %8s weighted to generate %d %s files (%s elapsed time sum)\n",
formatDuration(s.WeightedDuration),
s.Count,
s.Type,
formatDuration(s.Duration))
}
if length == 0 {
return nil
}
fmt.Printf(" %s weighted time (%s elapsed time sum, %1.1fx parallelism)\n",
formatDuration(length),
formatDuration(accumulatedDuration),
accumulatedDuration.Seconds()/length.Seconds())
fmt.Printf(" %d build steps completed, average of %1.2f/s\n",
len(metrics),
float64(len(metrics))/length.Seconds())
return nil
}
type targetMetric struct {
Output string
Start time.Duration
End time.Duration
weighted time.Duration
}
func (t targetMetric) Duration() time.Duration {
return t.End - t.Start
}
func (t targetMetric) WeightedDuration() time.Duration {
return t.weighted
}
type eventType int
const (
eventStart eventType = iota
eventStop
)
type buildEvent struct {
ts time.Duration
event eventType
target *targetMetric
}
type aggregatedMetric struct {
Type string
Count int
Duration time.Duration
WeightedDuration time.Duration
}
func (c *summaryRun) aggregate(metrics []*targetMetric) ([]aggregatedMetric, error) {
pats := strings.Split(c.stepTypes, ";")
am := make(map[string]aggregatedMetric)
for _, m := range metrics {
t, err := stepType(m, pats)
if err != nil {
return nil, err
}
a := am[t]
a.Type = t
a.Count++
a.Duration += m.Duration()
a.WeightedDuration += time.Duration(m.WeightedDuration())
am[t] = a
}
var ret []aggregatedMetric
for _, v := range am {
ret = append(ret, v)
}
return ret, nil
}
func relPath(base, fname string) string {
r, err := filepath.Rel(base, fname)
if err != nil {
return fname
}
return r
}
func stepType(metric *targetMetric, pats []string) (string, error) {
for _, p := range pats {
ok, err := filepath.Match(p, metric.Output)
if err != nil {
return "", err
}
if ok {
return p, nil
}
}
return filepath.Ext(metric.Output), nil
}
func formatDuration(d time.Duration) string {
return fmt.Sprintf("%.1fs", d.Seconds())
}