| ... |
... |
@@ -83,9 +83,18 @@ PerfStat = NamedTuple('PerfStat', [('test_env', TestEnv), |
|
83
|
83
|
('metric', MetricName),
|
|
84
|
84
|
('value', float)])
|
|
85
|
85
|
|
|
|
86
|
+# A test's metric recovered from a commit's git note: the raw sample values
|
|
|
87
|
+# recorded there, and a PerfStat whose value is their mean.
|
|
|
88
|
+class CommitMetric(NamedTuple):
|
|
|
89
|
+ perfStat: PerfStat
|
|
|
90
|
+ samples: List[float]
|
|
|
91
|
+
|
|
86
|
92
|
# A baseline recovered form stored metrics.
|
|
87
|
|
-Baseline = NamedTuple('Baseline', [('perfStat', PerfStat),
|
|
88
|
|
- ('commit', GitHash)])
|
|
|
93
|
+class Baseline(NamedTuple):
|
|
|
94
|
+ perfStat: PerfStat
|
|
|
95
|
+ commit: GitHash
|
|
|
96
|
+ # The raw samples the baseline value was averaged over.
|
|
|
97
|
+ samples: List[float] = []
|
|
89
|
98
|
|
|
90
|
99
|
# The type of exceptions which are thrown when computing the current stat value
|
|
91
|
100
|
# fails.
|
| ... |
... |
@@ -460,10 +469,10 @@ def get_allowed_changes(baseline_ref: Optional[GitRef]) -> Dict[TestName, List[A |
|
460
|
469
|
else:
|
|
461
|
470
|
return get_allowed_perf_changes()
|
|
462
|
471
|
|
|
463
|
|
-# Cache of baseline values. This is a dict of dicts indexed on:
|
|
464
|
|
-# (useCiNamespace, commit) -> (test_env, test, metric, way) -> baseline
|
|
465
|
|
-# (bool , str ) -> (str , str , str , str) -> float
|
|
466
|
|
-_commit_metric_cache = {} # type: ignore
|
|
|
472
|
+# Cache of commit metrics.
|
|
|
473
|
+_commit_metric_cache: Dict[Tuple[NoteNamespace, GitHash],
|
|
|
474
|
+ Dict[Tuple[TestEnv, TestName, MetricName, WayName],
|
|
|
475
|
+ CommitMetric]] = {}
|
|
467
|
476
|
|
|
468
|
477
|
# Get the baseline of a test at a given commit. This is the expected value
|
|
469
|
478
|
# *before* the commit is applied (i.e. on the parent commit).
|
| ... |
... |
@@ -506,7 +515,8 @@ def baseline_metric(commit: GitHash, |
|
506
|
515
|
if baseline_commit is not None:
|
|
507
|
516
|
current_metric = get_commit_metric(namespace, baseline_commit, test_env, name, metric, way)
|
|
508
|
517
|
if current_metric is not None:
|
|
509
|
|
- return Baseline(current_metric, baseline_commit)
|
|
|
518
|
+ return Baseline(current_metric.perfStat, baseline_commit,
|
|
|
519
|
+ current_metric.samples)
|
|
510
|
520
|
else:
|
|
511
|
521
|
return None
|
|
512
|
522
|
|
| ... |
... |
@@ -515,7 +525,8 @@ def baseline_metric(commit: GitHash, |
|
515
|
525
|
# Check for a metric on this commit.
|
|
516
|
526
|
current_metric = get_commit_metric(namespace, current_commit, test_env, name, metric, way)
|
|
517
|
527
|
if current_metric is not None:
|
|
518
|
|
- return Baseline(current_metric, current_commit)
|
|
|
528
|
+ return Baseline(current_metric.perfStat, current_commit,
|
|
|
529
|
+ current_metric.samples)
|
|
519
|
530
|
|
|
520
|
531
|
# Stop if there is an expected change at this commit. In that case
|
|
521
|
532
|
# metrics on ancestor commits will not be a valid baseline.
|
| ... |
... |
@@ -545,23 +556,23 @@ def get_commit_metric_value_str_or_none(gitNoteRef, |
|
545
|
556
|
result = get_commit_metric(gitNoteRef, commit, test_env, name, metric, way)
|
|
546
|
557
|
if result is None:
|
|
547
|
558
|
return None
|
|
548
|
|
- return str(result.value)
|
|
|
559
|
+ return str(result.perfStat.value)
|
|
549
|
560
|
|
|
550
|
|
-# gets the average commit metric from git notes.
|
|
|
561
|
+# gets the commit metric (average and raw samples) from git notes.
|
|
551
|
562
|
# gitNoteRef: git notes ref space e.g. "perf" or "ci/perf"
|
|
552
|
563
|
# ref: git commit
|
|
553
|
564
|
# test_env: test environment
|
|
554
|
565
|
# name: test name
|
|
555
|
566
|
# metric: test metric
|
|
556
|
567
|
# way: test way
|
|
557
|
|
-# returns: PerfStat | None if stats don't exist for the given input
|
|
|
568
|
+# returns: CommitMetric | None if stats don't exist for the given input
|
|
558
|
569
|
def get_commit_metric(gitNoteRef,
|
|
559
|
570
|
ref: Union[GitRef, GitHash],
|
|
560
|
571
|
test_env: TestEnv,
|
|
561
|
572
|
name: TestName,
|
|
562
|
573
|
metric: MetricName,
|
|
563
|
574
|
way: WayName
|
|
564
|
|
- ) -> Optional[PerfStat]:
|
|
|
575
|
+ ) -> Optional[CommitMetric]:
|
|
565
|
576
|
global _commit_metric_cache
|
|
566
|
577
|
assert test_env != None
|
|
567
|
578
|
commit = commit_hash(ref)
|
| ... |
... |
@@ -573,9 +584,9 @@ def get_commit_metric(gitNoteRef, |
|
573
|
584
|
return _commit_metric_cache[cacheKeyA].get(cacheKeyB)
|
|
574
|
585
|
|
|
575
|
586
|
# Cache miss.
|
|
576
|
|
- # Calculate baselines from the current commit's git note.
|
|
|
587
|
+ # Calculate metrics from the current commit's git note.
|
|
577
|
588
|
# Note that the git note may contain data for other tests. All tests'
|
|
578
|
|
- # baselines will be collected and cached for future use.
|
|
|
589
|
+ # metrics will be collected and cached for future use.
|
|
579
|
590
|
allCommitMetrics = get_perf_stats(ref, gitNoteRef)
|
|
580
|
591
|
|
|
581
|
592
|
# Collect recorded values by cacheKeyB.
|
| ... |
... |
@@ -586,19 +597,25 @@ def get_commit_metric(gitNoteRef, |
|
586
|
597
|
currentValues = values_by_cache_key_b.setdefault(currentCacheKey, [])
|
|
587
|
598
|
currentValues.append(float(perfStat.value))
|
|
588
|
599
|
|
|
589
|
|
- # Calculate and baseline (average of values) by cacheKeyB.
|
|
590
|
|
- baseline_by_cache_key_b = {}
|
|
|
600
|
+ # Calculate the metric (average of values, plus the values themselves)
|
|
|
601
|
+ # by cacheKeyB.
|
|
|
602
|
+ metric_by_cache_key_b = {}
|
|
591
|
603
|
for currentCacheKey, currentValues in values_by_cache_key_b.items():
|
|
592
|
|
- baseline_by_cache_key_b[currentCacheKey] = PerfStat( \
|
|
593
|
|
- currentCacheKey[0],
|
|
594
|
|
- currentCacheKey[1],
|
|
595
|
|
- currentCacheKey[3],
|
|
596
|
|
- currentCacheKey[2],
|
|
597
|
|
- sum(currentValues) / len(currentValues))
|
|
598
|
|
-
|
|
599
|
|
- # Save baselines to the cache.
|
|
600
|
|
- _commit_metric_cache[cacheKeyA] = baseline_by_cache_key_b
|
|
601
|
|
- return baseline_by_cache_key_b.get(cacheKeyB)
|
|
|
604
|
+ metric_by_cache_key_b[currentCacheKey] = CommitMetric(
|
|
|
605
|
+ PerfStat(
|
|
|
606
|
+ currentCacheKey[0],
|
|
|
607
|
+ currentCacheKey[1],
|
|
|
608
|
+ currentCacheKey[3],
|
|
|
609
|
+ currentCacheKey[2],
|
|
|
610
|
+ sum(currentValues) / len(currentValues)),
|
|
|
611
|
+ currentValues)
|
|
|
612
|
+
|
|
|
613
|
+ # Save metrics to the cache.
|
|
|
614
|
+ _commit_metric_cache[cacheKeyA] = metric_by_cache_key_b
|
|
|
615
|
+ return metric_by_cache_key_b.get(cacheKeyB)
|
|
|
616
|
+
|
|
|
617
|
+def format_samples(samples: List[float]) -> str:
|
|
|
618
|
+ return ', '.join(str(int(s)) if s == int(s) else str(s) for s in samples)
|
|
602
|
619
|
|
|
603
|
620
|
def check_stats_change(actual: PerfStat,
|
|
604
|
621
|
baseline: Baseline,
|
| ... |
... |
@@ -654,9 +671,16 @@ def check_stats_change(actual: PerfStat, |
|
654
|
671
|
' baseline @ %s' % baseline.commit
|
|
655
|
672
|
print(actual.metric, error + ':')
|
|
656
|
673
|
dev = 100.0 if expected_val == 0 else round(((float(actual.value) * 100) / int(expected_val)) - 100, 1)
|
|
|
674
|
+ # Show the samples so outliers become visible (#27602).
|
|
|
675
|
+ if len(baseline.samples) > 1:
|
|
|
676
|
+ samples_note = ('; baseline is mean of %d samples: %s'
|
|
|
677
|
+ % (len(baseline.samples),
|
|
|
678
|
+ format_samples(baseline.samples)))
|
|
|
679
|
+ else:
|
|
|
680
|
+ samples_note = ''
|
|
657
|
681
|
change_line = (f'{actual.metric} {change.value} from {baseline.perfStat.test_env} '
|
|
658
|
682
|
f'baseline @ {baseline.commit[:7]}: {expected_val} -> {actual.value} '
|
|
659
|
|
- f'({dev:+g}%, allowed {acceptance_window.describe()})')
|
|
|
683
|
+ f'({dev:+g}%, allowed {acceptance_window.describe()}{samples_note})')
|
|
660
|
684
|
result = failBecause('stat ' + change_line, tag='stat')
|
|
661
|
685
|
|
|
662
|
686
|
if not change_allowed or force_print:
|
| ... |
... |
@@ -666,6 +690,10 @@ def check_stats_change(actual: PerfStat, |
|
666
|
690
|
print(descr, str(val).rjust(length), extra)
|
|
667
|
691
|
|
|
668
|
692
|
display(' Expected ' + full_name + ' ' + actual.metric + ':', expected_val, acceptance_window.describe())
|
|
|
693
|
+ if len(baseline.samples) > 1:
|
|
|
694
|
+ display(' Samples ' + full_name + ' ' + actual.metric + ':',
|
|
|
695
|
+ len(baseline.samples),
|
|
|
696
|
+ '(' + format_samples(baseline.samples) + ')')
|
|
669
|
697
|
display(' Lower bound ' + full_name + ' ' + actual.metric + ':', lowerBound, '')
|
|
670
|
698
|
display(' Upper bound ' + full_name + ' ' + actual.metric + ':', upperBound, '')
|
|
671
|
699
|
display(' Actual ' + full_name + ' ' + actual.metric + ':', actual.value, '')
|
| ... |
... |
@@ -866,7 +894,7 @@ def main() -> None: |
|
866
|
894
|
# HEAD~2 21234 21234
|
|
867
|
895
|
# HEAD~3 20000 20000
|
|
868
|
896
|
def strMetric(x):
|
|
869
|
|
- return '{:.2f}'.format(x.value) if x != None else ""
|
|
|
897
|
+ return '{:.2f}'.format(x.perfStat.value) if x != None else ""
|
|
870
|
898
|
# Data is in column major format, so transpose and pass to print_table.
|
|
871
|
899
|
T = TypeVar('T')
|
|
872
|
900
|
def transpose(xss: List[List[T]]) -> List[List[T]]:
|