github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/shared/test_run_query.go (about)

     1  // Copyright 2019 The WPT Dashboard Project. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  //go:generate mockgen -destination sharedtest/test_run_query_mock.go -package sharedtest github.com/web-platform-tests/wpt.fyi/shared TestRunQuery
     6  
     7  package shared
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  	"sort"
    13  	"strconv"
    14  	"time"
    15  
    16  	mapset "github.com/deckarep/golang-set"
    17  )
    18  
    19  var errNoProducts = errors.New("No products specified in request to load test runs")
    20  
    21  // TestRunQuery abstracts complex queries of TestRun entities.
    22  type TestRunQuery interface {
    23  	// LoadTestRuns loads the test runs for the TestRun entities for the given parameters.
    24  	// It is encapsulated because we cannot run single queries with multiple inequality
    25  	// filters, so must load the keys and merge the results.
    26  	LoadTestRuns(
    27  		products []ProductSpec,
    28  		labels mapset.Set,
    29  		revisions []string,
    30  		from *time.Time,
    31  		to *time.Time,
    32  		limit,
    33  		offset *int) (result TestRunsByProduct, err error)
    34  
    35  	// LoadTestRunKeys loads the keys for the TestRun entities for the given parameters.
    36  	// It is encapsulated because we cannot run single queries with multiple inequality
    37  	// filters, so must load the keys and merge the results.
    38  	LoadTestRunKeys(
    39  		products []ProductSpec,
    40  		labels mapset.Set,
    41  		revisions []string,
    42  		from *time.Time,
    43  		to *time.Time,
    44  		limit *int,
    45  		offset *int) (result KeysByProduct, err error)
    46  
    47  	// LoadTestRunsByKeys loads test runs by keys and sets their IDs.
    48  	LoadTestRunsByKeys(KeysByProduct) (result TestRunsByProduct, err error)
    49  
    50  	// GetAlignedRunSHAs returns an array of the SHA[0:10] for runs that
    51  	// exists for all the given products, ordered by most-recent, as well as a map
    52  	// of those SHAs to a KeysByProduct map of products to the TestRun keys, for the
    53  	// runs in the aligned run.
    54  	GetAlignedRunSHAs(
    55  		products ProductSpecs,
    56  		labels mapset.Set,
    57  		from,
    58  		to *time.Time,
    59  		limit *int,
    60  		offset *int) (shas []string, keys map[string]KeysByProduct, err error)
    61  }
    62  
    63  type testRunQueryImpl struct {
    64  	store Datastore
    65  }
    66  
    67  // NewTestRunQuery creates a concrete TestRunQuery backed by a Datastore interface.
    68  func NewTestRunQuery(store Datastore) TestRunQuery {
    69  	return testRunQueryImpl{store}
    70  }
    71  
    72  func (t testRunQueryImpl) LoadTestRuns(
    73  	products []ProductSpec,
    74  	labels mapset.Set,
    75  	revisions []string,
    76  	from *time.Time,
    77  	to *time.Time,
    78  	limit,
    79  	offset *int) (result TestRunsByProduct, err error) {
    80  	if len(products) == 0 {
    81  		return nil, errNoProducts
    82  	}
    83  
    84  	keys, err := t.LoadTestRunKeys(products, labels, revisions, from, to, limit, offset)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	return t.LoadTestRunsByKeys(keys)
    89  }
    90  
    91  func (t testRunQueryImpl) LoadTestRunsByKeys(keysByProduct KeysByProduct) (result TestRunsByProduct, err error) {
    92  	result = TestRunsByProduct{}
    93  	for _, kbp := range keysByProduct {
    94  		runs := make(TestRuns, len(kbp.Keys))
    95  		if err := t.store.GetMulti(kbp.Keys, runs); err != nil {
    96  			return nil, err
    97  		}
    98  		result = append(result, ProductTestRuns{
    99  			Product:  kbp.Product,
   100  			TestRuns: runs,
   101  		})
   102  	}
   103  
   104  	// Append the keys as ID
   105  	for i, kbp := range keysByProduct {
   106  		result[i].TestRuns.SetTestRunIDs(GetTestRunIDs(kbp.Keys))
   107  	}
   108  	return result, err
   109  }
   110  
   111  func (t testRunQueryImpl) LoadTestRunKeys(
   112  	products []ProductSpec,
   113  	labels mapset.Set,
   114  	revisions []string,
   115  	from *time.Time,
   116  	to *time.Time,
   117  	limit *int,
   118  	offset *int) (result KeysByProduct, err error) {
   119  	log := GetLogger(t.store.Context())
   120  	result = make(KeysByProduct, len(products))
   121  	baseQuery := t.store.NewQuery("TestRun")
   122  	if offset != nil {
   123  		baseQuery = baseQuery.Offset(*offset)
   124  	}
   125  	if labels != nil {
   126  		labels.Remove("") // Ensure the empty string isn't present.
   127  		for i := range labels.Iter() {
   128  			baseQuery = baseQuery.Filter("Labels =", i.(string))
   129  		}
   130  	}
   131  	var globalIDFilter mapset.Set
   132  	if len(revisions) > 1 || len(revisions) == 1 && !IsLatest(revisions[0]) {
   133  		globalIDFilter = mapset.NewSet()
   134  		for _, sha := range revisions {
   135  			var ids mapset.Set
   136  			if ids, err = loadIDsForRevision(t.store, baseQuery, sha); err != nil {
   137  				return nil, err
   138  			}
   139  			globalIDFilter = globalIDFilter.Union(ids)
   140  		}
   141  		log.Debugf("Found %d keys across %d revisions", globalIDFilter.Cardinality(), len(revisions))
   142  	}
   143  
   144  	for i, product := range products {
   145  		var productIDFilter = merge(globalIDFilter, nil)
   146  		query := baseQuery.Filter("BrowserName =", product.BrowserName)
   147  		if product.Labels != nil {
   148  			for i := range product.Labels.Iter() {
   149  				query = query.Filter("Labels =", i.(string))
   150  			}
   151  		}
   152  		if !IsLatest(product.Revision) {
   153  			var revIDFilter mapset.Set
   154  			if revIDFilter, err = loadIDsForRevision(t.store, query, product.Revision); err != nil {
   155  				return nil, err
   156  			}
   157  			log.Debugf("Found %v keys for %s@%s", revIDFilter.Cardinality(), product.BrowserName, product.Revision)
   158  			productIDFilter = merge(productIDFilter, revIDFilter)
   159  		}
   160  		if product.BrowserVersion != "" {
   161  			var versionIDs mapset.Set
   162  			if versionIDs, err = loadIDsForBrowserVersion(t.store, query, product.BrowserVersion); err != nil {
   163  				return nil, err
   164  			}
   165  			log.Debugf("Found %v keys for %s", versionIDs.Cardinality(), product.BrowserVersion)
   166  			productIDFilter = merge(productIDFilter, versionIDs)
   167  		}
   168  
   169  		// If we have a specific set of possibilities, it's much cheaper to
   170  		// turn the query on its head (filter the entities).
   171  		var keys []Key
   172  		if productIDFilter != nil {
   173  			keys, err = clientSideFilter(t.store, product, productIDFilter, from, to, limit)
   174  			if err != nil {
   175  				return nil, err
   176  			}
   177  		} else {
   178  			// Otherwise, just run a "GetAll" filter. Expensive.
   179  			log.Debugf("Falling back to GetAll datastore query.")
   180  			// TODO(lukebjerring): Indexes + filtering for OS + version.
   181  			query = query.Order("-TimeStart")
   182  			if from != nil {
   183  				query = query.Filter("TimeStart >=", *from)
   184  			}
   185  			if to != nil {
   186  				query = query.Filter("TimeStart <", *to)
   187  			}
   188  			max := MaxCountMaxValue
   189  			if limit != nil && *limit < MaxCountMaxValue {
   190  				max = *limit
   191  			}
   192  			keys, err = t.store.GetAll(query.KeysOnly().Limit(max), nil)
   193  			if err != nil {
   194  				return nil, err
   195  			}
   196  			log.Debugf("Loaded %v results for %s", len(keys), product.String())
   197  		}
   198  
   199  		log.Debugf("Found %v results for %s", len(keys), product.String())
   200  		result[i] = ProductTestRunKeys{
   201  			Product: product,
   202  			Keys:    keys,
   203  		}
   204  	}
   205  	return result, nil
   206  }
   207  
   208  func clientSideFilter(
   209  	store Datastore,
   210  	product ProductSpec,
   211  	productIDFilter mapset.Set,
   212  	from,
   213  	to *time.Time,
   214  	limit *int) (keys []Key, err error) {
   215  	log := GetLogger(store.Context())
   216  	capacity := productIDFilter.Cardinality()
   217  	if productIDFilter.Cardinality() > MaxKeysPerLookup {
   218  		log.Warningf("%d viable runs exceed the lookup limit %d", productIDFilter.Cardinality(), MaxKeysPerLookup)
   219  		capacity = MaxKeysPerLookup
   220  	}
   221  	log.Debugf("Loading %d viable runs to filter.", capacity)
   222  	keys = make([]Key, 0, capacity)
   223  	for key := range productIDFilter.Iter() {
   224  		keys = append(keys, store.NewIDKey("TestRun", key.(int64)))
   225  		if len(keys) == capacity {
   226  			// FIXME: This might produce incomplete results.
   227  			// https://github.com/web-platform-tests/wpt.fyi/pull/1914
   228  			break
   229  		}
   230  	}
   231  	runs := make(TestRuns, len(keys))
   232  	err = store.GetMulti(keys, runs)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	runs.SetTestRunIDs(GetTestRunIDs(keys))
   237  	// TestRuns sorted by TimeStart asc by default
   238  	sort.Sort(sort.Reverse(runs))
   239  	keys = make([]Key, 0)
   240  	for _, run := range runs {
   241  		if !product.Matches(run) ||
   242  			from != nil && !from.Before(run.TimeStart) ||
   243  			to != nil && !run.TimeStart.Before(*to) {
   244  			continue
   245  		}
   246  		keys = append(keys, store.NewIDKey("TestRun", run.ID))
   247  	}
   248  	if limit != nil && len(keys) >= *limit {
   249  		keys = keys[:*limit]
   250  	} else if len(keys) >= MaxCountMaxValue {
   251  		keys = keys[:MaxCountMaxValue]
   252  	}
   253  	return keys, nil
   254  }
   255  
   256  func (t testRunQueryImpl) GetAlignedRunSHAs(
   257  	products ProductSpecs,
   258  	labels mapset.Set,
   259  	from,
   260  	to *time.Time,
   261  	limit *int,
   262  	offset *int) (shas []string, keys map[string]KeysByProduct, err error) {
   263  	if limit == nil {
   264  		maxMax := MaxCountMaxValue
   265  		limit = &maxMax
   266  	}
   267  	query := t.store.
   268  		NewQuery("TestRun").
   269  		Order("-TimeStart")
   270  
   271  	if labels != nil {
   272  		for i := range labels.Iter() {
   273  			query = query.Filter("Labels =", i.(string))
   274  		}
   275  	}
   276  	if from != nil {
   277  		query = query.Filter("TimeStart >=", *from)
   278  	}
   279  	if to != nil {
   280  		query = query.Filter("TimeStart <", *to)
   281  	}
   282  
   283  	productsBySHA := make(map[string]mapset.Set)
   284  	keyCollector := make(map[string]KeysByProduct)
   285  	keys = make(map[string]KeysByProduct)
   286  	done := mapset.NewSet()
   287  	it := query.Run(t.store)
   288  	for {
   289  		var testRun TestRun
   290  		var key Key
   291  		matchingProduct := -1
   292  		key, err := it.Next(&testRun)
   293  		if err == t.store.Done() {
   294  			break
   295  		} else if err != nil {
   296  			return nil, nil, err
   297  		} else {
   298  			for i := range products {
   299  				if products[i].Matches(testRun) {
   300  					matchingProduct = i
   301  					break
   302  				}
   303  			}
   304  		}
   305  		if matchingProduct < 0 {
   306  			continue
   307  		}
   308  		if _, ok := productsBySHA[testRun.Revision]; !ok {
   309  			productsBySHA[testRun.Revision] = mapset.NewSet()
   310  			keyCollector[testRun.Revision] = make(KeysByProduct, len(products))
   311  		}
   312  		set := productsBySHA[testRun.Revision]
   313  		if set.Contains(matchingProduct) {
   314  			continue
   315  		}
   316  		set.Add(matchingProduct)
   317  		keyCollector[testRun.Revision][matchingProduct].Keys = []Key{key}
   318  		if set.Cardinality() == len(products) && !done.Contains(testRun.Revision) {
   319  			if offset == nil || done.Cardinality() >= *offset {
   320  				shas = append(shas, testRun.Revision)
   321  			}
   322  			done.Add(testRun.Revision)
   323  			keys[testRun.Revision] = keyCollector[testRun.Revision]
   324  			if len(shas) >= *limit {
   325  				return shas, keys, nil
   326  			}
   327  		}
   328  	}
   329  	return shas, keys, err
   330  }
   331  
   332  // merge gives the set of elements present in both of the given sets (Intersect).
   333  // If one of the sets is nil, returns a set with the contents of the non-nil set.
   334  // If both sets are nil, returns nil.
   335  func merge(s1, s2 mapset.Set) mapset.Set {
   336  	if s1 == nil && s2 == nil {
   337  		return nil
   338  	} else if s1 == nil {
   339  		return merge(s2, nil)
   340  	} else if s2 == nil {
   341  		return mapset.NewSetWith(s1.ToSlice()...)
   342  	}
   343  	return s1.Intersect(s2)
   344  }
   345  
   346  func contains(s []string, x string) bool {
   347  	for _, v := range s {
   348  		if v == x {
   349  			return true
   350  		}
   351  	}
   352  	return false
   353  }
   354  
   355  // Loads any keys for a revision prefix or full string match
   356  func loadIDsForRevision(store Datastore, query Query, sha string) (result mapset.Set, err error) {
   357  	log := GetLogger(store.Context())
   358  	var revQuery Query
   359  	if len(sha) < 40 {
   360  		log.Debugf("Finding revisions %s <= SHA < %s", sha, sha+"g")
   361  		revQuery = query.
   362  			Order("FullRevisionHash").
   363  			Limit(MaxCountMaxValue).
   364  			Filter("FullRevisionHash >=", sha).
   365  			Filter("FullRevisionHash <", sha+"g") // g > f
   366  	} else {
   367  		log.Debugf("Finding exact revision %s", sha)
   368  		revQuery = query.Filter("FullRevisionHash =", sha[:40])
   369  	}
   370  
   371  	var keys []Key
   372  	if keys, err = store.GetAll(revQuery.KeysOnly(), nil); err != nil {
   373  		return nil, err
   374  	}
   375  	result = mapset.NewSet()
   376  	for _, id := range GetTestRunIDs(keys) {
   377  		result.Add(id)
   378  	}
   379  	return result, nil
   380  }
   381  
   382  // Loads any keys for a full string match or a version prefix (Between [version].* and [version].9*).
   383  // Entries in the set are the int64 value of the keys.
   384  func loadIDsForBrowserVersion(store Datastore, query Query, version string) (result mapset.Set, err error) {
   385  	result = mapset.NewSet()
   386  	// By prefix
   387  	var keys []Key
   388  	versionQuery := VersionPrefix(query, "BrowserVersion", version, true)
   389  	if keys, err = store.GetAll(versionQuery.KeysOnly(), nil); err != nil {
   390  		return nil, err
   391  	}
   392  	for _, id := range GetTestRunIDs(keys) {
   393  		result.Add(id)
   394  	}
   395  	// By exact match
   396  	if keys, err = store.GetAll(query.Filter("BrowserVersion =", version).KeysOnly(), nil); err != nil {
   397  		return nil, err
   398  	}
   399  	for _, id := range GetTestRunIDs(keys) {
   400  		result.Add(id)
   401  	}
   402  	return result, nil
   403  }
   404  
   405  // VersionPrefix returns the given query with a prefix filter on the given
   406  // field name, using the >= and < filters.
   407  func VersionPrefix(query Query, fieldName, versionPrefix string, desc bool) Query {
   408  	order := fieldName
   409  	if desc {
   410  		order = "-" + order
   411  	}
   412  	return query.
   413  		Limit(MaxCountMaxValue).
   414  		Order(order).
   415  		Filter(fieldName+" >=", fmt.Sprintf("%s.", versionPrefix)).
   416  		Filter(fieldName+" <=", fmt.Sprintf("%s.%c", versionPrefix, '9'+1))
   417  }
   418  
   419  func getTestRunRedisKey(id int64) string {
   420  	return "TEST_RUN-" + strconv.FormatInt(id, 10)
   421  }