[tools] Add 'time-cmd'

A command that can time how long a command takes to execute for each file in a filepath glob.

Useful for finding the top-N shaders that take the longest to compile in a large corpus directory.

Change-Id: I416f300f7344480a939b9304bd3b49c378af9fef
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/132641
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
This commit is contained in:
Ben Clayton 2023-05-15 12:49:23 +00:00 committed by Dawn LUCI CQ
parent ac4b5f2bd2
commit 0699b4f21d
2 changed files with 199 additions and 2 deletions

View File

@ -0,0 +1,154 @@
// Copyright 2023 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// time-cmd runs a given command for each file found in a glob, returning the
// sorted run times.
package main
import (
"flag"
"fmt"
"math/rand"
"os"
"os/exec"
"sort"
"strings"
"text/tabwriter"
"time"
"dawn.googlesource.com/dawn/tools/src/fileutils"
"dawn.googlesource.com/dawn/tools/src/glob"
"dawn.googlesource.com/dawn/tools/src/progressbar"
)
func main() {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func run() error {
flag.Usage = func() {
out := flag.CommandLine.Output()
fmt.Fprintf(out, "time-cmd runs a given command for each file found in a glob, returning the sorted run times..\n")
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, "Usage:\n")
fmt.Fprintf(out, " time-cmd [flags] <cmd [args...]>\n")
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, "cmd is the path to the command to run\n")
fmt.Fprintf(out, "args are optional command line arguments to 'cmd'\n")
fmt.Fprintf(out, "args should include '%%F' for the globbed file path\n")
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, "flags may be any combination of:\n")
flag.PrintDefaults()
}
var fileGlob string
var top, runs int
flag.StringVar(&fileGlob, "f", "", "the files to glob. Paths are relative to the dawn root directory")
flag.IntVar(&top, "top", 0, "displays the longest N results")
flag.IntVar(&runs, "runs", 1, "takes the average of N runs")
flag.Parse()
args := flag.Args()
if fileGlob == "" {
flag.Usage()
return fmt.Errorf("Missing --f flag")
}
if len(args) < 1 {
flag.Usage()
return fmt.Errorf("Missing command")
}
fileGlob = fileutils.ExpandHome(fileGlob)
exe, err := exec.LookPath(fileutils.ExpandHome(args[0]))
if err != nil {
return fmt.Errorf("could not find executable '%v'", args[0])
}
files, err := glob.Glob(fileGlob)
if err != nil {
return err
}
if len(files) == 0 {
fmt.Println("no files found with glob '" + fileGlob + "'")
return nil
}
pb := progressbar.New(os.Stdout, nil)
defer pb.Stop()
type Result struct {
file string
time time.Duration
errs []error
}
status := progressbar.Status{
Total: len(files) * runs,
Segments: []progressbar.Segment{{Color: progressbar.Green}, {Color: progressbar.Red}},
}
segSuccess := &status.Segments[0]
segError := &status.Segments[1]
results := make([]Result, len(files))
for i, f := range files {
results[i].file = f
}
for run := 0; run < runs; run++ {
rand.Shuffle(len(results), func(i, j int) { results[i], results[j] = results[j], results[i] })
for i := range results {
result := &results[i]
exeArgs := make([]string, len(args[1:]))
for i, arg := range args[1:] {
exeArgs[i] = strings.ReplaceAll(arg, "%F", result.file)
}
cmd := exec.Command(exe, exeArgs...)
start := time.Now()
out, err := cmd.CombinedOutput()
time := time.Since(start)
result.time += time
if err != nil {
result.errs = append(result.errs, fmt.Errorf("%v", string(out)))
segError.Count++
} else {
segSuccess.Count++
}
pb.Update(status)
}
}
sort.Slice(results, func(i, j int) bool { return results[i].time > results[j].time })
if top > 0 {
if top > len(results) {
top = len(results)
}
results = results[:top]
}
tw := tabwriter.NewWriter(os.Stdout, 1, 0, 1, ' ', 0)
defer tw.Flush()
for i, r := range results {
s := ""
if len(r.errs) > 0 {
s = fmt.Sprint(r.errs)
}
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\n", i, r.time/time.Duration(runs), r.file, s)
}
return nil
}

View File

@ -27,6 +27,43 @@ import (
"dawn.googlesource.com/dawn/tools/src/match" "dawn.googlesource.com/dawn/tools/src/match"
) )
// Glob returns all the strings that match the given filepath glob
func Glob(str string) ([]string, error) {
abs, err := filepath.Abs(str)
if err != nil {
return nil, err
}
root, glob := "", ""
// Look for rightmost directory delimiter that's left of a wildcard. Use
// that to split the 'root' from the match 'glob'.
for i, c := range abs {
switch c {
case '/':
root, glob = abs[:i], abs[i+1:]
case '*', '?':
test, err := match.New(glob)
if err != nil {
return nil, err
}
files, err := Scan(root, Config{Paths: searchRules{
func(path string, cond bool) bool { return test(path) },
}})
if err != nil {
return nil, err
}
for i, f := range files {
files[i] = filepath.Join(root, f) // rel -> abs
}
return files, nil
}
}
// No wildcard found. Does the file exist at 'str'?
if s, err := os.Stat(str); err != nil && !s.IsDir() {
return []string{str}, nil
}
return []string{}, nil
}
// Scan walks all files and subdirectories from root, returning those // Scan walks all files and subdirectories from root, returning those
// that Config.shouldExamine() returns true for. // that Config.shouldExamine() returns true for.
func Scan(root string, cfg Config) ([]string, error) { func Scan(root string, cfg Config) ([]string, error) {
@ -155,12 +192,15 @@ func (l *searchRules) UnmarshalJSON(body []byte) error {
tests[i] = test tests[i] = test
} }
*l = append(*l, func(path string, cond bool) bool { *l = append(*l, func(path string, cond bool) bool {
if cond {
return true
}
for _, test := range tests { for _, test := range tests {
if test(path) { if test(path) {
return true return true
} }
} }
return cond return false
}) })
case len(rule.Exclude) > 0: case len(rule.Exclude) > 0:
tests := make([]match.Test, len(rule.Exclude)) tests := make([]match.Test, len(rule.Exclude))
@ -172,12 +212,15 @@ func (l *searchRules) UnmarshalJSON(body []byte) error {
tests[i] = test tests[i] = test
} }
*l = append(*l, func(path string, cond bool) bool { *l = append(*l, func(path string, cond bool) bool {
if !cond {
return false
}
for _, test := range tests { for _, test := range tests {
if test(path) { if test(path) {
return false return false
} }
} }
return cond return true
}) })
} }
} }