Compare commits

...

8 commits

Author SHA1 Message Date
opencode
330818a5ac [CI] Stop persisting Linux caches instead of caching the image
Measuring the follow-up showed the image half costs more than it frees:
the nine build images run 1.1 to 2.4 GB each on disk, about 5 to 6 GB of
cache quota for all of them once compressed, against the roughly 2.6 GB
the Linux ccache entries occupy. Persisting the image would push the
repository further over quota instead of under it, so these legs now
persist nothing: the image is rebuilt every run as before, and ccache
stays a throwaway dir shared only between the steps of one run.
2026-10-14 21:38:18 +02:00
opencode
66d067375e [CI] Invalidate build image cache when Dockerfile changes 2026-10-14 19:16:32 +02:00
Lukas Brübach
9de0539641 [CI] Load and save the docker image through the image cache 2026-10-14 19:16:32 +02:00
Lukas Brübach
f8ec965359 Fix cache step naming in macOS job per review 2026-10-14 19:16:32 +02:00
Lukas Brübach
452400475f [CI] Stop caching what the cache inventory says is not being used 2026-10-14 19:16:32 +02:00
BruebachL
fea12a74e2
[CI] Build Windows releases with Ninja (#7380)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
The A/B run measured Ninja against the Visual Studio generator across
ten builds: Ninja medians sit at ~17 min against ~21 for MSBuild, and
more importantly Ninja already matches the Linux and macOS legs, runs
the compiler itself and parallelizes the whole graph instead of one
MSBuild node at a time. Single runs on shared 4-core hosts swing by
minutes either way (VS and Ninja legs even ran in different Azure
regions inside one matrix), so this switches on medians and
engineering coherence, not on any one timing.

The Ninja leg is now the release leg: it keeps the -Win10 suffix and
the vcpkg scope (named after the vs2026 toolchain, whose cl.exe is
unchanged, so the warm archives keep restoring), uploads PDBs from the
single-config layout and ships installers to releases. The experiment
scaffolding goes away with the generator it measured.
2026-10-14 19:13:52 +02:00
BruebachL
0c10798ef4
[CI] Build the Windows solution on more than one MSBuild node (#7379)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
cmake --build only passes a job count to MSBuild when it is asked for
one, and .ci/compile.sh never asked, so every Visual Studio generator
build ran single-node: projects were built one after another and MTT
only ever parallelized the files of whichever project happened to be
building. Ask for one node per core, overridable with
BUILD_PARALLEL_LEVEL, and fall back to a single node when the core
count is not exposed.

Alongside it set EnforceProcessCountAcrossBuilds and CL_MPCount, the
knobs Microsoft's build throughput write-up pairs with MTT: without
them the node count and MTT multiply into more cl.exe processes than
the runner has cores for.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-14 16:15:01 +02:00
BruebachL
eefe0bd672
[CI] Collect an MSBuild performance summary and binary log on request (#7378)
The Windows job spends about 28 minutes inside one "Build project"
group, with nothing in it that says where the time goes: MSBuild runs
single-node because no --parallel is passed, so projects build one
after another and it is not visible which projects, tasks or phases are
responsible.

Add a --profile flag that makes the Windows build write
-flp:PerformanceSummary and -bl, repeat the summary in the job log so
the numbers need no download, and report the core count, which decides
how much parallelism is worth asking for. The binary log is uploaded as
an artifact only on the runs that asked for it.

The workflow drives it from a build_profile matrix key that is empty by
default, so no build changes behaviour until someone sets it.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-14 14:27:58 +02:00
4 changed files with 99 additions and 40 deletions

View file

@ -17,8 +17,9 @@
# --dir <dir> sets the name of the build dir, default is "build"
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
# --target-macos-version <version> sets the min os version - only used for macOS builds
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE VCPKG_TARGET_MACOS_VERSION
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --sanitize --coverage --ccache <ccache_size> --dir <dir>)
# --profile writes an MSBuild performance summary and binary log, Windows only
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE VCPKG_TARGET_MACOS_VERSION BUILD_PARALLEL_LEVEL BUILD_PROFILE
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --sanitize --coverage --ccache <ccache_size> --dir <dir> --profile)
# exitcode: 1 for failure, 3 for invalid arguments
# Read arguments
@ -115,6 +116,10 @@ while [[ $# != 0 ]]; do
export CMAKE_GENERATOR=$1
shift
;;
'--profile')
BUILD_PROFILE=1
shift
;;
'--target-macos-version')
shift
if [[ $# == 0 ]]; then
@ -209,6 +214,22 @@ function ccachestatsverbose() {
fi
}
function cpuinfo() {
# The number of cores decides how much build parallelism is useful; the hosted
# runners are not all sized alike, so read it off the machine instead of assuming.
case "$RUNNER_OS" in
Windows)
echo "cores: ${NUMBER_OF_PROCESSORS:-unknown}"
;;
macOS)
echo "cores: $(sysctl -n hw.ncpu)"
;;
*)
echo "cores: $(nproc 2>/dev/null || echo unknown)"
;;
esac
}
# Compile
if [[ $RUNNER_OS == macOS ]]; then
# QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action,
@ -293,9 +314,34 @@ if [[ $RUNNER_OS == macOS ]]; then
fi
elif [[ $RUNNER_OS == Windows ]]; then
# Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
# and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt
buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true)
if [[ $CMAKE_GENERATOR == "Visual Studio"* ]]; then
# cmake only hands MSBuild a job count when one is asked for, and without it
# MSBuild builds a single node: one project at a time, whatever MTT does inside
# it. Ask for a node per core.
if [[ ! $BUILD_PARALLEL_LEVEL ]]; then
BUILD_PARALLEL_LEVEL="${NUMBER_OF_PROCESSORS:-1}"
fi
buildflags+=(--parallel "$BUILD_PARALLEL_LEVEL")
# Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
# and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt
# EnforceProcessCountAcrossBuilds makes the CL_MPCount ceiling apply across all
# the projects the nodes are building, instead of each node running its own
# cl.exe processes and /m and MTT multiplying into an oversubscribed machine.
buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true -p:EnforceProcessCountAcrossBuilds=true -p:CL_MPCount="$BUILD_PARALLEL_LEVEL")
if [[ $BUILD_PROFILE ]]; then
# A performance summary says which projects and tasks cost what, the binary log
# keeps the full timeline; both are far too noisy to always produce. The files
# land next to the solution, i.e. in the build dir the workflow uploads. These
# join the MTT options above, which already carried the "--" separator.
buildflags+=("-flp:PerformanceSummary;v=q;LogFile=msbuild-perf.log" -bl:msbuild.binlog)
fi
else
# Ninja runs the compiler itself, so it neither takes the MSBuild switches
# (it rejects -p: options outright) nor a job count: it sizes its own pool
# for the cores it finds.
echo "ninja $(ninja --version)"
fi
fi
if [[ $USE_CCACHE ]]; then
@ -311,10 +357,19 @@ cmake .. "${flags[@]}"
echo "::endgroup::"
echo "::group::Build project"
cpuinfo
echo "Running cmake --build with flags: ${buildflags[*]}"
cmake --build . "${buildflags[@]}"
echo "::endgroup::"
if [[ $BUILD_PROFILE && $RUNNER_OS == Windows && -f msbuild-perf.log ]]; then
# Repeat the summary in the job log so the numbers are readable without
# downloading the binary log, which is hundreds of megabytes.
echo "::group::MSBuild performance summary"
cat msbuild-perf.log
echo "::endgroup::"
fi
if [[ $USE_CCACHE ]]; then
if [[ $CCACHE_EVICTION_AGE ]]; then
echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE"

View file

@ -51,6 +51,11 @@ jobs:
# https://docs.github.com/en/code-security/reference/code-scanning/codeql/codeql-queries/actions-built-in-queries
queries: security-extended
dependency-caching: true
# A TRAP cache costs about 974 MiB per language here. On master that is worth
# paying, because the next push reuses it. On a pull request it is pure loss:
# the cache lands on refs/pull/N/merge, which no other run ever reads, so every
# new pull request leaves a dead 974 MiB entry behind.
trap-caching: ${{ github.event_name != 'pull_request' }}
- name: "[C++] Install dependencies"
if: matrix.language == 'cpp' && matrix.build-mode == 'manual'

View file

@ -168,7 +168,7 @@ jobs:
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
timeout-minutes: 70
env:
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # scratch dir for the container image build and throwaway ccache; nothing below is cached
CCACHE_EVICTION_AGE: 7d
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CMAKE_GENERATOR: 'Ninja'
@ -179,13 +179,13 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v7
- name: "Restore compiler cache (ccache)"
id: restore_ccache
uses: actions/cache/restore@v6
with:
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
path: ${{ env.CACHE }}/.ccache
# Nothing on these legs is worth persisting. A compile here runs inside a
# container, so ccache measures a 0.00 to 1.85 percent hit rate against 49 to
# 50 percent on the macOS legs: persisting it spent a few hundred MiB per
# entry buying nothing. The build image is not an alternative: it runs 1.1 to
# 2.4 GB per leg on disk, about 5 to 6 GB of quota for all nine legs once
# compressed, so caching it would cost more than dropping the ccache frees.
# The old ccache-<distro> entries are never read again and expire on their own.
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
shell: bash
run: source .ci/docker.sh --build
@ -250,25 +250,6 @@ jobs:
RUN --server --release --package "$package" "${args[@]}"
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: "Delete remote compiler cache (ccache)"
if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
continue-on-error: true
env:
CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
echo "Cache deleted successfully"
fi
- name: "Save updated compiler cache (ccache)"
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
path: ${{ env.CACHE }}/.ccache
- name: "Upload artifact"
id: upload_artifact
if: matrix.package != 'skip'
@ -385,13 +366,20 @@ jobs:
target: 10
runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
cmake_generator: "Visual Studio 18 2026"
cmake_generator_platform: x64
# Ninja builds the whole dependency graph at once instead of one
# MSBuild node at a time, runs the compiler itself, and matches the
# Linux and macOS legs. Its median build phase measured ~17 min
# against ~21 for the Visual Studio generator (shared 4-core hosts
# swing either by minutes, so read medians, not single runs).
cmake_generator: Ninja
make_package: 1
package_suffix: "-Win10"
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
type: Release
# Scope is named after the vs2026 toolchain, not the generator: the
# ABI hash covers cl.exe, which is identical, so this leg keeps
# restoring the warm archives. Renaming would orphan them.
vcpkg_scope: 'vcpkg/windows10-x64-vs2026/'
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
@ -487,6 +475,13 @@ jobs:
shell: bash
run: choco install nsis
# Ninja invokes cl.exe directly and needs the MSVC environment on PATH.
- name: "[Windows] Set up MSVC environment"
if: matrix.os == 'Windows'
uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9 # v4.1.0
with:
arch: x64
- name: "Setup vcpkg cache"
id: vcpkg-cache
uses: TAServers/vcpkg-cache@v3
@ -521,7 +516,6 @@ jobs:
BUILDTYPE: '${{ matrix.type }}'
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
@ -690,6 +684,8 @@ jobs:
if-no-files-found: error
path: ${{ steps.build.outputs.path }}
# Ninja is single-config: binaries and their PDBs land directly in the
# target directories instead of a Release subdirectory.
- name: "[Windows] Upload PDBs (Program Databases)"
if: matrix.os == 'Windows' && github.ref_type != 'tag'
uses: actions/upload-artifact@v7
@ -697,9 +693,9 @@ jobs:
if-no-files-found: error
name: ${{ steps.build.outputs.name }}-PDBs
path: |
build/cockatrice/Release/*.pdb
build/oracle/Release/*.pdb
build/servatrice/Release/*.pdb
build/cockatrice/*.pdb
build/oracle/*.pdb
build/servatrice/*.pdb
- name: "Upload to release"
if: needs.configure.outputs.tag != null && matrix.make_package == '1'

View file

@ -89,7 +89,10 @@ jobs:
uses: docker/build-push-action@v7
with:
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }}
# mode=min exports only the layers of the final image. mode=max also exports
# every intermediate build layer, which for this multi-stage build is where the
# bulk of the 42 cached blobs came from, and none of it survives into the image.
cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=min,scope={0}', env.CACHE_SCOPE), '') }}
context: .
platforms: ${{ matrix.platform }}
push: false
@ -101,7 +104,7 @@ jobs:
uses: docker/build-push-action@v7
with:
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
cache-to: type=gha,mode=min,scope=${{ env.CACHE_SCOPE }}
context: .
labels: |
org.opencontainers.image.description=${{ env.OCI_DESCRIPTION }}