| // 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()) |
| 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 |
| } |