Reduce runtime overhead of line-level per-test coverage#11638
Reduce runtime overhead of line-level per-test coverage#11638daniel-mohedano wants to merge 2 commits into
Conversation
|
🎯 Code Coverage (details) 🔗 Commit SHA: 826244a | Docs | Datadog PR Page | Give us feedback! |
Test Environment - netflix-zuulJob Status: 🟢 success
Baseline: median of |
Test Environment - nebula-release-pluginJob Status: 🟢 success
Baseline: median of |
Test Environment - pass4sJob Status: 🟢 success
Baseline: median of |
Test Environment - sonar-kotlinJob Status: 🟢 success
Baseline: median of |
Test Environment - jolokiaJob Status: 🟢 success
Baseline: median of |
Test Environment - okhttpJob Status: 🟢 success
Baseline: median of |
Test Environment - sbt-scalatestJob Status: 🟢 success
Baseline: median of |
Test Environment - reactive-streams-jvmJob Status: 🟢 success
Baseline: median of |
Test Environment - spring_bootJob Status: 🟢 success
Baseline: median of |
Test Environment - sonar-javaJob Status: 🟢 success
Baseline: median of |
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
…-array swap Swap Jacoco's shared probe array for a per-test array once at method entry, so Jacoco's native probes record per-test coverage with no per-probe callback. Aggregate coverage is preserved by OR-ing per-test bits back into the shared array at report time. Excludes the CoverageProbes interface (whose only bytecode is the new fallback default) from the coverage check, consistent with the rest of the coverage-infra classes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jacoco's Analyzer re-parsed each covered class once per test; cache the covered lines per (class id, probe set) so a class covered identically by many tests is analyzed only once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2ce5fdf to
826244a
Compare
Test Environment - heliboardJob Status: 🟢 success
Baseline: median of |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 826244aaef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Back-fill Jacoco's aggregate coverage (total coverage percentage and report uploads). Skip | ||
| // classes with no covered probes: the per-test array is allocated on method entry, so a | ||
| // method that throws before its first probe fires would otherwise yield an empty entry. | ||
| if (!executionDataAdapter.mergeIntoJacocoProbes()) { |
There was a problem hiding this comment.
Backfill skipped tests before suppressing reports
When a test is marked skipped after executing code (for example a JUnit assumption/abort after setup or part of the test body), TestImpl.end skips coverageStore.report(...) for TestStatus.skip at dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java:281-284. Since this new path makes report() the only caller that ORs the swapped per-test probes back into JaCoCo's shared array, those executed probes are dropped from aggregate coverage percentages and report uploads, whereas the previous per-probe instrumentation updated JaCoCo's shared array immediately.
Useful? React with 👍 / 👎.
| "resolveProbeArray", | ||
| "([ZLjava/lang/Class;J)[Z", | ||
| false); | ||
| methodVisitor.visitVarInsn(Opcodes.ASTORE, variable); |
There was a problem hiding this comment.
Avoid stranding probes after a method outlives its test
When an instrumented method is entered while a test is active but continues on a background thread past TestImpl.end, this stores the per-test array in the method-local probe slot for the rest of that invocation. Any JaCoCo probe writes that happen after coverageStore.report(...) has already merged the array will keep writing to that stale per-test array, so they are never reflected in JaCoCo's shared aggregate data used for coverage percentages and report uploads; the previous per-probe bridge re-resolved the current store at each probe and could fall back to the shared array after the test ended.
Useful? React with 👍 / 👎.
|
closing for now in favor of #11969 |
What Does This Do
Reduces the runtime overhead of line-level per-test code coverage (the JaCoCo-based
LineCoverageStorepath), with two complementary changes:Per-method probe-array swap — instead of appending a
CoveragePerTestBridge.recordCoverage(...)call after every JaCoCo probe, swap JaCoCo's shared probe array for a per-test array once at method entry (ProbeInserterInstrumentation). JaCoCo's own probe writes then record per-test coverage with zero per-probe overhead. JaCoCo's aggregate coverage (total coverage % and report uploads) is preserved by OR-ing the per-test probes back into the shared array at report time.Report-time analysis memoization —
LineCoverageStore.report()ran JaCoCo'sAnalyzer.analyzeClassonce per covered class per test, which is the dominant cost of reporting (~97% of report time, ~0.8 ms/class). Results are now memoized per(classId, probe-set)in a module-wide cache, so a class covered identically across tests is parsed once.Plus cleanup/fixes:
ClassInstrumenterInstrumentation,CoverageStore.Registry/setTotalProbeCount,recordCoverage(Class, long, int), theprobeCountsplumbing) — per-test array sizing now comes from JaCoCo's array length.Motivation
Line-level coverage is significantly more expensive than file-level, which discourages enabling the finer-grained data that keeps total-coverage figures accurate when ITR/TIA skips tests. Profiling via test-environment showed the cost is dominated by report-time class re-analysis, not the per-probe recording.
Additional Notes
Results (test-environment, median baseline → this PR).
marginal = line − fileoverhead; file-level coverage is unchanged by this PR (control), so the marginal isolates the line-specific cost:LineProbes,ExecutionDataAdapter,LineCoverageStore) plusGradleDaemonSmokeTest(real JaCoCo, fixture bitmaps) are green.Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueUse
solvesinstead, and assign the PR milestone to the issue/merge. You can also:/merge --commit-message "..."/merge -c/merge -f --reason "reason"; please use this judiciously, as some checks do not run at the PR-level (note: the PR still needs to be mergeable, this will only skip the pre-merge build)Jira ticket: SDTEST-3847