| // Copyright 2022 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| package time | |
| import ( | |
| "context" | |
| "flag" | |
| "fmt" | |
| "math" | |
| "sort" | |
| "time" | |
| "dawn.googlesource.com/dawn/tools/src/auth" | |
| "dawn.googlesource.com/dawn/tools/src/cmd/cts/common" | |
| "dawn.googlesource.com/dawn/tools/src/cts/query" | |
| "dawn.googlesource.com/dawn/tools/src/cts/result" | |
| "dawn.googlesource.com/dawn/tools/src/subcmd" | |
| "go.chromium.org/luci/auth/client/authcli" | |
| ) | |
| func init() { | |
| common.Register(&cmd{}) | |
| } | |
| type cmd struct { | |
| flags struct { | |
| source common.ResultSource | |
| auth authcli.Flags | |
| tags string | |
| query string | |
| aggregate bool | |
| topN int | |
| histogram bool | |
| } | |
| } | |
| func (cmd) Name() string { | |
| return "time" | |
| } | |
| func (cmd) Desc() string { | |
| return "displays timing information for tests" | |
| } | |
| func (c *cmd) RegisterFlags(ctx context.Context, cfg common.Config) ([]string, error) { | |
| c.flags.source.RegisterFlags(cfg) | |
| c.flags.auth.Register(flag.CommandLine, auth.DefaultAuthOptions(cfg.OsWrapper)) | |
| flag.IntVar(&c.flags.topN, "top", 0, "print the top N slowest tests") | |
| flag.BoolVar(&c.flags.histogram, "histogram", false, "print a histogram of test timings") | |
| flag.StringVar(&c.flags.query, "query", "", "test query to filter results") | |
| flag.StringVar(&c.flags.tags, "tags", "", "comma-separated list of tags to filter results") | |
| flag.BoolVar(&c.flags.aggregate, "aggregate", false, "aggregate times by test") | |
| return nil, nil | |
| } | |
| func (c *cmd) Run(ctx context.Context, cfg common.Config) error { | |
| // Validate command line arguments | |
| auth, err := c.flags.auth.Options() | |
| if err != nil { | |
| return fmt.Errorf("failed to obtain authentication options: %w", err) | |
| } | |
| // Obtain the resultsByExecutionMode | |
| resultsByExecutionMode, err := c.flags.source.GetResults(ctx, cfg, auth) | |
| if err != nil { | |
| return err | |
| } | |
| if len(resultsByExecutionMode) == 0 { | |
| return fmt.Errorf("no results found") | |
| } | |
| // If tags were provided, filter the results to those that contain these tags | |
| if c.flags.tags != "" { | |
| for name := range resultsByExecutionMode { | |
| resultsByExecutionMode[name] = resultsByExecutionMode[name].FilterByTags(result.StringToTags(c.flags.tags)) | |
| } | |
| if len(resultsByExecutionMode) == 0 { | |
| return fmt.Errorf("no results after filtering by tags") | |
| } | |
| } | |
| if c.flags.query != "" { | |
| for name := range resultsByExecutionMode { | |
| resultsByExecutionMode[name] = resultsByExecutionMode[name].FilterByQuery(query.Parse(c.flags.query)) | |
| } | |
| if len(resultsByExecutionMode) == 0 { | |
| return fmt.Errorf("no results after filtering by test query") | |
| } | |
| } | |
| if c.flags.aggregate { | |
| type Key struct { | |
| Query query.Query | |
| Status result.Status | |
| Tags string | |
| } | |
| merged := map[Key]result.Result{} | |
| for name, results := range resultsByExecutionMode { | |
| for _, r := range results { | |
| k := Key{ | |
| Query: query.Query{ | |
| Suite: r.Query.Suite, | |
| Files: r.Query.Files, | |
| Tests: r.Query.Tests, | |
| Cases: "*", | |
| }, | |
| Status: r.Status, | |
| Tags: result.TagsToString(r.Tags), | |
| } | |
| entry, exists := merged[k] | |
| if exists { | |
| entry.Duration += r.Duration | |
| } else { | |
| entry = result.Result{ | |
| Query: k.Query, | |
| Duration: r.Duration, | |
| Status: r.Status, | |
| Tags: r.Tags, | |
| } | |
| } | |
| merged[k] = entry | |
| } | |
| newResultList := result.List{} | |
| for _, r := range merged { | |
| newResultList = append(results, r) | |
| } | |
| resultsByExecutionMode[name] = newResultList | |
| } | |
| } | |
| // Sort the results with longest duration first | |
| for name, results := range resultsByExecutionMode { | |
| sort.Slice(results, func(i, j int) bool { | |
| return results[i].Duration > results[j].Duration | |
| }) | |
| resultsByExecutionMode[name] = results | |
| } | |
| didSomething := false | |
| // Did the user request --top N ? | |
| if c.flags.topN > 0 { | |
| didSomething = true | |
| for name, results := range resultsByExecutionMode { | |
| topN := results | |
| if c.flags.topN < len(results) { | |
| topN = topN[:c.flags.topN] | |
| } | |
| for i, r := range topN { | |
| fmt.Printf("%s %3.1d: %v\n", name, i, r) | |
| } | |
| } | |
| } | |
| // Did the user request --histogram ? | |
| if c.flags.histogram { | |
| for name, results := range resultsByExecutionMode { | |
| maxTime := results[0].Duration | |
| const ( | |
| numBins = 25 | |
| pow = 2.0 | |
| ) | |
| binToDuration := func(i int) time.Duration { | |
| frac := math.Pow(float64(i)/float64(numBins), pow) | |
| return time.Duration(float64(maxTime) * frac) | |
| } | |
| durationToBin := func(d time.Duration) int { | |
| frac := math.Pow(float64(d)/float64(maxTime), 1.0/pow) | |
| idx := int(frac * numBins) | |
| if idx >= numBins-1 { | |
| return numBins - 1 | |
| } | |
| return idx | |
| } | |
| didSomething = true | |
| bins := make([]int, numBins) | |
| for _, r := range results { | |
| idx := durationToBin(r.Duration) | |
| bins[idx] = bins[idx] + 1 | |
| } | |
| fmt.Printf("%s\n", name) | |
| for i, bin := range bins { | |
| fmt.Printf("[%.8v, %.8v]: %v\n", binToDuration(i), binToDuration(i+1), bin) | |
| } | |
| } | |
| } | |
| // If the user didn't request anything, show a helpful message | |
| if !didSomething { | |
| fmt.Fprintln(flag.CommandLine.Output(), "no action flags specified for", c.Name()) | |
| fmt.Fprintln(flag.CommandLine.Output()) | |
| flag.Usage() | |
| return subcmd.ErrInvalidCLA | |
| } | |
| return nil | |
| } |