Skip to content

ci: Speed up the Linux builds (parallel make, ccache, docs out of ALL) - #1408

Merged
mickem merged 3 commits into
mainfrom
ci-build-speedups
Aug 14, 2026
Merged

ci: Speed up the Linux builds (parallel make, ccache, docs out of ALL)#1408
mickem merged 3 commits into
mainfrom
ci-build-speedups

Conversation

@mickem

@mickem mickem commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Three independent changes, one per commit, all aimed at the one step that dominates every build job.

Where the time goes today

Measured on the last release build of main (run 31686095180, commit 7acb180): 2 h 21 m wall, 10.2 runner-hours over 21 jobs, plus 5.0 h of jobs sitting in the queue waiting for a runner. Build nsclient is 96-97 % of every Linux job:

Job Total Build nsclient
build-rpm rocky10 arm64 65.7 m 63.9 m (97 %)
build-rpm rocky10 x64 62.2 m ~60 m
build-deb ubuntu arm64 57.4 m 55.9 m (97 %)
build-deb ubuntu x64 44.8 m 42.9 m (96 %)
sanitizer-tests 47.6 m 45.8 m (96 %)

Dependency install, packaging, artifact upload and ctest are all noise by comparison.

1. make -j$(nproc) (4d1e256b)

Both Linux package workflows ran a bare make, leaving three of the four cores on a hosted runner idle for the whole build.

A full local build is 6 600 CPU-seconds over 819 translation units and sustained 1433 % CPU at -j16 - the module graph is wide enough (70-odd independent modules) to scale near-linearly. tests-sanitizers.yml already builds this tree with -j$(nproc) in the same ubuntu:24.04 container, so parallel builds are known-good here.

2. HTML docs out of ALL off Windows (13991a7d)

build_docs_html was in ALL on every platform, but only the Windows installer ships the site (install() is guarded by WIN32). Linux built it and threw it away - and because it was in ALL, a mkdocs failure aborted the whole build before the binaries were finished. Hit exactly that while benchmarking.

New NSCP_BUILD_DOCS_HTML option: ON on Windows (the installer is built with /p:BuildProjectReferences=false, so it cannot pull the docs in as a dependency), OFF elsewhere. The target still exists everywhere - cmake --build . --target build_docs_html.

3. ccache on the Linux jobs (06a7f9b9)

Nothing was cached between runs. Wired in as a CMake compiler launcher with the ccache dir in actions/cache, only when ccache is actually on PATH so a distro dropping the package degrades to today's behaviour rather than failing to configure. Every job prints ccache --show-stats after building, so the hit rate is in the log.

This PR's own run is a cold cache - it shows the -j win only. The second push to this branch is where ccache shows up.

Not in this PR

  • Shared sources (NSCP_DEF_PLUGIN_CPP and friends) are pasted into every module's SRCS, so 36 % of all compile CPU is recompiling a file already compiled in another target - 521 of 819 TUs, 2 644 CPU-seconds. The seven worst (nscapi/settings/helper.cpp 43x, net/socket/socket_helpers.cpp 26x, nscapi_program_options.cpp 42x, ...) are 19 % on their own. Fixing that means one object library and it pays off on Windows too.
  • msbuild has no /m (mitigated by /MP being set globally).
  • Single hot TUs: check_tcp.cpp 64 s, CheckNSCP.cpp 62 s, check_http.cpp 52 s - candidates for a shared PCH.
  • build-feature.yml runs this same 21-job matrix on every PR push (~10 runner-hours/push); gating arm64 and the legacy x86-static build to tags would halve it, but that is a support-policy call.

🤖 Generated with Claude Code

mickem added 3 commits August 14, 2026 10:27
The `Build nsclient` step is 96-97% of every Linux package job - 43 minutes
on x64, 64 on arm64 - and it ran a bare `make`, so three of the four cores
on a hosted runner sat idle for the whole build.

The dependency graph is wide (70-odd independent modules): a full build
measured 6600 CPU-seconds over 819 translation units and sustained 1433%
CPU at -j16, so it scales near-linearly with the cores available. The
sanitizer job already builds this tree with `-j$(nproc)` in the same
ubuntu:24.04 container, so parallel builds are known to work here.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Michael Medin <michael@medin.name>
The mkdocs site was in `ALL`, so every build on every platform rendered it -
but only the Windows installer ships it (the install() is guarded by WIN32).
The Linux package and sanitizer jobs built the site and threw it away.

Worse, being in `ALL` made mkdocs a load-bearing part of the build: a docs
failure aborted the whole thing before the binaries were done, which is not
what you want from a step whose output is discarded.

Add NSCP_BUILD_DOCS_HTML, defaulting to ON on Windows (where the installer
needs the site pre-built - it is built with /p:BuildProjectReferences=false,
so it cannot pull the docs target in as a dependency) and OFF elsewhere. The
target still exists everywhere:

    cmake --build . --target build_docs_html

The Windows install() is guarded by the same option, since with the site not
built there is no directory to install and install(DIRECTORY) on a missing
source is a hard error at package time.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Michael Medin <michael@medin.name>
Nothing was cached between runs, so every push recompiled all ~820
translation units from scratch in each of the three Linux jobs (two package
builds plus the sanitizer job) - and a push typically only touches a handful
of files.

Wire ccache in as a CMake compiler launcher and keep its directory in the
GitHub Actions cache. The launcher is only passed when ccache is actually on
PATH, so a distro that stops shipping it degrades to today's behaviour
instead of failing to configure; on Rocky the package comes from EPEL, which
the job already enables.

The cache directory sits inside the workspace, which is the one path spelled
the same way by actions/cache (which resolves `path:` relative to the
workspace) and by the build running inside the container, where
${{ github.workspace }} is the host's view and not the container's. It is
restored after checkout, since checkout prunes untracked files.

Each job writes an entry keyed by sha and restores by prefix, so a run always
starts from the most recent cache for its distro and arch, and a new branch
inherits main's. Capped at 500M per job to stay well inside the repository's
10G cache budget. Every job prints `ccache --show-stats` after building, so
the hit rate is visible in the log rather than inferred from the timings.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Michael Medin <michael@medin.name>
@mickem
mickem merged commit a26d28d into main Aug 14, 2026
29 checks passed
@mickem
mickem deleted the ci-build-speedups branch August 14, 2026 09:55
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.

1 participant