Simon Jakobi pushed to branch wip/sjakobi/T27602 at Glasgow Haskell Compiler / GHC

Commits:

3 changed files:

Changes:

  • .gitlab/ci.sh
    ... ... @@ -1120,9 +1120,10 @@ case ${1:-help} in
    1120 1120
       setup) setup && cleanup_submodules ;;
    
    1121 1121
       configure) time_it "configure" configure ;;
    
    1122 1122
       build_hadrian) time_it "build" build_hadrian ;;
    
    1123
    -  # N.B. Always push notes, even if the build fails. This is okay to do as the
    
    1124
    -  # testsuite driver doesn't record notes for tests that fail due to
    
    1125
    -  # correctness.
    
    1123
    +  # N.B. Always push notes, even if the build fails. Metrics from runs failing
    
    1124
    +  # a perf stat check are deliberately recorded too — discarding them would
    
    1125
    +  # bias the baseline towards whichever sample came first. Only correctness
    
    1126
    +  # failures record nothing.
    
    1126 1127
       test_hadrian)
    
    1127 1128
         fetch_perf_notes
    
    1128 1129
         res=0
    

  • testsuite/driver/perf_notes.py
    ... ... @@ -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,28 @@ 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))
    
    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)
    
    598 616
     
    
    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)
    
    617
    +def format_sample(s: float) -> str:
    
    618
    +    return str(int(s)) if s == int(s) else str(s)
    
    619
    +
    
    620
    +def format_samples(samples: List[float]) -> str:
    
    621
    +    return ', '.join(format_sample(s) for s in samples)
    
    602 622
     
    
    603 623
     def check_stats_change(actual: PerfStat,
    
    604 624
                            baseline: Baseline,
    
    ... ... @@ -654,9 +674,17 @@ def check_stats_change(actual: PerfStat,
    654 674
                     ' baseline @ %s' % baseline.commit
    
    655 675
             print(actual.metric, error + ':')
    
    656 676
             dev = 100.0 if expected_val == 0 else round(((float(actual.value) * 100) / int(expected_val)) - 100, 1)
    
    677
    +        # Show the sample spread so unreliable baselines become visible (#27602).
    
    678
    +        if len(baseline.samples) > 1:
    
    679
    +            samples_note = ('; baseline is mean of %d samples spanning %s..%s'
    
    680
    +                            % (len(baseline.samples),
    
    681
    +                               format_sample(min(baseline.samples)),
    
    682
    +                               format_sample(max(baseline.samples))))
    
    683
    +        else:
    
    684
    +            samples_note = ''
    
    657 685
             change_line = (f'{actual.metric} {change.value} from {baseline.perfStat.test_env} '
    
    658 686
                            f'baseline @ {baseline.commit[:7]}: {expected_val} -> {actual.value} '
    
    659
    -                       f'({dev:+g}%, allowed {acceptance_window.describe()})')
    
    687
    +                       f'({dev:+g}%, allowed {acceptance_window.describe()}{samples_note})')
    
    660 688
             result = failBecause('stat ' + change_line, tag='stat')
    
    661 689
     
    
    662 690
         if not change_allowed or force_print:
    
    ... ... @@ -666,6 +694,10 @@ def check_stats_change(actual: PerfStat,
    666 694
                 print(descr, str(val).rjust(length), extra)
    
    667 695
     
    
    668 696
             display('    Expected    ' + full_name + ' ' + actual.metric + ':', expected_val, acceptance_window.describe())
    
    697
    +        if len(baseline.samples) > 1:
    
    698
    +            display('    Samples     ' + full_name + ' ' + actual.metric + ':',
    
    699
    +                    len(baseline.samples),
    
    700
    +                    '(' + format_samples(baseline.samples) + ')')
    
    669 701
             display('    Lower bound ' + full_name + ' ' + actual.metric + ':', lowerBound, '')
    
    670 702
             display('    Upper bound ' + full_name + ' ' + actual.metric + ':', upperBound, '')
    
    671 703
             display('    Actual      ' + full_name + ' ' + actual.metric + ':', actual.value, '')
    
    ... ... @@ -866,7 +898,7 @@ def main() -> None:
    866 898
         # HEAD~2           21234                 21234
    
    867 899
         # HEAD~3           20000                 20000
    
    868 900
         def strMetric(x):
    
    869
    -        return '{:.2f}'.format(x.value) if x != None else ""
    
    901
    +        return '{:.2f}'.format(x.perfStat.value) if x != None else ""
    
    870 902
         # Data is in column major format, so transpose and pass to print_table.
    
    871 903
         T = TypeVar('T')
    
    872 904
         def transpose(xss: List[List[T]]) -> List[List[T]]:
    

  • testsuite/tests/perf/compiler/all.T
    ... ... @@ -48,7 +48,9 @@ test('T4801',
    48 48
          [''])
    
    49 49
     
    
    50 50
     test('T3064',
    
    51
    -     [collect_compiler_residency(20),
    
    51
    +     [# Demo-only: tightened from 20 so the bimodal max_bytes_used
    
    52
    +      # baseline on aarch64-linux-deb13-validate fails.
    
    53
    +      collect_compiler_residency(1),
    
    52 54
           collect_compiler_runtime(2),
    
    53 55
           only_ways(['normal']),
    
    54 56
           ],