tools: Add the 'cts time' sub-command
Add the common utilities for starting and fetching build results. 'cts time' provides views on the longest running tests, along with a histogram view of all test times. Bug: dawn:1342 Change-Id: Ia3707f7f062ea26a2406e3163a26e1cc7e30e3b2 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/88319 Reviewed-by: Corentin Wallez <cwallez@chromium.org> Kokoro: Kokoro <noreply+kokoro@google.com>
This commit is contained in:
parent
251e464af1
commit
40bed821f8
2
go.mod
2
go.mod
|
@ -27,8 +27,10 @@ require (
|
||||||
github.com/klauspost/compress v1.13.5 // indirect
|
github.com/klauspost/compress v1.13.5 // indirect
|
||||||
github.com/kr/pretty v0.3.0 // indirect
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/maruel/subcommands v1.1.0 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.8.0 // indirect
|
github.com/rogpeppe/go-internal v1.8.0 // indirect
|
||||||
|
github.com/texttheater/golang-levenshtein v1.0.1 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -337,7 +337,9 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6Fm
|
||||||
github.com/luci/gtreap v0.0.0-20161228054646-35df89791e8f/go.mod h1:OjKOY0UvVOOH5nWXSIWTbQWESn8dDiGlaEZx6IAsWhU=
|
github.com/luci/gtreap v0.0.0-20161228054646-35df89791e8f/go.mod h1:OjKOY0UvVOOH5nWXSIWTbQWESn8dDiGlaEZx6IAsWhU=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/maruel/subcommands v1.1.0 h1:5k7Y1KXDrI4U2Q7J6R7rbnpoNAiklUDTdaK2fFT998g=
|
||||||
github.com/maruel/subcommands v1.1.0/go.mod h1:b25AG9Eho2Rs1NUPAPAYBFy1B5y63QMxw/2WmLGO8m8=
|
github.com/maruel/subcommands v1.1.0/go.mod h1:b25AG9Eho2Rs1NUPAPAYBFy1B5y63QMxw/2WmLGO8m8=
|
||||||
|
github.com/maruel/ut v1.0.2 h1:mQTlQk3jubTbdTcza+hwoZQWhzcvE4L6K6RTtAFlA1k=
|
||||||
github.com/maruel/ut v1.0.2/go.mod h1:RV8PwPD9dd2KFlnlCc/DB2JVvkXmyaalfc5xvmSrRSs=
|
github.com/maruel/ut v1.0.2/go.mod h1:RV8PwPD9dd2KFlnlCc/DB2JVvkXmyaalfc5xvmSrRSs=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
@ -455,6 +457,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
|
||||||
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
|
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
|
||||||
github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
|
github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
|
||||||
github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
|
github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
// Copyright 2022 The Dawn 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.
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/buildbucket"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/gerrit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildsByName is a map of builder name to build result
|
||||||
|
type BuildsByName map[string]buildbucket.Build
|
||||||
|
|
||||||
|
func (b BuildsByName) ids() []buildbucket.BuildID {
|
||||||
|
ids := make([]buildbucket.BuildID, 0, len(b))
|
||||||
|
for _, build := range b {
|
||||||
|
ids = append(ids, build.ID)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuilds returns the builds, as declared in the config file, for the given
|
||||||
|
// patchset
|
||||||
|
func GetBuilds(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg Config,
|
||||||
|
ps gerrit.Patchset,
|
||||||
|
bb *buildbucket.Buildbucket) (BuildsByName, error) {
|
||||||
|
|
||||||
|
builds := BuildsByName{}
|
||||||
|
|
||||||
|
err := bb.SearchBuilds(ctx, ps, func(build buildbucket.Build) error {
|
||||||
|
for name, builder := range cfg.Builders {
|
||||||
|
if build.Builder == builder {
|
||||||
|
builds[name] = build
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return builds, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForBuildsToComplete waits until all the provided builds have finished.
|
||||||
|
func WaitForBuildsToComplete(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg Config,
|
||||||
|
ps gerrit.Patchset,
|
||||||
|
bb *buildbucket.Buildbucket,
|
||||||
|
builds BuildsByName) error {
|
||||||
|
|
||||||
|
buildsStillRunning := func() []string {
|
||||||
|
out := []string{}
|
||||||
|
for name, build := range builds {
|
||||||
|
if build.Status.Running() {
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Refresh build status
|
||||||
|
for name, build := range builds {
|
||||||
|
build, err := bb.QueryBuild(ctx, build.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query build for '%v': %w", name, err)
|
||||||
|
}
|
||||||
|
builds[name] = build
|
||||||
|
}
|
||||||
|
running := buildsStillRunning()
|
||||||
|
if len(running) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Println("waiting for builds to complete: ", running)
|
||||||
|
time.Sleep(time.Minute * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, build := range builds {
|
||||||
|
if build.Status == buildbucket.StatusInfraFailure ||
|
||||||
|
build.Status == buildbucket.StatusCanceled {
|
||||||
|
return fmt.Errorf("%v builder failed with %v", name, build.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrStartBuildsAndWait starts the builds as declared in the config file,
|
||||||
|
// for the given patchset, if they haven't already been started or if retest is
|
||||||
|
// true. GetOrStartBuildsAndWait then waits for the builds to complete, and then
|
||||||
|
// returns the results.
|
||||||
|
func GetOrStartBuildsAndWait(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg Config,
|
||||||
|
ps gerrit.Patchset,
|
||||||
|
bb *buildbucket.Buildbucket,
|
||||||
|
retest bool) (BuildsByName, error) {
|
||||||
|
|
||||||
|
builds := BuildsByName{}
|
||||||
|
|
||||||
|
if !retest {
|
||||||
|
// Find any existing builds for the patchset
|
||||||
|
err := bb.SearchBuilds(ctx, ps, func(build buildbucket.Build) error {
|
||||||
|
for name, builder := range cfg.Builders {
|
||||||
|
if build.Builder == builder {
|
||||||
|
builds[name] = build
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick any missing builds
|
||||||
|
for name, builder := range cfg.Builders {
|
||||||
|
if _, existing := builds[name]; !existing {
|
||||||
|
build, err := bb.StartBuild(ctx, ps, builder, retest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Printf("started build: %+v", build)
|
||||||
|
builds[name] = build
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WaitForBuildsToComplete(ctx, cfg, ps, bb, builds); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return builds, nil
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright 2022 The Dawn 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.
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/utils"
|
||||||
|
"go.chromium.org/luci/auth"
|
||||||
|
"go.chromium.org/luci/hardcoded/chromeinfra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RollSubjectPrefix is the subject prefix for CTS roll changes
|
||||||
|
RollSubjectPrefix = "Roll third_party/webgpu-cts/ "
|
||||||
|
|
||||||
|
// DefaultCacheDir is the default directory for the results cache
|
||||||
|
DefaultCacheDir = "~/.cache/webgpu-cts-results"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultAuthOptions returns the default authentication options for use by
|
||||||
|
// command line arguments.
|
||||||
|
func DefaultAuthOptions() auth.Options {
|
||||||
|
def := chromeinfra.DefaultAuthOptions()
|
||||||
|
def.SecretsDir = utils.ExpandHome("~/.config/dawn-cts")
|
||||||
|
return def
|
||||||
|
}
|
|
@ -0,0 +1,349 @@
|
||||||
|
// Copyright 2022 The Dawn 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.
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/buildbucket"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/query"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/result"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/gerrit"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/resultsdb"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/subcmd"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/utils"
|
||||||
|
"go.chromium.org/luci/auth"
|
||||||
|
rdbpb "go.chromium.org/luci/resultdb/proto/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResultSource describes the source of CTS test results.
|
||||||
|
// ResultSource is commonly used by command line flags for specifying from
|
||||||
|
// where the results should be loaded / fetched.
|
||||||
|
// If neither File or Patchset are specified, then results will be fetched from
|
||||||
|
// the last successful CTS roll.
|
||||||
|
type ResultSource struct {
|
||||||
|
// The directory used to cache results fetched from ResultDB
|
||||||
|
CacheDir string
|
||||||
|
// If specified, results will be loaded from this file path
|
||||||
|
// Must not be specified if Patchset is also specified.
|
||||||
|
File string
|
||||||
|
// If specified, results will be fetched from this gerrit patchset
|
||||||
|
// Must not be specified if File is also specified.
|
||||||
|
Patchset gerrit.Patchset
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterFlags registers the ResultSource fields as commandline flags for use
|
||||||
|
// by command line tools.
|
||||||
|
func (r *ResultSource) RegisterFlags(cfg Config) {
|
||||||
|
flag.StringVar(&r.CacheDir, "cache", DefaultCacheDir, "path to the results cache")
|
||||||
|
flag.StringVar(&r.File, "results", "", "local results.txt file (mutually exclusive with --cl)")
|
||||||
|
r.Patchset.RegisterFlags(cfg.Gerrit.Host, cfg.Gerrit.Project)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResults loads or fetches the results, based on the values of r.
|
||||||
|
func (r *ResultSource) GetResults(ctx context.Context, cfg Config, auth auth.Options) (result.List, error) {
|
||||||
|
// Check that File and Patchset weren't both specified
|
||||||
|
ps := r.Patchset
|
||||||
|
if r.File != "" && ps.Change != 0 {
|
||||||
|
fmt.Fprintln(flag.CommandLine.Output(), "only one of --results and --cl can be specified")
|
||||||
|
return nil, subcmd.ErrInvalidCLA
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a file was specified, then load that.
|
||||||
|
if r.File != "" {
|
||||||
|
return result.Load(r.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the buildbucket and resultdb clients
|
||||||
|
bb, err := buildbucket.New(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rdb, err := resultsdb.New(ctx, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no change was specified, then pull the results from the most recent
|
||||||
|
// CTS roll.
|
||||||
|
if ps.Change == 0 {
|
||||||
|
fmt.Println("no change specified, scanning gerrit for last CTS roll...")
|
||||||
|
gerrit, err := gerrit.New(cfg.Gerrit.Host, gerrit.Credentials{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
latest, err := LatestCTSRoll(gerrit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results, ps, err := MostRecentResultsForChange(ctx, cfg, r.CacheDir, gerrit, bb, rdb, latest.Number)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Printf("using results from cl %v ps %v...\n", ps.Change, ps.Patchset)
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a change, but no patchset was specified, then query the most recent
|
||||||
|
// patchset.
|
||||||
|
if ps.Patchset == 0 {
|
||||||
|
gerrit, err := gerrit.New(cfg.Gerrit.Host, gerrit.Credentials{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ps, err := gerrit.LatestPatchest(strconv.Itoa(ps.Change))
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("failed to find latest patchset of change %v: %w",
|
||||||
|
ps.Change, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the patchset's results, kicking a build if there are no results
|
||||||
|
// already available.
|
||||||
|
log.Printf("fetching results from cl %v ps %v...", ps.Change, ps.Patchset)
|
||||||
|
builds, err := GetOrStartBuildsAndWait(ctx, cfg, ps, bb, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := CacheResults(ctx, cfg, ps, r.CacheDir, rdb, builds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheResults looks in the cache at 'cacheDir' for the results for the given
|
||||||
|
// patchset. If the cache contains the results, then these are loaded and
|
||||||
|
// returned. If the cache does not contain the results, then they are fetched
|
||||||
|
// using GetResults(), saved to the cache directory and are returned.
|
||||||
|
func CacheResults(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg Config,
|
||||||
|
ps gerrit.Patchset,
|
||||||
|
cacheDir string,
|
||||||
|
rdb *resultsdb.ResultsDB,
|
||||||
|
builds BuildsByName) (result.List, error) {
|
||||||
|
|
||||||
|
var cachePath string
|
||||||
|
if cacheDir != "" {
|
||||||
|
dir := utils.ExpandHome(cacheDir)
|
||||||
|
path := filepath.Join(dir, strconv.Itoa(ps.Change), fmt.Sprintf("ps-%v.txt", ps.Patchset))
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return result.Load(path)
|
||||||
|
}
|
||||||
|
cachePath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := GetResults(ctx, cfg, rdb, builds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := result.Save(cachePath, results); err != nil {
|
||||||
|
log.Println("failed to save results to cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResults fetches the build results from ResultDB.
|
||||||
|
// GetResults does not trigger new builds.
|
||||||
|
func GetResults(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg Config,
|
||||||
|
rdb *resultsdb.ResultsDB,
|
||||||
|
builds BuildsByName) (result.List, error) {
|
||||||
|
|
||||||
|
fmt.Printf("fetching results from resultdb...")
|
||||||
|
|
||||||
|
lastPrintedDot := time.Now()
|
||||||
|
|
||||||
|
toStatus := func(s rdbpb.TestStatus) result.Status {
|
||||||
|
switch s {
|
||||||
|
default:
|
||||||
|
return result.Unknown
|
||||||
|
case rdbpb.TestStatus_PASS:
|
||||||
|
return result.Pass
|
||||||
|
case rdbpb.TestStatus_FAIL:
|
||||||
|
return result.Failure
|
||||||
|
case rdbpb.TestStatus_CRASH:
|
||||||
|
return result.Crash
|
||||||
|
case rdbpb.TestStatus_ABORT:
|
||||||
|
return result.Abort
|
||||||
|
case rdbpb.TestStatus_SKIP:
|
||||||
|
return result.Skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := result.List{}
|
||||||
|
err := rdb.QueryTestResults(ctx, builds.ids(), cfg.Test.Prefix+".*", func(rpb *rdbpb.TestResult) error {
|
||||||
|
if time.Since(lastPrintedDot) > 5*time.Second {
|
||||||
|
lastPrintedDot = time.Now()
|
||||||
|
fmt.Printf(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(rpb.GetTestId(), cfg.Test.Prefix) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
testName := rpb.GetTestId()[len(cfg.Test.Prefix):]
|
||||||
|
status := toStatus(rpb.Status)
|
||||||
|
tags := result.NewTags()
|
||||||
|
|
||||||
|
for _, sp := range rpb.Tags {
|
||||||
|
if sp.Key == "typ_tag" {
|
||||||
|
tags.Add(sp.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fr := rpb.GetFailureReason(); fr != nil {
|
||||||
|
if strings.Contains(fr.GetPrimaryErrorMessage(), "asyncio.exceptions.TimeoutError") {
|
||||||
|
status = result.Slow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := rpb.GetDuration().AsDuration()
|
||||||
|
if status == result.Pass && duration > cfg.Test.SlowThreshold {
|
||||||
|
status = result.Slow
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, result.Result{
|
||||||
|
Query: query.Parse(testName),
|
||||||
|
Status: status,
|
||||||
|
Tags: tags,
|
||||||
|
Duration: duration,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println(" done")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand any aliased tags
|
||||||
|
ExpandAliasedTags(cfg, results)
|
||||||
|
|
||||||
|
// Remove any duplicates from the results.
|
||||||
|
results = results.ReplaceDuplicates(result.Deduplicate)
|
||||||
|
|
||||||
|
results.Sort()
|
||||||
|
return results, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestCTSRoll returns for the latest merged CTS roll that landed in the past
|
||||||
|
// month. If no roll can be found, then an error is returned.
|
||||||
|
func LatestCTSRoll(g *gerrit.Gerrit) (gerrit.ChangeInfo, error) {
|
||||||
|
changes, _, err := g.QueryChanges(
|
||||||
|
`status:merged`,
|
||||||
|
`-age:1month`,
|
||||||
|
fmt.Sprintf(`message:"%v"`, RollSubjectPrefix))
|
||||||
|
if err != nil {
|
||||||
|
return gerrit.ChangeInfo{}, err
|
||||||
|
}
|
||||||
|
if len(changes) == 0 {
|
||||||
|
return gerrit.ChangeInfo{}, fmt.Errorf("no change found")
|
||||||
|
}
|
||||||
|
sort.Slice(changes, func(i, j int) bool {
|
||||||
|
return changes[i].Submitted.Time.After(changes[j].Submitted.Time)
|
||||||
|
})
|
||||||
|
return changes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestPatchset returns the most recent patchset for the given change.
|
||||||
|
func LatestPatchset(g *gerrit.Gerrit, change int) (gerrit.Patchset, error) {
|
||||||
|
ps, err := g.LatestPatchest(strconv.Itoa(change))
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("failed to find latest patchset of change %v: %w",
|
||||||
|
ps.Change, err)
|
||||||
|
return gerrit.Patchset{}, err
|
||||||
|
}
|
||||||
|
return ps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MostRecentResultsForChange returns the results from the most recent patchset
|
||||||
|
// that has build results. If no results can be found for the entire change,
|
||||||
|
// then an error is returned.
|
||||||
|
func MostRecentResultsForChange(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg Config,
|
||||||
|
cacheDir string,
|
||||||
|
g *gerrit.Gerrit,
|
||||||
|
bb *buildbucket.Buildbucket,
|
||||||
|
rdb *resultsdb.ResultsDB,
|
||||||
|
change int) (result.List, gerrit.Patchset, error) {
|
||||||
|
|
||||||
|
ps, err := LatestPatchset(g, change)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerrit.Patchset{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for ps.Patchset > 0 {
|
||||||
|
builds, err := GetBuilds(ctx, cfg, ps, bb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerrit.Patchset{}, err
|
||||||
|
}
|
||||||
|
if len(builds) > 0 {
|
||||||
|
if err := WaitForBuildsToComplete(ctx, cfg, ps, bb, builds); err != nil {
|
||||||
|
return nil, gerrit.Patchset{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := CacheResults(ctx, cfg, ps, cacheDir, rdb, builds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerrit.Patchset{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
return results, ps, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ps.Patchset--
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, gerrit.Patchset{}, fmt.Errorf("no builds found for change %v", change)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandAliasedTags modifies each result so that tags which are found in
|
||||||
|
// cfg.TagAliases are expanded to include all the tag aliases.
|
||||||
|
// This is bodge for crbug.com/dawn/1387.
|
||||||
|
func ExpandAliasedTags(cfg Config, results result.List) {
|
||||||
|
// Build the result sets
|
||||||
|
sets := make([]result.Tags, len(cfg.TagAliases))
|
||||||
|
for i, l := range cfg.TagAliases {
|
||||||
|
sets[i] = result.NewTags(l...)
|
||||||
|
}
|
||||||
|
// Expand the result tags for the aliased tag sets
|
||||||
|
for _, r := range results {
|
||||||
|
for _, set := range sets {
|
||||||
|
if r.Tags.ContainsAny(set) {
|
||||||
|
r.Tags.AddAll(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import (
|
||||||
|
|
||||||
// Register sub-commands
|
// Register sub-commands
|
||||||
_ "dawn.googlesource.com/dawn/tools/src/cmd/cts/format"
|
_ "dawn.googlesource.com/dawn/tools/src/cmd/cts/format"
|
||||||
|
_ "dawn.googlesource.com/dawn/tools/src/cmd/cts/time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
// Copyright 2022 The Dawn 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.
|
||||||
|
|
||||||
|
package time
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cmd/cts/common"
|
||||||
|
"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
|
||||||
|
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, common.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.tags, "tags", "", "comma-separated list of tags to filter results")
|
||||||
|
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 results
|
||||||
|
results, err := c.flags.source.GetResults(ctx, cfg, auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
return fmt.Errorf("no results found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If tags were provided, filter the results to those that contain these tags
|
||||||
|
if c.flags.tags != "" {
|
||||||
|
results = results.FilterByTags(result.StringToTags(c.flags.tags))
|
||||||
|
if len(results) == 0 {
|
||||||
|
return fmt.Errorf("no results after filtering by tags")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the results with longest duration first
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].Duration > results[j].Duration
|
||||||
|
})
|
||||||
|
|
||||||
|
didSomething := false
|
||||||
|
|
||||||
|
// Did the user request --top N ?
|
||||||
|
if c.flags.topN > 0 {
|
||||||
|
didSomething = true
|
||||||
|
topN := results
|
||||||
|
if c.flags.topN < len(results) {
|
||||||
|
topN = topN[:c.flags.topN]
|
||||||
|
}
|
||||||
|
for i, r := range topN {
|
||||||
|
fmt.Printf("%3.1d: %v\n", i, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did the user request --histogram ?
|
||||||
|
if c.flags.histogram {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
Loading…
Reference in New Issue