Skip to content

perf(cmake): analyse a package's translation units in parallel under clang-tidy - #588

Merged
bburda merged 2 commits into
mainfrom
perf/clang-tidy-parallel-jobs
Aug 5, 2026
Merged

perf(cmake): analyse a package's translation units in parallel under clang-tidy#588
bburda merged 2 commits into
mainfrom
perf/clang-tidy-parallel-jobs

Conversation

@bburda

@bburda bburda commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

The full clang-tidy gate analysed one translation unit at a time regardless of how many cores the machine had.

ament_clang_tidy defaults to --jobs 1, and ros2_medkit_clang_tidy() passed neither a JOBS argument nor the ament_cmake_clang_tidy_JOBS variable the upstream macro also honours. CTest-level parallelism did not compensate, because each package registers a single clang_tidy test, so -j only overlaps packages while the largest one sets the wall clock on its own.

This passes a job count through the wrapper, defaulting to the host core count, and runs the tidy preset with one CTest job. The parallelism now lives inside each package test; keeping both levels would multiply peak memory by the number of packages, and a clang-tidy process on this codebase holds roughly half a gigabyte.

Two paths are deliberately unchanged:

  • CI does not use this code path. quality.yml drives run-clang-tidy with clang-tidy-cache, and analyses only changed files on pull requests.
  • scripts/clang-tidy-diff.sh loops serially, but pre-commit already invokes it with several file batches at once, so it is not the bottleneck.

Issue


Type

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Build tooling / developer ergonomics.


Testing

Measured on the ros2_medkit_gateway package (244 entries in its compile database), 16-core machine, same worktree and same build for both runs, colcon test --ctest-args -j 1 -R '^clang_tidy$':

ROS2_MEDKIT_CLANG_TIDY_JOBS Wall clock
1 (previous behaviour) 2125 s
16 (new default on this host) 261 s

Both runs report the same single pre-existing finding, so the change alters scheduling only, not what gets analysed.

To verify: configure with -DENABLE_CLANG_TIDY=ON and check the registered command in build/<pkg>/CTestTestfile.cmake - it now ends in --jobs <n>. -DROS2_MEDKIT_CLANG_TIDY_JOBS=1 restores the old behaviour, and ros2_medkit_clang_tidy(JOBS <n>) overrides it for one package.

The serialisation is already being worked around in the tree. ament_clang_tidy defaults to a 300 s test timeout, and ros2_medkit_gateway registers its check with TIMEOUT 3000 - a tenfold raise. ros2_medkit_fault_manager takes the default and, measured on the same 16-core machine, its serial run exceeds it and the test fails by timeout; with the job count passed through it finishes in roughly a minute. So this is not only a wall-clock cost: on a machine with cores to spare, a package that never raised its timeout has a red gate for no reason other than scheduling.

The TIMEOUT 3000 on the test is left as is - it was close to being reached by the serial run and is now comfortably clear.


Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed)
  • Tests were added or updated if needed
  • Docs were updated if behavior or public API changed

No behaviour change for callers; the ros2_medkit_cmake README gained a section describing the module, the new knob, and why the two levels of parallelism must not both be enabled.

…clang-tidy

ament_clang_tidy defaults to --jobs 1 and the wrapper passed neither JOBS nor
ament_cmake_clang_tidy_JOBS, so the full gate walked one translation unit at a
time while the rest of the machine idled. CTest parallelism did not compensate:
each package registers a single clang_tidy test, so -j only overlaps packages
and the largest one still set the wall clock.

Pass a job count through the wrapper, defaulting to the host core count, and run
the tidy preset with one CTest job. The parallelism now lives inside each package
test; keeping both levels would multiply peak memory by the number of packages,
and a clang-tidy process on this codebase holds roughly half a gigabyte.

Closes #586
Copilot AI lite review requested due to automatic review settings August 4, 2026 17:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

The updated README inaccurately states this clang-tidy CTest path is “on in CI”, but CI’s quality.yml clang-tidy job configures with -DENABLE_CLANG_TIDY=OFF and uses run-clang-tidy instead.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Improves the developer-local “full clang-tidy gate” by enabling per-package parallel analysis (instead of one translation unit at a time), while avoiding unsafe memory amplification from also running multiple packages’ clang-tidy tests concurrently.

Changes:

  • Add ROS2_MEDKIT_CLANG_TIDY_JOBS (default: host core count) and a per-package JOBS override to ros2_medkit_clang_tidy(), forwarding the value to ament_clang_tidy.
  • Update scripts/test.sh tidy to run the clang_tidy CTest target with -j 1 so parallelism lives inside each package’s test.
  • Document the new knobs and the intended parallelism model in ros2_medkit_cmake README.
File summaries
File Description
src/ros2_medkit_cmake/README.md Documents the clang-tidy CTest target behavior and new per-package job configuration.
src/ros2_medkit_cmake/cmake/ROS2MedkitLinting.cmake Adds job-count configuration and forwards it to ament_clang_tidy to enable parallel TU analysis.
scripts/test.sh Adjusts the tidy preset to avoid CTest-level parallelism when running clang-tidy.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread src/ros2_medkit_cmake/README.md Outdated
@bburda bburda self-assigned this Aug 4, 2026
Comment thread scripts/test.sh Outdated
Three problems with the parallel clang-tidy gate as it stood.

The default job count was the host core count, which is unbounded in
memory. Measured on ros2_medkit_gateway, memory scales linearly at
roughly 1.2 GiB per job while wall clock does not:

  jobs=2   1078 s    2.6 GiB peak resident
  jobs=4    595 s    4.7 GiB
  jobs=8    359 s    9.4 GiB
  jobs=16   272 s   17.4 GiB

Size the default so one package fits an 8 GB machine and it lands at
min(host cores, 2). That is deliberately the slow end of the curve; the
alternative is a default that only works on the largest machine anyone
here has, and 17.4 GiB for a single package is not something we can
assume.

Add the switch that makes the low default liveable. The count is baked
into the CTest command at configure time, so ./scripts/test.sh tidy
--jobs <n> reconfigures the clang-tidy packages first - about three
seconds each, no recompilation - then runs. The value persists in the
CMake cache, so every tidy run prints the count in effect rather than
leaving it as invisible state.

The tidy preset did not actually serialise packages. It passed
--ctest-args -j 1, but that flag cannot do it: a participating package
registers exactly one clang_tidy test and colcon runs a separate ctest
per package, so -j has nothing to overlap. What schedules packages is
colcon's --parallel-workers, which COMMON_ARGS set to nproc - so every
package could analyse concurrently while each one spawned its own jobs,
the multiplication the comment claimed to prevent.

Pin --parallel-workers 1 after the caller's arguments. colcon keeps
recognising its own options after --ctest-args and the option is a plain
argparse store, so the last occurrence wins; placed earlier, a caller's
own --parallel-workers silently undid the guard. The preset reports when
it ignores such an override. Confirmed against colcon's scheduler:
package Starting/Finished pairs no longer interleave, with or without a
trailing override.

Fail the run when a clang-tidy process dies. ament_clang_tidy derives its
exit status from the warnings it parsed, so a process killed by the OOM
reaper contributes nothing and the test passes as if the package were
clean - the worst way to lose coverage. run_test.py merges the child's
stderr into the per-test log, so the tidy preset scans those logs for the
failure marker and fails, naming what went unanalysed.

Correct the CI description where it contradicted itself. The module table
and the linting section both called clang-tidy "mandatory in CI", and the
design doc said the same, but quality.yml configures with
-DENABLE_CLANG_TIDY=OFF and runs run-clang-tidy over the compilation
database, so the CTest target is a local gate only. The 0.4.0 changelog
entry keeps that wording: CI only switched in April, after that release.
Also qualify "each package" - packages excluding ament_cmake_clang_tidy
register no such test - and note the ProcessorCount fallback to 1.
@bburda
bburda merged commit ebb01b2 into main Aug 5, 2026
14 checks passed
@bburda
bburda deleted the perf/clang-tidy-parallel-jobs branch August 5, 2026 20:34
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.

Full clang-tidy gate runs one file at a time

3 participants