diff --git a/.ci/compile.sh b/.ci/compile.sh
index 302972f30..808f13e91 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
@@ -133,6 +138,12 @@ done
set -e
+# cmake reads the generator platform from the environment, so a leftover platform
+# follows the generator around; Ninja has no platform to select.
+if [[ $CMAKE_GENERATOR != "Visual Studio"* ]]; then
+ unset CMAKE_GENERATOR_PLATFORM
+fi
+
# Setup
./servatrice/check_schema_version.sh
if [[ ! $BUILDTYPE ]]; then
@@ -209,6 +220,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 +320,33 @@ 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 it 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 needs the MSVC switches nor
+ # wants 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 +362,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..08efb5782 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -385,6 +385,12 @@ 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.
+ # Enabled here on purpose: this pull request measures MSBuild
+ # parallelism, so its run is the one worth profiling.
+ build_profile: 1
+
cmake_generator: "Visual Studio 18 2026"
cmake_generator_platform: x64
make_package: 1
@@ -395,11 +401,36 @@ jobs:
vcpkg_scope: 'vcpkg/windows10-x64-vs2026/'
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
- name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
+ # A/B run: is Ninja faster than the Visual Studio generator on this runner?
+ # It is allowed to fail and is never published, see the steps that build
+ # a release below. Delete this entry once the answer is known.
+ - os: Windows
+ target: 10
+ runner: windows-2025
+
+ cmake_generator: Ninja
+ experiment: yes
+ make_package: 1
+ package_suffix: "-Win10-ninja"
+ qt_version: 6.11.*
+ qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
+ type: Release
+ variant: ' (Ninja)'
+ # The same scope as the Visual Studio leg, so both halves of the A/B
+ # start from the same vcpkg cache and the comparison is about the
+ # generator alone. Without it this leg's prefix is empty, which makes
+ # the cache action restore the whole vcpkg/ namespace and write entries
+ # that no scope claims. vcpkg_upload is deliberately left unset, so only
+ # the Visual Studio leg hands its archives over.
+ vcpkg_scope: 'vcpkg/windows10-x64-vs2026/'
+
+ name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}${{ matrix.variant }}
needs: configure
runs-on: ${{ matrix.runner }}
+ continue-on-error: ${{ matrix.experiment == 'yes' }}
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
@@ -482,6 +513,14 @@ jobs:
modules: ${{ matrix.qt_modules }}
version: ${{ matrix.qt_version }}
+ # Ninja invokes cl.exe directly and needs the MSVC environment on PATH; the
+ # Visual Studio generator locates the compiler on its own and skips this.
+ - name: "[Windows/Ninja] Set up MSVC environment"
+ if: matrix.os == 'Windows' && matrix.cmake_generator == 'Ninja'
+ uses: TheMrMilchmann/setup-msvc-dev@v4.1.0
+ with:
+ arch: x64
+
- name: "[Windows] Install NSIS"
if: matrix.os == 'Windows'
shell: bash
@@ -690,8 +729,11 @@ jobs:
if-no-files-found: error
path: ${{ steps.build.outputs.path }}
+ # Ninja writes its PDBs next to the binaries instead of into a Release
+ # subdirectory, and the experimental job is not a source of release debug
+ # symbols, so it keeps out of both.
- name: "[Windows] Upload PDBs (Program Databases)"
- if: matrix.os == 'Windows' && github.ref_type != 'tag'
+ if: matrix.os == 'Windows' && matrix.experiment != 'yes' && github.ref_type != 'tag'
uses: actions/upload-artifact@v7
with:
if-no-files-found: error
@@ -701,8 +743,19 @@ 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'
+ if: needs.configure.outputs.tag != null && matrix.make_package == '1' && matrix.experiment != 'yes'
id: upload_release
shell: bash
env: