Skip to content

Probe consteval coverage in batched rounds across all headers - #386

Merged
philipcraig merged 3 commits into
mainfrom
consteval-coverage-speed-374
Sep 13, 2026
Merged

philipcraig merged 3 commits into
mainfrom
consteval-coverage-speed-374

Conversation

@philipcraig

@philipcraig philipcraig commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #374.

Replaces one compile per probe point with batched rounds, and instruments every library header instead of just protocol.hh.

Timings

The CI probe step for the same 177 probe points, single runner unless stated:

Run Probe points Probe step
main (protocol.hh only) 83 10.1 min
#369 unsharded 177 36.3 min
#369 four shards 44 each 9.3–11.8 min per shard, 4× runner minutes
this PR 177 3.1 min

The whole job now takes 5 minutes, and timeout-minutes drops from 90 to 30.

Locally, against the one-compile-per-probe script from #369 with name_mangling_tests.cc added:

Script Cores Wall Rounds
#369, one compile per probe 24 419 s
this PR 4 94 s 23
this PR, contiguous instead of interleaved batches 4 155 s 42

Both scripts report 150/177 probe points covered, with identical per-header LCOV records.

How

  • Traps are keyed on (header index, __LINE__), so all headers are instrumented in one work directory and one -include file arms any subset of them.
  • Each round arms every probe point not yet covered and compiles a translation unit once per batch. GCC reports every independent constant evaluation that throws, so one compile finds many points; a point masked by an earlier trap on its path surfaces in the next round once that trap is disarmed. Rounds stop when one finds nothing new (from jbcoe's prototype on the closed Instrument all consteval headers in coverage script #369 branch).
  • Batches are interleaved rather than contiguous, so neighbouring probe points, which usually share an evaluation path, do not mask each other.
  • Translation units are probed cheapest first (name_mangling_tests.cc is now included), so protocol_test.cc only ever arms what the cheaper ones left uncovered.
  • Unarmed traps are if constexpr no-ops, so they cost the constant evaluator nothing.
  • A compile that fails with no trap fired is reported as an instrumenter bug, which replaces the separate unarmed verify pass.
  • The CI job is back to a single runner.

Key each trap on (header index, __LINE__) so every library header is
instrumented in one work directory, arm every uncovered probe point at
once and let GCC report each independent evaluation that throws,
repeating with the found points disarmed until a round finds nothing.
Batches are interleaved so neighbouring points do not mask each other,
and translation units are probed cheapest first.

The CI job returns to a single runner.

Closes #374.
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.57%. Comparing base (55268e1) to head (cef9446).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #386      +/-   ##
==========================================
+ Coverage   72.79%   74.57%   +1.78%     
==========================================
  Files          12       15       +3     
  Lines         838     1003     +165     
  Branches      221      221              
==========================================
+ Hits          610      748     +138     
- Misses         22       49      +27     
  Partials      206      206              
Flag Coverage Δ
consteval 84.74% <ø> (-15.26%) ⬇️
runtime 72.39% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@philipcraig
philipcraig marked this pull request as ready for review September 13, 2026 18:55
@philipcraig
philipcraig requested a review from jbcoe as a code owner September 13, 2026 18:55
Comment thread scripts/consteval_coverage.py Outdated
Library headers use the .hh suffix; the coverage script no longer needs
a special case for it.
@philipcraig
philipcraig requested a review from jbcoe September 13, 2026 19:08
@philipcraig
philipcraig enabled auto-merge (squash) September 13, 2026 19:09
@jbcoe

jbcoe commented Sep 13, 2026

Copy link
Copy Markdown
Owner

probe_key() calls INSTRUMENTED_HEADERS.index(probe_point.header) on every call. Storing the index on ProbePoint instead of just the header name would remove the repeated linear scan.

@jbcoe

jbcoe commented Sep 13, 2026

Copy link
Copy Markdown
Owner

I'm struggling to decipher why arming multiple traps at once is valid. The speedup is impressive and makes sense but it's unclear to me (from reading code and comments) why we can arm multiple traps at the same time.

ProbePoint carries its trap key instead of looking the header up in
INSTRUMENTED_HEADERS on every call, and probe_translation_unit's
docstring says why arming many traps in one compile is sound.
@philipcraig

Copy link
Copy Markdown
Collaborator Author

Two properties make it sound.

No false positives. A trap only throws when the evaluator executes it, and the diagnostic names the trap that threw. So every trap_hit<H, L> reported means line L of header H was evaluated, however many other traps were armed. Unarmed traps are if constexpr no-ops, so arming a set changes nothing but where evaluations abort.

No permanent false negatives. A throw aborts only its own top-level constant evaluation; GCC carries on with the rest of the translation unit and reports each failed evaluation separately. Within one evaluation, only the first armed trap on its path reports, and any armed trap after it is masked. The next round disarms everything found so far, so the evaluation now runs past that point and reports the next armed trap on its path. Any trap the test suite evaluates is preceded on its path only by traps the suite also evaluates, so by induction it is reported once those are disarmed. The armed set shrinks every productive round, so this terminates, and a round with no new hits means no remaining armed trap is on any evaluation path.

Cascading errors from an aborted evaluation (incomplete class, missing member) are not trap diagnostics, so they are ignored, and the evaluations they suppressed run in a later round once the trap is gone. A compile that fails with no trap firing raises instead.

Small illustration, three consteval functions with traps <0,10>, <1,20> then <1,21>, <1,21>, all armed: GCC reports <1,20> for the second function (its <1,21> is masked), <1,21> for the third, and <0,10> once per instantiation that calls the first. Disarming those three would surface the masked <1,21> in round two.

The one-compile-per-probe script and this one give the same 150/177, identical per header, which is the empirical check. cef9446 moves this explanation into the probe_translation_unit docstring, since that is where you looked for it.

@philipcraig

Copy link
Copy Markdown
Collaborator Author

ProbePoint now carries header_index and a key property, and probe_key() is gone: cef9446.

@jbcoe jbcoe left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this - a significant improvement.

@philipcraig
philipcraig merged commit 77c7595 into main Sep 13, 2026
21 checks passed
@philipcraig
philipcraig deleted the consteval-coverage-speed-374 branch September 13, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cut the consteval coverage job's serial and per-probe costs

2 participants