go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/query_failure_rate.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package testresults
    16  
    17  import (
    18  	"context"
    19  	"text/template"
    20  	"time"
    21  
    22  	"cloud.google.com/go/spanner"
    23  	"google.golang.org/protobuf/types/known/timestamppb"
    24  
    25  	"go.chromium.org/luci/common/sync/parallel"
    26  	"go.chromium.org/luci/server/span"
    27  
    28  	spanutil "go.chromium.org/luci/analysis/internal/span"
    29  	"go.chromium.org/luci/analysis/pbutil"
    30  	pb "go.chromium.org/luci/analysis/proto/v1"
    31  )
    32  
    33  const (
    34  	// The maximum number of workers to run in parallel.
    35  	// Given 100 is the maximum number of test variants queried at once,
    36  	// it is desirable that maxWorkers * batchSize >= 100.
    37  	maxWorkers = 10
    38  
    39  	// The size of each batch (in test variants).
    40  	batchSize = 10
    41  )
    42  
    43  // QueryFailureRateOptions specifies options for QueryFailureRate().
    44  type QueryFailureRateOptions struct {
    45  	// Project is the LUCI Project to query.
    46  	Project string
    47  	// SubRealms are the realms (of the form "ci", NOT "chromium:ci")
    48  	// within the project to query.
    49  	SubRealms []string
    50  	// TestVariants are the test variants to query.
    51  	TestVariants []*pb.TestVariantIdentifier
    52  	// AsAtTime is latest parititon time to include in the results;
    53  	// outside of testing contexts, this should be the current time.
    54  	// QueryTestVariants returns data for the 5 * 24 weekday hour
    55  	// period leading up to this time.
    56  	AsAtTime time.Time
    57  }
    58  
    59  // QueryFailureRate queries the failure rate of nominated test variants.
    60  //
    61  // Must be called in a Spanner transactional context. Context must
    62  // support multiple reads (i.e. NOT spanner.Single()) as request may
    63  // batched over multiple reads.
    64  func QueryFailureRate(ctx context.Context, opts QueryFailureRateOptions) (*pb.QueryTestVariantFailureRateResponse, error) {
    65  	batches := partitionIntoBatches(opts.TestVariants)
    66  
    67  	intervals := defineIntervals(opts.AsAtTime)
    68  
    69  	err := parallel.WorkPool(maxWorkers, func(c chan<- func() error) {
    70  		for _, b := range batches {
    71  			// Assign batch to a local variable to ensure its current
    72  			// value is captured by function closures.
    73  			batch := b
    74  			c <- func() error {
    75  				var err error
    76  				batchOpts := opts
    77  				batchOpts.TestVariants = batch.input
    78  				// queryFailureRateShard ensures test variants appear
    79  				// in the output in the same order as they appear in the
    80  				// input.
    81  				batch.output, err = queryFailureRateShard(ctx, batchOpts, intervals)
    82  				return err
    83  			}
    84  		}
    85  	})
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	// The order of test variants in the output should be the
    91  	// same as the input. Perform the inverse to what we did
    92  	// in batching.
    93  	analysis := make([]*pb.TestVariantFailureRateAnalysis, 0, len(opts.TestVariants))
    94  	for _, b := range batches {
    95  		analysis = append(analysis, b.output...)
    96  	}
    97  
    98  	response := &pb.QueryTestVariantFailureRateResponse{
    99  		Intervals:    toPBIntervals(intervals),
   100  		TestVariants: analysis,
   101  	}
   102  	return response, nil
   103  }
   104  
   105  type batch struct {
   106  	input  []*pb.TestVariantIdentifier
   107  	output []*pb.TestVariantFailureRateAnalysis
   108  }
   109  
   110  // partitionIntoBatches partitions a list of test variants into batches.
   111  func partitionIntoBatches(tvs []*pb.TestVariantIdentifier) []*batch {
   112  	var batches []*batch
   113  	batchInput := make([]*pb.TestVariantIdentifier, 0, batchSize)
   114  	for _, tv := range tvs {
   115  		if len(batchInput) >= batchSize {
   116  			batches = append(batches, &batch{
   117  				input: batchInput,
   118  			})
   119  			batchInput = make([]*pb.TestVariantIdentifier, 0, batchSize)
   120  		}
   121  		batchInput = append(batchInput, tv)
   122  	}
   123  	if len(batchInput) > 0 {
   124  		batches = append(batches, &batch{
   125  			input: batchInput,
   126  		})
   127  	}
   128  	return batches
   129  }
   130  
   131  // queryFailureRateShard reads failure rate statistics for test variants.
   132  // Must be called in a spanner transactional context.
   133  func queryFailureRateShard(ctx context.Context, opts QueryFailureRateOptions, intervals []interval) ([]*pb.TestVariantFailureRateAnalysis, error) {
   134  	type testVariant struct {
   135  		TestID      string
   136  		VariantHash string
   137  	}
   138  
   139  	tvs := make([]testVariant, 0, len(opts.TestVariants))
   140  	for _, ptv := range opts.TestVariants {
   141  		variantHash := ptv.VariantHash
   142  		if variantHash == "" {
   143  			variantHash = pbutil.VariantHash(ptv.Variant)
   144  		}
   145  
   146  		tvs = append(tvs, testVariant{
   147  			TestID:      ptv.TestId,
   148  			VariantHash: variantHash,
   149  		})
   150  	}
   151  
   152  	stmt, err := spanutil.GenerateStatement(failureRateQueryTmpl, failureRateQueryTmpl.Name(), nil)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	stmt.Params = map[string]any{
   157  		"project":             opts.Project,
   158  		"testVariants":        tvs,
   159  		"subRealms":           opts.SubRealms,
   160  		"afterPartitionTime":  queryStartTime(intervals),
   161  		"beforePartitionTime": queryEndTime(intervals),
   162  		"skip":                int64(pb.TestResultStatus_SKIP),
   163  	}
   164  
   165  	results := make([]*pb.TestVariantFailureRateAnalysis, 0, len(tvs))
   166  
   167  	index := 0
   168  	var b spanutil.Buffer
   169  	err = span.Query(ctx, stmt).Do(func(row *spanner.Row) error {
   170  		var testID, variantHash string
   171  		var intervalStats []*intervalStats
   172  		var runFlakyExamples []*verdictExample
   173  		var recentVerdicts []*recentVerdict
   174  
   175  		err := b.FromSpanner(
   176  			row,
   177  			&testID,
   178  			&variantHash,
   179  			&intervalStats,
   180  			&runFlakyExamples,
   181  			&recentVerdicts,
   182  		)
   183  		if err != nil {
   184  			return err
   185  		}
   186  
   187  		analysis := &pb.TestVariantFailureRateAnalysis{}
   188  		if testID != tvs[index].TestID || variantHash != tvs[index].VariantHash {
   189  			// This should never happen, as the SQL statement is designed
   190  			// to return results in the same order as test variants requested.
   191  			panic("results in incorrect order")
   192  		}
   193  
   194  		analysis.TestId = testID
   195  		analysis.Variant = opts.TestVariants[index].Variant
   196  		analysis.VariantHash = opts.TestVariants[index].VariantHash
   197  		analysis.IntervalStats = toPBIntervalStats(intervalStats, intervals)
   198  		analysis.RunFlakyVerdictExamples = toPBVerdictExamples(runFlakyExamples)
   199  		analysis.RecentVerdicts = toPBRecentVerdicts(recentVerdicts)
   200  		results = append(results, analysis)
   201  		index++
   202  		return nil
   203  	})
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	return results, nil
   208  }
   209  
   210  // jumpBack24WeekdayHours calculates the start time of an interval
   211  // ending at the given endTime, such that the interval includes exactly
   212  // 24 hours of weekday data (in UTC). Where there are multiple interval
   213  // start times satisfying this criteria, the latest time is selected.
   214  //
   215  // For example, if the endTime is 08:21 on Tuesday, we would pick 08:21
   216  // on Monday as the start time, as the period from start time to end time
   217  // includes 24 hours of weekday (split over Monday and Tuesday).
   218  //
   219  // If the endTime is 08:21 on Monday, we would pick 08:21 on the previous
   220  // Friday as the start time, as the period from start time to end time
   221  // includes 24 hours of weekday (split over the Friday and Monday).
   222  //
   223  // If the endTime is midnight on the morning of Tuesday, any start time from
   224  // midnight on Saturday morning to midnight on Monday morning would produce
   225  // an interval that includes 24 hours of weekday data (i.e. the 24 hours of
   226  // Monday). In this case, we pick midnight on Monday. This is the only case
   227  // that is ambiguous.
   228  //
   229  // Rationale:
   230  // Many projects see reduced testing activity on weekends, as fewer CLs are
   231  // submitted. To avoid a dip in the sample size of statistics returned
   232  // on these days (which stops exoneration), we effectively bunch weekend
   233  // data together with Friday data in one period.
   234  func jumpBack24WeekdayHours(endTime time.Time) time.Time {
   235  	endTime = endTime.In(time.UTC)
   236  	var startTime time.Time
   237  	switch endTime.Weekday() {
   238  	case time.Saturday:
   239  		// Take us back to Saturday at 0:00.
   240  		startTime = endTime.Truncate(24 * time.Hour)
   241  		// Now take us back to Friday at 0:00.
   242  		startTime = startTime.Add(-1 * 24 * time.Hour)
   243  	case time.Sunday:
   244  		// Take us back to Sunday at 0:00.
   245  		startTime = endTime.Truncate(24 * time.Hour)
   246  		// Now take us back to Friday at 0:00.
   247  		startTime = startTime.Add(-2 * 24 * time.Hour)
   248  	case time.Monday:
   249  		// Take take us back to the same time
   250  		// on the Friday.
   251  		startTime = endTime.Add(-3 * 24 * time.Hour)
   252  	default:
   253  		// Take take us back to the same time
   254  		// on the previous day (which will be a weekday).
   255  		startTime = endTime.Add(-24 * time.Hour)
   256  	}
   257  	return startTime
   258  }
   259  
   260  // interval represents a time interval of data to be returned by
   261  // QueryFailureRate.
   262  type interval struct {
   263  	// The interval start time (inclusive).
   264  	startTime time.Time
   265  	// The interval end time (exclusive).
   266  	endTime time.Time
   267  }
   268  
   269  // defineIntervals defines the time intervals that should be returned
   270  // by the QueryFailureRate query. This comprises five consecutive
   271  // 24 weekday hour intervals ending at the given asAtTime.
   272  //
   273  // The first interval is the most recent interval, which ends at asAtTime
   274  // and starts at such a time that means the interval includes 24
   275  // hours of weekday data (as measured in UTC).
   276  // The second interval ends at the start time of the first interval,
   277  // and is generated in a similar fashion, and so on for the other
   278  // three intervals.
   279  // See jumpBack24WeekdayHours for how "24 weekday hours" is defined.
   280  func defineIntervals(asAtTime time.Time) []interval {
   281  	const intervalCount = 5
   282  	result := make([]interval, 0, intervalCount)
   283  	endTime := asAtTime.In(time.UTC)
   284  	for i := 0; i < intervalCount; i++ {
   285  		startTime := jumpBack24WeekdayHours(endTime)
   286  		result = append(result, interval{
   287  			startTime: startTime,
   288  			endTime:   endTime,
   289  		})
   290  		endTime = startTime
   291  	}
   292  	return result
   293  }
   294  
   295  // queryStartTime returns the start of the partition time range that
   296  // should be queried (inclusive). Verdicts with a partition time
   297  // earlier than this time should not be included in the results.
   298  func queryStartTime(intervals []interval) time.Time {
   299  	return intervals[len(intervals)-1].startTime
   300  }
   301  
   302  // queryEndTime returns the end of the partition time range that should be
   303  // queried (exclusive). Verdicts with partition times later than (or equal to)
   304  // this time should not be included in the results.
   305  func queryEndTime(intervals []interval) time.Time {
   306  	return intervals[0].endTime
   307  }
   308  
   309  func toPBIntervals(intervals []interval) []*pb.QueryTestVariantFailureRateResponse_Interval {
   310  	result := make([]*pb.QueryTestVariantFailureRateResponse_Interval, 0, len(intervals))
   311  	for i, iv := range intervals {
   312  		result = append(result, &pb.QueryTestVariantFailureRateResponse_Interval{
   313  			IntervalAge: int32(i + 1),
   314  			StartTime:   timestamppb.New(iv.startTime),
   315  			EndTime:     timestamppb.New(iv.endTime),
   316  		})
   317  	}
   318  	return result
   319  }
   320  
   321  // intervalStats represents time interval data returned by the
   322  // QueryFailureRate query. Each interval represents 24 hours of data.
   323  type intervalStats struct {
   324  	// DaysSinceQueryStart is the time interval bucket represented by this
   325  	// row.
   326  	// Interval 0 represents the first 24 hours of the partition time range
   327  	// queried, interval 1 is the second 24 hours, and so on.
   328  	DaysSinceQueryStart int64
   329  	// TotalRunExpectedVerdicts is the number of verdicts which had only
   330  	// expected runs. An expected run is a run in which at least one test
   331  	// result was expected (excluding skips).
   332  	TotalRunExpectedVerdicts int64
   333  	// TotalRunFlakyVerdicts is the number of verdicts which had both expected
   334  	// and unexpected runs. An expected run is a run in which at least one
   335  	// test result was expected (excluding skips). An unexpected run is a run
   336  	// in which all test results are unexpected (excluding skips).
   337  	TotalRunFlakyVerdicts int64
   338  	// TotalRunExpectedVerdicts is the number of verdicts which had only
   339  	// unexpected runs. An unexpected run is a run in which
   340  	// al test results are unexpected (excluding skips).
   341  	TotalRunUnexpectedVerdicts int64
   342  }
   343  
   344  func toPBIntervalStats(stats []*intervalStats, intervals []interval) []*pb.TestVariantFailureRateAnalysis_IntervalStats {
   345  	queryStartTime := queryStartTime(intervals)
   346  	queryEndTime := queryEndTime(intervals)
   347  
   348  	results := make([]*pb.TestVariantFailureRateAnalysis_IntervalStats, 0, len(intervals))
   349  	// Ensure every interval is included in the output, even if there are
   350  	// no verdicts in it.
   351  	for i := range intervals {
   352  		results = append(results, &pb.TestVariantFailureRateAnalysis_IntervalStats{
   353  			IntervalAge: int32(i + 1),
   354  		})
   355  	}
   356  	for _, s := range stats {
   357  		// Match the interval data returned by the SQL query to the intervals
   358  		// returned by the RPC. The SQL query returns one interval per
   359  		// rolling 24 hour period, whereas the RPC returns one interval per
   360  		// rolling 24 weekday hour period (including any weekend included
   361  		// in that range). In practice, this means weekend data needs to get
   362  		// summed into the 24 weekday hour period that includes the Friday.
   363  
   364  		// Calculate the time interval represented by the interval data
   365  		// returned by the query.
   366  		intervalStartTime := queryStartTime.Add(time.Duration(s.DaysSinceQueryStart) * 24 * time.Hour)
   367  		intervalEndTime := intervalStartTime.Add(24 * time.Hour)
   368  		if queryEndTime.Before(intervalEndTime) {
   369  			intervalEndTime = queryEndTime
   370  		}
   371  
   372  		// Find the output interval that contains the query interval.
   373  		intervalIndex := -1
   374  		for i, iv := range intervals {
   375  			if !intervalEndTime.After(iv.endTime) && !intervalStartTime.Before(iv.startTime) {
   376  				intervalIndex = i
   377  				break
   378  			}
   379  		}
   380  		if intervalIndex == -1 {
   381  			// This should never happen.
   382  			panic("could not reconcile query intervals with output intervals")
   383  		}
   384  
   385  		results[intervalIndex].TotalRunExpectedVerdicts += int32(s.TotalRunExpectedVerdicts)
   386  		results[intervalIndex].TotalRunFlakyVerdicts += int32(s.TotalRunFlakyVerdicts)
   387  		results[intervalIndex].TotalRunUnexpectedVerdicts += int32(s.TotalRunUnexpectedVerdicts)
   388  	}
   389  	return results
   390  }
   391  
   392  // verdictExample is used to store an example verdict returned by
   393  // a Spanner query.
   394  type verdictExample struct {
   395  	PartitionTime        time.Time
   396  	IngestedInvocationId string
   397  	ChangelistHosts      []string
   398  	ChangelistChanges    []int64
   399  	ChangelistPatchsets  []int64
   400  	ChangelistOwnerKinds []string
   401  }
   402  
   403  func toPBVerdictExamples(ves []*verdictExample) []*pb.TestVariantFailureRateAnalysis_VerdictExample {
   404  	results := make([]*pb.TestVariantFailureRateAnalysis_VerdictExample, 0, len(ves))
   405  	for _, ve := range ves {
   406  		cls := make([]*pb.Changelist, 0, len(ve.ChangelistHosts))
   407  		// TODO(b/258734241): Expect ChangelistOwnerKinds will
   408  		// have matching length in all cases from March 2023.
   409  		if len(ve.ChangelistHosts) != len(ve.ChangelistChanges) ||
   410  			len(ve.ChangelistChanges) != len(ve.ChangelistPatchsets) ||
   411  			(ve.ChangelistOwnerKinds != nil && len(ve.ChangelistOwnerKinds) != len(ve.ChangelistHosts)) {
   412  			panic("data consistency issue: length of changelist arrays do not match")
   413  		}
   414  		for i := range ve.ChangelistHosts {
   415  			var ownerKind pb.ChangelistOwnerKind
   416  			if ve.ChangelistOwnerKinds != nil {
   417  				ownerKind = OwnerKindFromDB(ve.ChangelistOwnerKinds[i])
   418  			}
   419  			cls = append(cls, &pb.Changelist{
   420  				Host:      DecompressHost(ve.ChangelistHosts[i]),
   421  				Change:    ve.ChangelistChanges[i],
   422  				Patchset:  int32(ve.ChangelistPatchsets[i]),
   423  				OwnerKind: ownerKind,
   424  			})
   425  		}
   426  		results = append(results, &pb.TestVariantFailureRateAnalysis_VerdictExample{
   427  			PartitionTime:        timestamppb.New(ve.PartitionTime),
   428  			IngestedInvocationId: ve.IngestedInvocationId,
   429  			Changelists:          cls,
   430  		})
   431  	}
   432  	return results
   433  }
   434  
   435  // recentVerdict represents one of the most recent verdicts for the test variant.
   436  type recentVerdict struct {
   437  	verdictExample
   438  	HasUnexpectedRun bool
   439  }
   440  
   441  func toPBRecentVerdicts(verdicts []*recentVerdict) []*pb.TestVariantFailureRateAnalysis_RecentVerdict {
   442  	results := make([]*pb.TestVariantFailureRateAnalysis_RecentVerdict, 0, len(verdicts))
   443  	for _, v := range verdicts {
   444  		cls := make([]*pb.Changelist, 0, len(v.ChangelistHosts))
   445  		// TODO(b/258734241): Expect ChangelistOwnerKinds will
   446  		// have matching length in all cases from March 2023.
   447  		if len(v.ChangelistHosts) != len(v.ChangelistChanges) ||
   448  			len(v.ChangelistChanges) != len(v.ChangelistPatchsets) ||
   449  			(v.ChangelistOwnerKinds != nil && len(v.ChangelistOwnerKinds) != len(v.ChangelistHosts)) {
   450  			panic("data consistency issue: length of changelist arrays do not match")
   451  		}
   452  		for i := range v.ChangelistHosts {
   453  			var ownerKind pb.ChangelistOwnerKind
   454  			if v.ChangelistOwnerKinds != nil {
   455  				ownerKind = OwnerKindFromDB(v.ChangelistOwnerKinds[i])
   456  			}
   457  			cls = append(cls, &pb.Changelist{
   458  				Host:      DecompressHost(v.ChangelistHosts[i]),
   459  				Change:    v.ChangelistChanges[i],
   460  				Patchset:  int32(v.ChangelistPatchsets[i]),
   461  				OwnerKind: ownerKind,
   462  			})
   463  		}
   464  		results = append(results, &pb.TestVariantFailureRateAnalysis_RecentVerdict{
   465  			PartitionTime:        timestamppb.New(v.PartitionTime),
   466  			IngestedInvocationId: v.IngestedInvocationId,
   467  			Changelists:          cls,
   468  			HasUnexpectedRuns:    v.HasUnexpectedRun,
   469  		})
   470  	}
   471  	return results
   472  }
   473  
   474  var failureRateQueryTmpl = template.Must(template.New("").Parse(`
   475  WITH test_variant_verdicts AS (
   476  	SELECT
   477  		Index,
   478  		TestId,
   479  		VariantHash,
   480  		ARRAY(
   481  			-- Filter verdicts to at most one per unsubmitted changelist under
   482  			-- test. Don't filter verdicts without an unsubmitted changelist
   483  			-- under test (i.e. CI data).
   484  			SELECT
   485  				ANY_VALUE(STRUCT(
   486  				PartitionTime,
   487  				IngestedInvocationId,
   488  				HasUnexpectedRun,
   489  				HasExpectedRun,
   490  				ChangelistHosts,
   491  				ChangelistChanges,
   492  				ChangelistPatchsets,
   493  				ChangelistOwnerKinds,
   494  				AnyChangelistsByAutomation)
   495  				-- Prefer the verdict that is flaky. If both (or neither) are flaky,
   496  				-- pick the verdict with the highest partition time. If partition
   497  				-- times are also the same, pick any.
   498  				HAVING MAX IF(HasExpectedRun AND HasUnexpectedRun, TIMESTAMP_ADD(PartitionTime, INTERVAL 365 DAY), PartitionTime)) AS Verdict,
   499  			FROM (
   500  				-- Flatten test runs to test verdicts.
   501  				SELECT
   502  					PartitionTime,
   503  					IngestedInvocationId,
   504  					LOGICAL_OR(UnexpectedRun) AS HasUnexpectedRun,
   505  					LOGICAL_OR(NOT UnexpectedRun) AS HasExpectedRun,
   506  					ANY_VALUE(ChangelistHosts) AS ChangelistHosts,
   507  					ANY_VALUE(ChangelistChanges) AS ChangelistChanges,
   508  					ANY_VALUE(ChangelistPatchsets) AS ChangelistPatchsets,
   509  					ANY_VALUE(ChangelistOwnerKinds) AS ChangelistOwnerKinds,
   510  					ANY_VALUE(AnyChangelistsByAutomation) As AnyChangelistsByAutomation
   511  				FROM (
   512  					-- Flatten test results to test runs.
   513  					SELECT
   514  						PartitionTime,
   515  						IngestedInvocationId,
   516  						RunIndex,
   517  						LOGICAL_AND(COALESCE(IsUnexpected, FALSE)) AS UnexpectedRun,
   518  						ANY_VALUE(ChangelistHosts) AS ChangelistHosts,
   519  						ANY_VALUE(ChangelistChanges) AS ChangelistChanges,
   520  						ANY_VALUE(ChangelistPatchsets) AS ChangelistPatchsets,
   521  						ANY_VALUE(ChangelistOwnerKinds) AS ChangelistOwnerKinds,
   522  						'A' IN UNNEST(ANY_VALUE(ChangelistOwnerKinds)) AS AnyChangelistsByAutomation
   523  					FROM TestResults
   524  					WHERE Project = @project
   525  						AND PartitionTime >= @afterPartitionTime
   526  						AND PartitionTime < @beforePartitionTime
   527  						AND TestId = tv.TestId And VariantHash = tv.VariantHash
   528  						AND SubRealm IN UNNEST(@subRealms)
   529  						-- Exclude skipped results.
   530  						AND Status <> @skip
   531  						-- Exclude test results testing multiple CLs, as
   532  						-- we cannot ensure at most one verdict per CL for
   533  						-- them.
   534  						AND (ChangelistHosts IS NULL OR ARRAY_LENGTH(ChangelistHosts) <= 1)
   535  					GROUP BY PartitionTime, IngestedInvocationId, RunIndex
   536  				)
   537  				GROUP BY PartitionTime, IngestedInvocationId
   538  				ORDER BY PartitionTime DESC, IngestedInvocationId
   539  			)
   540  			-- Unique CL (if there is a CL under test).
   541  			GROUP BY
   542  				IF(ChangelistHosts IS NOT NULL AND ARRAY_LENGTH(ChangelistHosts) > 0, ChangelistHosts[OFFSET(0)], IngestedInvocationId),
   543  				IF(ChangelistHosts IS NOT NULL AND ARRAY_LENGTH(ChangelistHosts) > 0, ChangelistChanges[OFFSET(0)], NULL)
   544  			ORDER BY Verdict.PartitionTime DESC, Verdict.IngestedInvocationId
   545  		) AS Verdicts,
   546  	FROM UNNEST(@testVariants) tv WITH OFFSET Index
   547  )
   548  
   549  SELECT
   550  	TestId,
   551  	VariantHash,
   552  	ARRAY(
   553  		SELECT AS STRUCT
   554  			TIMESTAMP_DIFF(v.PartitionTime, @afterPartitionTime, DAY) as DaysSinceQueryStart,
   555  			COUNTIF(NOT v.HasUnexpectedRun AND v.HasExpectedRun) AS TotalRunExpectedVerdicts,
   556  			COUNTIF(v.HasUnexpectedRun AND v.HasExpectedRun) AS TotalRunFlakyVerdicts,
   557  			COUNTIF(v.HasUnexpectedRun AND NOT v.HasExpectedRun) AS TotalRunUnexpectedVerdicts
   558  		FROM UNNEST(Verdicts) v
   559  		WHERE
   560  			-- Filter out CLs authored by automation.
   561  			NOT v.AnyChangelistsByAutomation
   562  		GROUP BY DaysSinceQueryStart
   563  		ORDER BY DaysSinceQueryStart DESC
   564  	) As IntervalStats,
   565  	ARRAY(
   566  		SELECT AS STRUCT
   567  			v.PartitionTime,
   568  			v.IngestedInvocationId,
   569  			v.ChangelistHosts,
   570  			v.ChangelistChanges,
   571  			v.ChangelistPatchsets,
   572  			v.ChangelistOwnerKinds,
   573  		FROM UNNEST(Verdicts) v WITH OFFSET o
   574  		WHERE v.HasUnexpectedRun AND v.HasExpectedRun AND
   575  			-- Filter out CLs authored by automation.
   576  			NOT v.AnyChangelistsByAutomation
   577  		ORDER BY o -- Order by descending partition time.
   578  		LIMIT 10
   579  	) as RunFlakyExamples,
   580  	ARRAY(
   581  		SELECT AS STRUCT
   582  			v.PartitionTime,
   583  			v.IngestedInvocationId,
   584  			v.ChangelistHosts,
   585  			v.ChangelistChanges,
   586  			v.ChangelistPatchsets,
   587  			v.ChangelistOwnerKinds,
   588  			v.HasUnexpectedRun
   589  		FROM UNNEST(Verdicts) v WITH OFFSET o
   590  		WHERE
   591  			-- Filter out CLs authored by automation.
   592  			NOT v.AnyChangelistsByAutomation
   593  		ORDER BY o
   594  		LIMIT 10
   595  	) as RecentVerdicts,
   596  FROM test_variant_verdicts
   597  ORDER BY Index
   598  `))