From eefe0bd672cfa8d0b982fbb6dbcf1316273d2f85 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:27:58 +0200 Subject: [PATCH 1/8] [CI] Collect an MSBuild performance summary and binary log on request (#7378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .ci/compile.sh | 41 +++++++++++++++++++++++++++-- .github/workflows/desktop-build.yml | 16 +++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.ci/compile.sh b/.ci/compile.sh index 302972f30..eb850a88b 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -17,8 +17,9 @@ # --dir sets the name of the build dir, default is "build" # --cmake-generator sets CMAKE_GENERATOR as used by cmake # --target-macos-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 --suffix --server --test --sanitize --coverage --ccache --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_PROFILE +# (correspond to args: --debug/--release --install --package --suffix --server --test --sanitize --coverage --ccache --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, @@ -296,6 +317,13 @@ 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 [[ $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 fi if [[ $USE_CCACHE ]]; then @@ -311,10 +339,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" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 1ce51afaa..7384ec8ad 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -385,6 +385,10 @@ jobs: target: 10 runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md + # Set build_profile: 1 to have the build write an MSBuild performance + # summary and a binary log; empty on every entry that runs by default. + build_profile: '' + cmake_generator: "Visual Studio 18 2026" cmake_generator_platform: x64 make_package: 1 @@ -400,6 +404,7 @@ jobs: runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: + BUILD_PROFILE: '${{ matrix.build_profile }}' CCACHE_DIR: ${{ github.workspace }}/.cache/ 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 @@ -701,6 +706,17 @@ jobs: build/oracle/Release/*.pdb build/servatrice/Release/*.pdb + # Written only when the matrix entry sets build_profile, so the artifact + # exists exactly on the runs that were asked to produce one. + - name: "[Windows] Upload MSBuild binary log" + if: always() && matrix.os == 'Windows' && env.BUILD_PROFILE == '1' + uses: actions/upload-artifact@v7 + with: + if-no-files-found: error + name: ${{ steps.build.outputs.name }}-msbuild + path: build/msbuild.binlog + retention-days: 7 + - name: "Upload to release" if: needs.configure.outputs.tag != null && matrix.make_package == '1' id: upload_release From 0c10798ef42c5fbe0b371ecc12f49bfd84946d17 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:15:01 +0200 Subject: [PATCH 2/8] [CI] Build the Windows solution on more than one MSBuild node (#7379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .ci/compile.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.ci/compile.sh b/.ci/compile.sh index eb850a88b..4aa189bc7 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -18,7 +18,7 @@ # --cmake-generator sets CMAKE_GENERATOR as used by cmake # --target-macos-version sets the min os version - only used for macOS builds # --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_PROFILE +# 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 --suffix --server --test --sanitize --coverage --ccache --dir --profile) # exitcode: 1 for failure, 3 for invalid arguments @@ -314,9 +314,20 @@ if [[ $RUNNER_OS == macOS ]]; then fi elif [[ $RUNNER_OS == Windows ]]; 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 - buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true) + # 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 From fea12a74e2df0dd18dd65a3d2178d2002f289d17 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:13:52 +0200 Subject: [PATCH 3/8] [CI] Build Windows releases with Ninja (#7380) 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. --- .ci/compile.sh | 45 +++++++++++++++++------------ .github/workflows/desktop-build.yml | 43 ++++++++++++++------------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/.ci/compile.sh b/.ci/compile.sh index 4aa189bc7..6789b9817 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -314,26 +314,33 @@ if [[ $RUNNER_OS == macOS ]]; then fi elif [[ $RUNNER_OS == Windows ]]; 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") + 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) + # 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 diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 7384ec8ad..beedaeaf2 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -385,17 +385,20 @@ jobs: target: 10 runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md - # Set build_profile: 1 to have the build write an MSBuild performance - # summary and a binary log; empty on every entry that runs by default. - build_profile: '' - - 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 @@ -404,7 +407,6 @@ jobs: runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: - BUILD_PROFILE: '${{ matrix.build_profile }}' CCACHE_DIR: ${{ github.workspace }}/.cache/ 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 @@ -492,6 +494,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 @@ -526,7 +535,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 }} @@ -695,6 +703,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 @@ -702,20 +712,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 - - # Written only when the matrix entry sets build_profile, so the artifact - # exists exactly on the runs that were asked to produce one. - - name: "[Windows] Upload MSBuild binary log" - if: always() && matrix.os == 'Windows' && env.BUILD_PROFILE == '1' - uses: actions/upload-artifact@v7 - with: - if-no-files-found: error - name: ${{ steps.build.outputs.name }}-msbuild - path: build/msbuild.binlog - retention-days: 7 + build/cockatrice/*.pdb + build/oracle/*.pdb + build/servatrice/*.pdb - name: "Upload to release" if: needs.configure.outputs.tag != null && matrix.make_package == '1' From 452400475f1ab21e9d92830da5fef91446356949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Mon, 5 Oct 2026 18:33:23 +0200 Subject: [PATCH 4/8] [CI] Stop caching what the cache inventory says is not being used --- .github/workflows/codeql.yml | 5 ++++ .github/workflows/desktop-build.yml | 38 ++++++++++++++++------------ .github/workflows/docker-release.yml | 7 +++-- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 75fbc59f1..18bb5e8ed 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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' diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index beedaeaf2..063c8ed38 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -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 }} # directory holding the build image and ccache, only the image subdir is cached below 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,12 +179,18 @@ jobs: - name: "Checkout" uses: actions/checkout@v7 - - name: "Restore compiler cache (ccache)" - id: restore_ccache + # Only the image half of this cache is worth keeping. A compile on these legs runs + # inside a container, so ccache measures a 0.00 to 1.85 percent hit rate here against + # 49 to 50 percent on the macOS legs, and the roughly 420 MiB of ccache each entry + # carried was buying nothing. Caching $CACHE/image alone drops that. + # The key changes name because the cached content does: the old ccache- + # entries hold an image plus a ccache directory, and expire on their own untouched. + - name: "Restore build image cache" + id: restore_image_cache uses: actions/cache/restore@v6 with: - key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} - path: ${{ env.CACHE }}/.ccache + key: image-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} + path: ${{ env.CACHE }}/image - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" shell: bash @@ -250,24 +256,24 @@ 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 + # Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342 + - name: "Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342" + if: github.ref == 'refs/heads/master' && steps.restore_image_cache.outputs.cache-hit continue-on-error: true env: - CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} + CACHE_PRIMARY_KEY: ${{ steps.restore_image_cache.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)" + - name: "Save updated build image cache" if: github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: - key: ${{ steps.restore_ccache.outputs.cache-primary-key }} - path: ${{ env.CACHE }}/.ccache + key: ${{ steps.restore_image_cache.outputs.cache-primary-key }} + path: ${{ env.CACHE }}/image - name: "Upload artifact" id: upload_artifact @@ -428,7 +434,7 @@ jobs: - name: "[macOS] Restore compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 - id: restore_ccache + id: restore_image_cache uses: actions/cache/restore@v6 with: key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }} @@ -630,10 +636,10 @@ jobs: # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "[macOS] Delete remote compiler cache (ccache)" - if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_image_cache.outputs.cache-hit continue-on-error: true env: - CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} + CACHE_PRIMARY_KEY: ${{ steps.restore_image_cache.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then @@ -644,7 +650,7 @@ jobs: if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: - key: ${{ steps.restore_ccache.outputs.cache-primary-key }} + key: ${{ steps.restore_image_cache.outputs.cache-primary-key }} path: ${{ env.CCACHE_DIR }} - name: "[macOS] Sign app bundle" diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 937386940..b7c08d341 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -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 }} From f8ec965359e130a91619f68080aaeff4f19ed3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 6 Oct 2026 06:47:25 +0200 Subject: [PATCH 5/8] Fix cache step naming in macOS job per review --- .github/workflows/desktop-build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 063c8ed38..3df905a7d 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -257,7 +257,7 @@ jobs: RUN --server --release --package "$package" "${args[@]}" # Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342 - - name: "Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342" + - name: "[Linux] Delete used cache to emulate a cache update" if: github.ref == 'refs/heads/master' && steps.restore_image_cache.outputs.cache-hit continue-on-error: true env: @@ -434,7 +434,7 @@ jobs: - name: "[macOS] Restore compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 - id: restore_image_cache + id: restore_ccache uses: actions/cache/restore@v6 with: key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }} @@ -636,10 +636,10 @@ jobs: # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "[macOS] Delete remote compiler cache (ccache)" - if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_image_cache.outputs.cache-hit + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit continue-on-error: true env: - CACHE_PRIMARY_KEY: ${{ steps.restore_image_cache.outputs.cache-primary-key }} + 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 @@ -650,7 +650,7 @@ jobs: if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: - key: ${{ steps.restore_image_cache.outputs.cache-primary-key }} + key: ${{ steps.restore_ccache.outputs.cache-primary-key }} path: ${{ env.CCACHE_DIR }} - name: "[macOS] Sign app bundle" From 9de0539641164aaa59f3311cb82ead361efe8f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 11 Oct 2026 18:17:17 +0200 Subject: [PATCH 6/8] [CI] Load and save the docker image through the image cache --- .github/workflows/desktop-build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 3df905a7d..0409709cd 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -194,7 +194,14 @@ jobs: - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" shell: bash - run: source .ci/docker.sh --build + run: | + # --get/--save wire the $CACHE/image dir restored/saved above into the + # build: without them the image is rebuilt from scratch every run and + # the image cache saves nothing. Only persist on master; PR runs just + # reuse whatever the restore step found. + args=(--get --build) + [[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--save) + source .ci/docker.sh "${args[@]}" - name: "Build debug and test" if: matrix.test != 'skip' From 66d067375e0ca17920ffaf41c538495245be8c40 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 13 Oct 2026 07:17:37 +0200 Subject: [PATCH 7/8] [CI] Invalidate build image cache when Dockerfile changes --- .github/workflows/desktop-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 0409709cd..3dcbd7618 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -189,7 +189,7 @@ jobs: id: restore_image_cache uses: actions/cache/restore@v6 with: - key: image-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} + key: image-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}-${{ hashFiles(format('.ci/{0}{1}/**', matrix.distro, matrix.version)) }} path: ${{ env.CACHE }}/image - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" From 330818a5ac2e002affc62d023597b925226dd26e Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 14 Oct 2026 21:38:18 +0200 Subject: [PATCH 8/8] [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. --- .github/workflows/desktop-build.yml | 50 ++++++----------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 3dcbd7618..a379d1710 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -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 holding the build image and ccache, only the image subdir is cached below + 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,29 +179,16 @@ jobs: - name: "Checkout" uses: actions/checkout@v7 - # Only the image half of this cache is worth keeping. A compile on these legs runs - # inside a container, so ccache measures a 0.00 to 1.85 percent hit rate here against - # 49 to 50 percent on the macOS legs, and the roughly 420 MiB of ccache each entry - # carried was buying nothing. Caching $CACHE/image alone drops that. - # The key changes name because the cached content does: the old ccache- - # entries hold an image plus a ccache directory, and expire on their own untouched. - - name: "Restore build image cache" - id: restore_image_cache - uses: actions/cache/restore@v6 - with: - key: image-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}-${{ hashFiles(format('.ci/{0}{1}/**', matrix.distro, matrix.version)) }} - path: ${{ env.CACHE }}/image - + # 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- entries are never read again and expire on their own. - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" shell: bash - run: | - # --get/--save wire the $CACHE/image dir restored/saved above into the - # build: without them the image is rebuilt from scratch every run and - # the image cache saves nothing. Only persist on master; PR runs just - # reuse whatever the restore step found. - args=(--get --build) - [[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--save) - source .ci/docker.sh "${args[@]}" + run: source .ci/docker.sh --build - name: "Build debug and test" if: matrix.test != 'skip' @@ -263,25 +250,6 @@ jobs: RUN --server --release --package "$package" "${args[@]}" - # Delete used cache to emulate a cache update. See https://github.com/actions/cache/issues/342 - - name: "[Linux] Delete used cache to emulate a cache update" - if: github.ref == 'refs/heads/master' && steps.restore_image_cache.outputs.cache-hit - continue-on-error: true - env: - CACHE_PRIMARY_KEY: ${{ steps.restore_image_cache.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 build image cache" - if: github.ref == 'refs/heads/master' - uses: actions/cache/save@v6 - with: - key: ${{ steps.restore_image_cache.outputs.cache-primary-key }} - path: ${{ env.CACHE }}/image - - name: "Upload artifact" id: upload_artifact if: matrix.package != 'skip'