diff --git a/.ci/compile.sh b/.ci/compile.sh index 302972f30..6789b9817 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_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 # 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" 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 1ce51afaa..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 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- 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' 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 }}