[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:
parent
ac4b5f2bd2
commit
0699b4f21d
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue