From eebbb1a213ef0a1810a1bfdf298b0658d5bf3b08 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 17 Oct 2026 13:16:17 -0400 Subject: [PATCH] [CI] (.github/workflows, docs/ci, MG_IntegrationTest/Harness, scripts/data): reuse build-linux for split - build-linux and build-linux-split each probe the other's configure line with build_fingerprint.py on their own runner (disaggregation branches only); when it matches, build-linux-split skips its compile and the split consumers take build-linux's runtime and library - the consumers red when build-linux-split skipped but build-linux's guard did not agree, or when the link ratchet, now also run in build-linux, did not pass; a difference falls back to the old two builds, and dev is unchanged apart from the consumers also waiting for build-linux - the transport resolution check moves after 'Split lanes' and proves the IPC defaults from each passed entry's client log (split_log_paths.py ipc-config, canned tests); with both proven, Transport A/B and the non-strict split unit run are skipped as repeats - the test job carries REQUIRE_GPU and libegl-mesa0 on the three branches; skip census: arm-monolith, arm-inproc and unit-split become optional rows --- .github/workflows/test.yml | 403 +++++++++++++----- .../Harness/split_log_paths.py | 83 ++++ .../Harness/test_split_log_paths.py | 74 ++++ docs/ci/ci-cleanup-log.md | 3 + scripts/data/skip_census_baseline.json | 9 +- 5 files changed, 459 insertions(+), 113 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44511c06..cab93958 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,9 @@ jobs: permissions: actions: write contents: read + outputs: + split-identical: ${{ steps.split-guard.outputs.identical }} + link-ratchet: ${{ steps.ratchet.outcome }} env: BUILD_DIR: build-linux MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }} @@ -121,6 +124,25 @@ jobs: CI_BUILD_TYPE: ${{ secrets.ACTIONS_STEP_DEBUG == 'true' && 'Debug' || 'Release' }} run: bash scripts/ci/configure_build.sh primary "${BUILD_DIR}" + # On the disaggregation branches build-linux-split configures the same sources with only the + # benchmarks left out. This configures its line here as a probe, on this runner, and compares + # the two trees (scripts/ci/build_fingerprint.py). Equal means the split consumers take this + # job's artifacts; a difference, or a guard that cannot decide, means false, and + # build-linux-split builds its own as before. It never reds this job. + - name: Split configuration guard + id: split-guard + if: env.MOBILEGL_CI_DISAGGREGATED == 'ON' + run: | + if python3 scripts/ci/build_fingerprint.py --self-test > "${RUNNER_TEMP}/fingerprint-self-test.txt"; then + python3 scripts/ci/build_fingerprint.py guard --built "${BUILD_DIR}" --probe split \ + --probe-dir build-guard-split --github-output identical || echo "identical=false" >> "${GITHUB_OUTPUT}" + else + cat "${RUNNER_TEMP}/fingerprint-self-test.txt" + echo "::warning::build_fingerprint.py's self-test failed, so the split build is not reused" + echo "identical=false" >> "${GITHUB_OUTPUT}" + fi + rm -rf build-guard-split + - name: Build run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)" @@ -132,6 +154,32 @@ jobs: python3 scripts/ci/runtime_mode_proof.py build --build-dir "${BUILD_DIR}" \ --mode "${mode}" --output "${BUILD_DIR}/runtime-mode.json" + # build-linux-split's server link-closure ratchet, run on this build when that job reuses it. + # continue-on-error keeps a ratchet red from skipping this job's consumers; integration-split + # turns it into a red of its own instead (Split runtime agreement). + - name: Server link-closure ratchet + id: ratchet + if: steps.split-guard.outputs.identical == 'true' + continue-on-error: true + run: | + mkdir -p ci-artifacts + python3 scripts/link_ratchet.py --self-test + python3 scripts/link_ratchet.py \ + --build-dir "${BUILD_DIR}" \ + --baseline scripts/data/link_ratchet_baseline.txt \ + --assert-monotone \ + --bucket \ + --json ci-artifacts/link-ratchet.json + + - name: Upload link ratchet + if: ${{ always() && steps.split-guard.outputs.identical == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: link-ratchet + path: ci-artifacts/link-ratchet.json + if-no-files-found: warn + overwrite: true + - name: Show ccache stats if: ${{ always() }} run: ccache --show-stats @@ -302,6 +350,7 @@ jobs: # Initialize()'s glslang table prewarm. The retrace legs keep it: they replay real apps. env: MOBILEGL_DISABLE_SHADER_PREWARM: "1" + MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }} steps: - name: Checkout repo @@ -321,7 +370,7 @@ jobs: if: env.MOBILEGL_CI_NATIVE_GPU != '1' run: | sudo apt-get update - sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers + sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers ${{ env.MOBILEGL_CI_DISAGGREGATED == 'ON' && 'libegl-mesa0' || '' }} - name: Download Linux runtime uses: actions/download-artifact@v8 @@ -338,8 +387,13 @@ jobs: if [ -d "$root" ]; then python3 scripts/ci/prepare_test_runtime.py "$root"; fi done + # On the three branches this is also the split runtime's unit run (integration-split skips + # its copy when build-linux-split reuses this build), so it keeps that run's hardening: + # REQUIRE_GPU makes the server loop's EGL cases fail rather than skip without a driver. - name: Test working-directory: build-linux + env: + MOBILEGL_ITEST_REQUIRE_GPU: ${{ env.MOBILEGL_CI_DISAGGREGATED == 'ON' && '1' || '' }} run: | ulimit -c unlimited if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then @@ -1053,7 +1107,6 @@ jobs: cp -p "${RUNNER_TEMP}/verify-split.xml" verify-split-logs-lane/ || true echo "kept $(ls verify-split-logs-lane | wc -l) file(s) from the green lane run" - # NEGATIVE CONTROL A ON THE SPLIT ARM - integration-verify's step A, same knob, same shape # (this step passes when ctest FAILS), aimed at the ambient VerifySplit. entries of BOTH # backends. Measured at landing: 10 of 10 red, each with @@ -1114,28 +1167,28 @@ jobs: path: /tmp/core.* if-no-files-found: ignore - # P5's split build. The verify pair cloned, because the problem is the same one: a third - # configuration of the same sources whose whole value depends on the option having taken. + # The split build: the same sources with the transport and its in-process arm always on, which the + # split integration and retrace lanes consume. On the disaggregation branches build-linux has the + # same configuration apart from the benchmarks; the guard below proves that on this runner, and + # when it holds this job skips its compile and the consumers take build-linux's artifacts + # (build-linux runs the same guard, and the consumers require both). On dev it builds as before. # - # This dedicated build keeps the protocol/strict controls independent of the branch's - # primary runtime mode. MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (the split path decodes - # into the MGPipe applier and PIPE_PUSH is what compiles the applier), so the split arm needs a - # build of its own exactly as the verify arm does. - # - # THE `nm` STEP IS THE POINT OF THIS JOB. In a build without the option, MG_Remote is not - # compiled at all and MOBILEGL_TRANSPORT's parser does not exist - so the variable is accepted by - # the environment and silently ignored (CONTRACT-P5 5). Every downstream lane would then run - # monolith and go green under a name that says split. CMake will not complain about a typo'd -D, - # so the build-level assertion is the only guard, and its absence is precisely what would make - # the whole split arm meaningless. + # The `nm` step is the point of a build of its own. In a build without the option, MG_Remote is + # not compiled and MOBILEGL_TRANSPORT's parser does not exist, so the variable is accepted and + # ignored and every downstream lane would run monolith under a name that says split. CMake does + # not complain about a typo'd -D, so the build-level assertion is the guard; in reuse mode + # build-linux's runtime_mode_proof (--mode disaggregated) is the same assertion and more. build-linux-split: runs-on: ubuntu-26.04 timeout-minutes: 120 permissions: actions: write contents: read + outputs: + reuse-primary: ${{ steps.guard.outputs.identical == 'true' && 'true' || 'false' }} env: BUILD_DIR: build-split + MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }} CCACHE_BASEDIR: ${{ github.workspace }} CCACHE_COMPRESS: "true" CCACHE_DIR: ${{ github.workspace }}/.ccache @@ -1143,19 +1196,13 @@ jobs: CCACHE_NOHASHDIR: "true" steps: - - name: Set swap space - uses: pierotofy/set-swap-space@v1.0 - with: - swap-size-gb: 32 - - name: Checkout repo uses: actions/checkout@v6 with: # recursive, and load-bearing here beyond the usual: MOBILEGL_BUILD_DISAGGREGATED - # SHADOWS ITSELF BACK TO OFF when 3rdparty/flatbuffers/include is missing - # (CMakeLists.txt:471-486, a normal variable rather than a cache force, deliberately). - # A shallow checkout would therefore configure cleanly, build a monolith library, and - # be caught only by the nm step below. + # shadows itself back to OFF when 3rdparty/flatbuffers/include is missing (a normal + # variable rather than a cache force, deliberately). A shallow checkout would therefore + # configure cleanly, build a monolith library, and be caught only by the nm step below. submodules: recursive - name: Configure runner environment @@ -1167,14 +1214,6 @@ jobs: with: cmakeVersion: 3.31.10 - - name: Restore ccache - uses: actions/cache/restore@v5 - with: - path: .ccache - key: ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache-v1 - restore-keys: | - ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache- - - name: Prepare Vulkan SDK id: netsetup1274 continue-on-error: true @@ -1218,7 +1257,42 @@ jobs: - name: Configure CMake run: bash scripts/ci/configure_build.sh split "${BUILD_DIR}" + # build-linux's configuration, probed on this runner and compared with this tree. Equal + # means this job builds nothing and ends green; the consumers then take build-linux's + # artifacts, and red if build-linux's own guard disagrees. It never reds this job. + - name: Primary configuration guard + id: guard + if: env.MOBILEGL_CI_DISAGGREGATED == 'ON' + env: + CI_BUILD_TYPE: ${{ secrets.ACTIONS_STEP_DEBUG == 'true' && 'Debug' || 'Release' }} + run: | + if python3 scripts/ci/build_fingerprint.py --self-test > "${RUNNER_TEMP}/fingerprint-self-test.txt"; then + python3 scripts/ci/build_fingerprint.py guard --built "${BUILD_DIR}" --probe primary \ + --probe-dir build-guard-primary --github-output identical || echo "identical=false" >> "${GITHUB_OUTPUT}" + else + cat "${RUNNER_TEMP}/fingerprint-self-test.txt" + echo "::warning::build_fingerprint.py's self-test failed, so this job builds its own library" + echo "identical=false" >> "${GITHUB_OUTPUT}" + fi + rm -rf build-guard-primary + + - name: Set swap space + if: steps.guard.outputs.identical != 'true' + uses: pierotofy/set-swap-space@v1.0 + with: + swap-size-gb: 32 + + - name: Restore ccache + if: steps.guard.outputs.identical != 'true' + uses: actions/cache/restore@v5 + with: + path: .ccache + key: ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache-v1 + restore-keys: | + ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache- + - name: Build + if: steps.guard.outputs.identical != 'true' run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)" # `nm` and NOT `nm -D`, for the reason build-linux-verify spells out: everything under @@ -1227,6 +1301,7 @@ jobs: # stripped library would make the grep fail for a third, silent reason. # - name: MG_Remote symbols in split library + if: steps.guard.outputs.identical != 'true' run: | test -f "${BUILD_DIR}/libMobileGL.so" defined=$(nm --defined-only "${BUILD_DIR}/libMobileGL.so" | wc -l) @@ -1250,10 +1325,11 @@ jobs: # from the per-object partition the frontend symbols a server-only image still has to link # and compares the list with the stored baseline. Only new symbols fail; a vanished one is # printed with a request to re-baseline in the same commit. The baseline is a list, not a - # count, so swapping one symbol for another fails. It runs here, not in build-linux, because - # this job always builds MG_Remote (the script re-checks CMakeCache.txt), and before anything - # deletes the *.o files it reads. + # count, so swapping one symbol for another fails. It runs where MG_Remote is built (the + # script re-checks CMakeCache.txt) and before anything deletes the *.o files it reads: here, + # or in build-linux when this job reuses that build. - name: Server link-closure ratchet + if: steps.guard.outputs.identical != 'true' run: | mkdir -p ci-artifacts python3 scripts/link_ratchet.py --self-test @@ -1265,27 +1341,30 @@ jobs: --json ci-artifacts/link-ratchet.json # Uploaded even when the gate fired: a red ratchet is exactly the moment someone needs the - # per-symbol definer/referrer attribution the JSON carries, and a job that failed before - # uploading its evidence is a job nobody can act on. + # per-symbol definer/referrer attribution the JSON carries. - name: Upload link ratchet - if: ${{ always() }} + if: ${{ always() && steps.guard.outputs.identical != 'true' }} uses: actions/upload-artifact@v7 with: name: link-ratchet path: ci-artifacts/link-ratchet.json if-no-files-found: warn + overwrite: true - name: Show ccache stats - if: ${{ always() }} + if: ${{ always() && steps.guard.outputs.identical != 'true' }} run: ccache --show-stats + # Only after a build: with nothing built, deleting would drop this branch's entry for good. - name: Release superseded ccache entry + if: steps.guard.outputs.identical != 'true' env: GH_TOKEN: ${{ github.token }} CACHE_KEY: ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache-v1 run: gh cache delete "${CACHE_KEY}" --ref "${GITHUB_REF}" || true - name: Save ccache + if: steps.guard.outputs.identical != 'true' continue-on-error: true uses: actions/cache/save@v5 with: @@ -1293,9 +1372,11 @@ jobs: key: ${{ runner.os }}-ubuntu26-cpu-test-${{ github.job }}-ccache-v1 - name: Prepare packaged test metadata + if: steps.guard.outputs.identical != 'true' run: python3 scripts/ci/prepare_test_runtime.py "${BUILD_DIR}" - name: Package Linux split runtime + if: steps.guard.outputs.identical != 'true' run: | mkdir -p ci-artifacts mapfile -t SHARED_LIBS < <(find "${BUILD_DIR}" -type f \( -name '*.so' -o -name '*.so.*' \) -print | sort) @@ -1313,6 +1394,7 @@ jobs: "${SHARED_LIBS[@]}" - name: Upload Linux split runtime + if: steps.guard.outputs.identical != 'true' uses: actions/upload-artifact@v7 with: name: mobilegl-linux-runtime-split @@ -1322,12 +1404,14 @@ jobs: # The retrace legs load only the library, and the server image on the spawn arm, so they # take this instead of the whole test runtime. - name: Package retrace library + if: steps.guard.outputs.identical != 'true' run: | files=("${BUILD_DIR}/.ci-source-root" "${BUILD_DIR}/libMobileGL.so") if [ -f "${BUILD_DIR}/libMobileGLServer.so" ]; then files+=("${BUILD_DIR}/libMobileGLServer.so"); fi tar -czf ci-artifacts/mobilegl-linux-lib-split.tgz "${files[@]}" - name: Upload retrace library + if: steps.guard.outputs.identical != 'true' uses: actions/upload-artifact@v7 with: name: mobilegl-linux-lib-split @@ -1339,16 +1423,39 @@ jobs: integration-split: runs-on: ubuntu-26.04 timeout-minutes: 120 - needs: [build-linux-split] + needs: [build-linux-split, build-linux] + if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && (needs.build-linux-split.outputs.reuse-primary != 'true' || needs.build-linux.result == 'success') }} # Short-lived test processes skip the glslang prewarm, as in the test job. env: MOBILEGL_DISABLE_SHADER_PREWARM: "1" + SPLIT_REUSE: ${{ needs.build-linux-split.outputs.reuse-primary }} + SPLIT_BUILD_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }} + SPLIT_RUNTIME: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'mobilegl-linux-runtime' || 'mobilegl-linux-runtime-split' }} + MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }} steps: - name: Checkout repo uses: actions/checkout@v6 + # The two build guards must agree. build-linux-split skips its compile on its own guard's + # word; the artifacts these lanes then take are build-linux's, so build-linux's guard must + # have proved the same equality on the runner that built them. + - name: Split runtime agreement + env: + PRIMARY_IDENTICAL: ${{ needs.build-linux.outputs.split-identical }} + PRIMARY_RATCHET: ${{ needs.build-linux.outputs.link-ratchet }} + run: | + if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_IDENTICAL}" != "true" ]; then + echo "::error::build-linux-split skipped its build, but build-linux's guard did not prove the two configurations equal (split-identical='${PRIMARY_IDENTICAL}'): the runners disagree, so these lanes have no split runtime they can trust" + exit 1 + fi + if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_RATCHET}" != "success" ]; then + echo "::error::the server link-closure ratchet ran in build-linux for this reused build and did not pass (outcome '${PRIMARY_RATCHET}'); its link-ratchet artifact names the symbols" + exit 1 + fi + echo "split runtime: ${SPLIT_BUILD_DIR} (reuse of build-linux: ${SPLIT_REUSE})" + - name: Configure runner environment uses: ./.github/actions/setup-test-runner with: @@ -1370,13 +1477,17 @@ jobs: - name: Download Linux split runtime uses: actions/download-artifact@v8 with: - name: mobilegl-linux-runtime-split + name: ${{ env.SPLIT_RUNTIME }} path: . - name: Unpack Linux split runtime run: | - tar --zstd -xf mobilegl-linux-runtime-split.tar.zst - test -f build-split/libMobileGL.so + tar --zstd -xf "${SPLIT_RUNTIME}.tar.zst" + test -f "${SPLIT_BUILD_DIR}/libMobileGL.so" + if [ "${SPLIT_REUSE}" = "true" ]; then + python3 -c 'import json, sys; mode = json.load(open(sys.argv[1]))["mode"]; sys.exit(0 if mode == "disaggregated" else f"build-linux runtime mode is {mode}, not disaggregated")' \ + "${SPLIT_BUILD_DIR}/runtime-mode.json" + fi - name: Prepare downloaded test metadata run: | @@ -1385,7 +1496,7 @@ jobs: done - name: Spawn/split lane parity (9.2) - run: python3 scripts/ci/spawn_lane_parity.py build-split + run: python3 scripts/ci/spawn_lane_parity.py "${SPLIT_BUILD_DIR}" # P8-A (ID-P8-2): every monolith case, tails normalised, is on a gated split arm or is named # with a reason class in scripts/data/split_coverage_exemptions.txt. The same `ctest -N` @@ -1394,7 +1505,7 @@ jobs: - name: Split coverage against monolith (P8-A) run: | python3 scripts/ci/split_coverage.py --self-test - python3 scripts/ci/split_coverage.py build-split + python3 scripts/ci/split_coverage.py "${SPLIT_BUILD_DIR}" # P8-SE: PipeCatalogueTest's unmigrated-emulation list equals the MGPipeUnmigratedEmulation # call sites in the tree, one site per name. A source lint: no build needed. @@ -1403,8 +1514,11 @@ jobs: python3 scripts/ci/unmigrated_emulation_sites.py --self-test python3 scripts/ci/unmigrated_emulation_sites.py + # The test job runs the same unit lane, with the same REQUIRE_GPU, on this same build when + # build-linux-split was reused; the strict unit step in integration-split-strict always runs. - name: Unit tests on the split runtime - working-directory: build-split + if: env.SPLIT_REUSE != 'true' + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-split.xml" @@ -1429,7 +1543,7 @@ jobs: # the reason and never goes green - MG_IntegrationTest/CMakeLists.txt probes MG_Remote for # c0's signature stubs and disarms the lane while any remain. - name: Split lanes (inproc, spawn, TCP) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" run: | @@ -1453,14 +1567,63 @@ jobs: python3 ../scripts/ci/junit_tally.py "${RUNNER_TEMP}/tcp-baseline.xml" --require-tcp-ran --require-run-ahead --discovery "${RUNNER_TEMP}/tcp-discovery.json" python3 ../scripts/ci/junit_tally.py "${RUNNER_TEMP}/tcp-baseline.xml" --require-entry-passed TcpLane.SupervisorProtocolControls --require-entry-passed TcpLane.EventForfeitPeer --require-entry-passed TcpLane.ControlFrameFuzz + # THE RUNTIME HALF OF "THIS IS REALLY A SPLIT BUILD". The build-level nm check in + # build-linux-split proves the library CARRIES MG_Remote; this proves the transport + # RESOLVED in a process of this lane. ConfigLoader::InitTransport logs one INFO line when it + # selects InProcess, and the DirectGLES.Split.PersistentMapArm. entry retains its private + # counting log (all Split entries now have private paths). It runs right after the lane, so + # it reads the lane's own run rather than a later step's rerun of the same entry. + # The line is written during bring-up, before any scenario decides to skip, so this + # check is live from the day the lanes land rather than from the day they stop skipping. + - name: Transport and default-knob resolution check + id: resolution + working-directory: ${{ env.SPLIT_BUILD_DIR }} + run: | + # P6: the sink writes one file per ROLE from the base MOBILEGL_LOG_FILE_PATH; the + # transport-resolution line is ConfigLoader's, on the CLIENT thread, so it is in the + # .client half. The base name itself names no file now. + log=MobileGL/MG_IntegrationTest/persistent-map-arm-split-DirectGLES.client.log + if [ ! -f "${log}" ]; then + echo "::error::${log} does not exist: the DirectGLES.Split.PersistentMapArm. entry never ran (or the per-role split renamed it), so nothing in this job establishes that MOBILEGL_TRANSPORT ever resolved to inproc in a live process" + exit 1 + fi + # THE DISTINCTIVE PART OF THE INFO LINE, not the bare KEY=VALUE (review finding M-5): + # ConfigLoader logs `Config: Accepted env variable: MOBILEGL_TRANSPORT=inproc` for the + # env dump too, unconditionally and in a PULL build, and at DEBUG that line is live. + if ! grep -q "MOBILEGL_TRANSPORT=inproc - the MGPipe record stream" "${log}"; then + echo "::error::${log} carries no transport-resolution line. ConfigLoader::InitTransport logs it at INFO when it selects InProcess, and that code exists only in a MOBILEGL_BUILD_DISAGGREGATED build - so this lane ran a monolith library while claiming to be the split lane. (A bare MOBILEGL_TRANSPORT=inproc substring is NOT accepted: the env dump prints one in every build.)" + exit 1 + fi + echo "the split lane resolved MOBILEGL_TRANSPORT=inproc" + # The IPC knobs every passed entry resolved, from its own client log: the defaults the + # lane relies on (run-ahead and the role-split state on, strict off) unless the entry + # pins its own. Proven defaults make Transport A/B below a repeat of the integration + # job's two legs; exit 3 says a default changed, and A/B then runs. + ctest -L integration-split --show-only=json-v1 > "${RUNNER_TEMP}/split-lane.json" + rc=0 + python3 "${GITHUB_WORKSPACE}/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py" ipc-config \ + "${RUNNER_TEMP}/split-lane.json" "${RUNNER_TEMP}/split-baseline.xml" "${RUNNER_TEMP}/split-ipc.json" \ + strict=0 role-split-state=1 run-ahead=1 --assumed role-split-state,run-ahead || rc=$? + if [ "${rc}" -eq 0 ]; then + echo "defaults-on=true" >> "${GITHUB_OUTPUT}" + elif [ "${rc}" -eq 3 ]; then + echo "::warning::an IPC default differs from what the lanes assume (split-ipc.json names the entries), so Transport A/B runs" + echo "defaults-on=false" >> "${GITHUB_OUTPUT}" + else + exit "${rc}" + fi + # Both full transport runs are hard gates. The per-case comparison below # remains diagnostic because explicit mechanism controls retain their own mode. + # With build-linux reused and the defaults proven above, the integration job's two legs + # already ran these entries on this library in these configurations, so this is skipped. # # The entries that name MOBILEGL_TRANSPORT in their OWN property (the Split. lanes) keep # their value in both passes, which is correct: they are the split family in both arms and # the comparison is about the other 1100. - name: Transport A/B (monolith vs inproc) - working-directory: build-split + if: env.SPLIT_REUSE != 'true' || steps.resolution.outputs.defaults-on != 'true' + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_ITRP_FIX: "ForceOn" @@ -1497,33 +1660,6 @@ jobs: print(f"the two arms agree on all {len(a)} entries, name and status") PY - # THE RUNTIME HALF OF "THIS IS REALLY A SPLIT BUILD". The build-level nm check in - # build-linux-split proves the library CARRIES MG_Remote; this proves the transport - # RESOLVED in a process of this lane. ConfigLoader::InitTransport logs one INFO line when it - # selects InProcess, and the DirectGLES.Split.PersistentMapArm. entry retains its private - # counting log (all Split entries now have private paths; nothing else writes this file). - # The line is written during bring-up, before any scenario decides to skip, so this - # check is live from the day the lanes land rather than from the day they stop skipping. - - name: Transport resolution check - working-directory: build-split - run: | - # P6: the sink writes one file per ROLE from the base MOBILEGL_LOG_FILE_PATH; the - # transport-resolution line is ConfigLoader's, on the CLIENT thread, so it is in the - # .client half. The base name itself names no file now. - log=MobileGL/MG_IntegrationTest/persistent-map-arm-split-DirectGLES.client.log - if [ ! -f "${log}" ]; then - echo "::error::${log} does not exist: the DirectGLES.Split.PersistentMapArm. entry never ran (or the per-role split renamed it), so nothing in this job establishes that MOBILEGL_TRANSPORT ever resolved to inproc in a live process" - exit 1 - fi - # THE DISTINCTIVE PART OF THE INFO LINE, not the bare KEY=VALUE (review finding M-5): - # ConfigLoader logs `Config: Accepted env variable: MOBILEGL_TRANSPORT=inproc` for the - # env dump too, unconditionally and in a PULL build, and at DEBUG that line is live. - if ! grep -q "MOBILEGL_TRANSPORT=inproc - the MGPipe record stream" "${log}"; then - echo "::error::${log} carries no transport-resolution line. ConfigLoader::InitTransport logs it at INFO when it selects InProcess, and that code exists only in a MOBILEGL_BUILD_DISAGGREGATED build - so this lane ran a monolith library while claiming to be the split lane. (A bare MOBILEGL_TRANSPORT=inproc substring is NOT accepted: the env dump prints one in every build.)" - exit 1 - fi - echo "the split lane resolved MOBILEGL_TRANSPORT=inproc" - # Negative controls for the verb barrier (MOBILEGL_IPC_VERB_BARRIER=0) and the persistent-map # push (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0): each knob must turn its selected entries red. # The body is scripts/ci/split_negative_controls.sh, so scripts/ci/control_smoke_test.sh can @@ -1532,7 +1668,7 @@ jobs: # and accepts a red only when it carries the control's own diagnostic. - name: Negative controls (barrier, map push) if: ${{ !cancelled() }} - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" CONTROL_TMPDIR: ${{ runner.temp }} @@ -1551,8 +1687,8 @@ jobs: with: name: integration-split-logs path: | - build-split/MobileGL/MG_IntegrationTest/*.log* - build-split/MobileGL/MG_IntegrationTest/split-logs/*.log + ${{ env.SPLIT_BUILD_DIR }}/MobileGL/MG_IntegrationTest/*.log* + ${{ env.SPLIT_BUILD_DIR }}/MobileGL/MG_IntegrationTest/split-logs/*.log ${{ runner.temp }}/arm-inproc.xml ${{ runner.temp }}/arm-monolith.xml if-no-files-found: warn @@ -1582,16 +1718,35 @@ jobs: integration-split-strict: runs-on: ubuntu-26.04 timeout-minutes: 60 - needs: [build-linux-split] + needs: [build-linux-split, build-linux] + if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && (needs.build-linux-split.outputs.reuse-primary != 'true' || needs.build-linux.result == 'success') }} # Short-lived test processes skip the glslang prewarm, as in the test job. env: MOBILEGL_DISABLE_SHADER_PREWARM: "1" + SPLIT_REUSE: ${{ needs.build-linux-split.outputs.reuse-primary }} + SPLIT_BUILD_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }} + SPLIT_RUNTIME: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'mobilegl-linux-runtime' || 'mobilegl-linux-runtime-split' }} + MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }} steps: - name: Checkout repo uses: actions/checkout@v6 + # The two build guards must agree. build-linux-split skips its compile on its own guard's + # word; the artifacts these lanes then take are build-linux's, so build-linux's guard must + # have proved the same equality on the runner that built them. + - name: Split runtime agreement + env: + PRIMARY_IDENTICAL: ${{ needs.build-linux.outputs.split-identical }} + PRIMARY_RATCHET: ${{ needs.build-linux.outputs.link-ratchet }} + run: | + if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_IDENTICAL}" != "true" ]; then + echo "::error::build-linux-split skipped its build, but build-linux's guard did not prove the two configurations equal (split-identical='${PRIMARY_IDENTICAL}'): the runners disagree, so these lanes have no split runtime they can trust" + exit 1 + fi + echo "split runtime: ${SPLIT_BUILD_DIR} (reuse of build-linux: ${SPLIT_REUSE})" + - name: Configure runner environment uses: ./.github/actions/setup-test-runner with: @@ -1613,13 +1768,17 @@ jobs: - name: Download Linux split runtime uses: actions/download-artifact@v8 with: - name: mobilegl-linux-runtime-split + name: ${{ env.SPLIT_RUNTIME }} path: . - name: Unpack Linux split runtime run: | - tar --zstd -xf mobilegl-linux-runtime-split.tar.zst - test -f build-split/libMobileGL.so + tar --zstd -xf "${SPLIT_RUNTIME}.tar.zst" + test -f "${SPLIT_BUILD_DIR}/libMobileGL.so" + if [ "${SPLIT_REUSE}" = "true" ]; then + python3 -c 'import json, sys; mode = json.load(open(sys.argv[1]))["mode"]; sys.exit(0 if mode == "disaggregated" else f"build-linux runtime mode is {mode}, not disaggregated")' \ + "${SPLIT_BUILD_DIR}/runtime-mode.json" + fi - name: Prepare downloaded test metadata run: | @@ -1628,7 +1787,7 @@ jobs: done - name: Unit tests on the split runtime (strict) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_IPC_STRICT_ERRORS: "1" @@ -1640,7 +1799,7 @@ jobs: # admitted (ARCHITECTURE.md §9.2.1). The lane must be green, and its markers must match the # admitted list and strict-expected-markers.txt in both directions. - name: Strict split lane - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_IPC_STRICT_ERRORS: "1" @@ -1723,7 +1882,7 @@ jobs: # These entries own assertions removed from generic transport registrations. # Discovery, execution and zero-skip accounting must agree on the exact set. - name: Specialized configuration controls - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" run: | @@ -1744,7 +1903,7 @@ jobs: # debt somebody now owes, a vanished pair must be removed in the commit that retired it. # With the census empty, this lane is a hard green gate. - name: Dual-block census lane - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_TRANSPORT: "inproc" @@ -1789,7 +1948,7 @@ jobs: # The per-frame zero-residual-read case must execute and pass on both backends. - name: P5f per-frame zero-residual check - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_IPC_ROLE_SPLIT_STATE: "1" @@ -1806,7 +1965,7 @@ jobs: # The mechanism's own negative control (P5F §6): the same named case must be green with # the knob on and red - on its own assertion text - with it off. - name: Dual-block negative control (P5f f1) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} run: bash "${GITHUB_WORKSPACE}/scripts/ci/dualblock_negative_control.sh" # ---- MAGMA'S OWN WIRE LANE: FRAMEBUFFER, TEXTURE AND APPLICATION BUFFERS ------------ @@ -1814,7 +1973,7 @@ jobs: # P5f fm: Magma's framebuffer, copy and mipmap verbs consume server records. # The label remains separate so both backends' coverage stays explicit. - name: Magma split lane (strict, dual-block) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_IPC_STRICT_ERRORS: "1" @@ -1830,7 +1989,7 @@ jobs: # integration-magma-all-{split,spawn,tcp}, is not gated: it may go red while CI stays green, # and a case moves into a gating step once its blocker is fixed. - name: Magma spawn and TCP lanes - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_IPC_STRICT_ERRORS: "1" @@ -1842,7 +2001,7 @@ jobs: # Every named Magma application-buffer wire case must execute and pass. - name: Magma buffer wire cases (inproc) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} run: | ctest -L integration-magma-buffers --show-only=json-v1 > "${RUNNER_TEMP}/magma-buffer-lane.json" ctest -L integration-magma-buffers --no-tests=error -j 2 --output-on-failure \ @@ -1902,7 +2061,7 @@ jobs: # fixture entries under the label as well - and they must be green too: a lane whose # supervisor failed to start has nothing to say about the twenty-one. - name: Magma buffer wire cases (spawn, TCP) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" run: | @@ -1959,7 +2118,7 @@ jobs: # Run-ahead must queue real GPU work and enforce credits 1 and 3. - name: Magma run-ahead cases (credits 1, 3) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} run: | ctest -L integration-magma-runahead --show-only=json-v1 > "${RUNNER_TEMP}/magma-runahead-lane.json" ctest -L integration-magma-runahead --no-tests=error -j 2 --output-on-failure \ @@ -1986,17 +2145,17 @@ jobs: # Run-ahead off must go red on the lost queue lead; run-ahead back on must be green. - name: Magma run-ahead negative control run: | - python3 scripts/ci/magma_runahead_checks.py negative --build-dir build-split \ + python3 scripts/ci/magma_runahead_checks.py negative --build-dir "${SPLIT_BUILD_DIR}" \ --out "${RUNNER_TEMP}/magma-runahead-negative" --execute - name: Magma run-ahead sync validation run: | - python3 scripts/ci/magma_runahead_checks.py validation --build-dir build-split \ + python3 scripts/ci/magma_runahead_checks.py validation --build-dir "${SPLIT_BUILD_DIR}" \ --out "${RUNNER_TEMP}/magma-runahead-syncval" --execute - name: Magma view/framebuffer cache sync validation run: | - python3 scripts/ci/magma_cache_checks.py --build-dir build-split \ + python3 scripts/ci/magma_cache_checks.py --build-dir "${SPLIT_BUILD_DIR}" \ --out "${RUNNER_TEMP}/magma-cache-syncval" --execute - name: Upload Magma run-ahead and cache evidence @@ -2016,7 +2175,7 @@ jobs: # evidence behind it: the case must EXECUTE, PASS, and never skip - a lane-level exit code # alone would not separate "generated" from "quietly absent". - name: Magma packed-float mipmap case (P7) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_IPC_STRICT_ERRORS: "1" @@ -2039,7 +2198,7 @@ jobs: # Client vertex/index inputs are copied into owned wire buffer resources. # The original dedicated entries now have to execute successfully with run-ahead. - name: Client-array split lane (run-ahead) - working-directory: build-split + working-directory: ${{ env.SPLIT_BUILD_DIR }} env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_TRANSPORT: "inproc" @@ -2083,8 +2242,8 @@ jobs: with: name: integration-split-strict-logs path: | - build-split/MobileGL/MG_IntegrationTest/*.log* - build-split/MobileGL/MG_IntegrationTest/split-logs/*.log + ${{ env.SPLIT_BUILD_DIR }}/MobileGL/MG_IntegrationTest/*.log* + ${{ env.SPLIT_BUILD_DIR }}/MobileGL/MG_IntegrationTest/split-logs/*.log ${{ runner.temp }}/specialized*.json ${{ runner.temp }}/specialized-tests.txt ${{ runner.temp }}/specialized.xml @@ -2916,10 +3075,16 @@ jobs: # The setup action selects lavapipe on GitHub-hosted runners. needs: - build-linux-split + - build-linux - build-retrace - trace-cases - trace-fixtures - if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' }} + if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' && (needs.build-linux-split.outputs.reuse-primary != 'true' || needs.build-linux.result == 'success') }} + env: + SPLIT_REUSE: ${{ needs.build-linux-split.outputs.reuse-primary }} + SPLIT_LIB: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'mobilegl-linux-lib' || 'mobilegl-linux-lib-split' }} + SPLIT_LIB_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }} + SPLIT_BUILD_DIR: ${{ needs.build-linux-split.outputs.reuse-primary == 'true' && 'build-linux' || 'build-split' }} strategy: fail-fast: false max-parallel: 8 @@ -2935,6 +3100,20 @@ jobs: - name: Checkout repo uses: actions/checkout@v6 + # The two build guards must agree. build-linux-split skips its compile on its own guard's + # word; the artifacts these lanes then take are build-linux's, so build-linux's guard must + # have proved the same equality on the runner that built them. + - name: Split runtime agreement + env: + PRIMARY_IDENTICAL: ${{ needs.build-linux.outputs.split-identical }} + PRIMARY_RATCHET: ${{ needs.build-linux.outputs.link-ratchet }} + run: | + if [ "${SPLIT_REUSE}" = "true" ] && [ "${PRIMARY_IDENTICAL}" != "true" ]; then + echo "::error::build-linux-split skipped its build, but build-linux's guard did not prove the two configurations equal (split-identical='${PRIMARY_IDENTICAL}'): the runners disagree, so these lanes have no split runtime they can trust" + exit 1 + fi + echo "split runtime: ${SPLIT_BUILD_DIR} (reuse of build-linux: ${SPLIT_REUSE})" + - name: Configure runner environment uses: ./.github/actions/setup-test-runner with: @@ -2969,7 +3148,7 @@ jobs: - name: Download Linux split library uses: actions/download-artifact@v8 with: - name: mobilegl-linux-lib-split + name: ${{ env.SPLIT_LIB }} path: . - name: Download trace replay @@ -2983,12 +3162,14 @@ jobs: # /build-linux/libMobileGL.so frozen into every case, so the swap happens here # rather than through a variable, exactly as in retrace-verify. `nm`, not `nm -D`. run: | - tar -xzf mobilegl-linux-lib-split.tgz + tar -xzf "${SPLIT_LIB}.tgz" tar -xzf mobilegl-trace-replay.tgz - test -f build-split/libMobileGL.so + test -f "${SPLIT_LIB_DIR}/libMobileGL.so" test -f build-retrace/tools/trace_replay/mobilegl_trace_replay - mkdir -p build-linux - cp build-split/libMobileGL.so build-linux/libMobileGL.so + if [ "${SPLIT_LIB_DIR}" != build-linux ]; then + mkdir -p build-linux + cp "${SPLIT_LIB_DIR}/libMobileGL.so" build-linux/libMobileGL.so + fi if ! nm --defined-only build-linux/libMobileGL.so | grep -q -i MG_Remote; then echo "::error::the library unpacked at build-linux/libMobileGL.so defines no MG_Remote symbol, so this retrace would replay against a monolith build, ignore MOBILEGL_TRANSPORT entirely and match its golden having split nothing" exit 1 @@ -3004,17 +3185,19 @@ jobs: # THE SPAWN ARM NEEDS A SECOND BINARY and the inproc arm needs none, which is the whole # difference between the two at this level: inproc's server role is a thread in the replay # process, spawn's is a PROCESS the client launches from this image. It ships in - # build-linux-split's retrace library - libMobileGLServer.so is an executable that wears a + # the split retrace library - libMobileGLServer.so is an executable that wears a # .so name so an APK will install it somewhere exec-able - and a missing one must be a NAMED # failure here rather than a LaunchServer refusal 78 jobs deep. - name: Place the server image (spawn arm only) if: ${{ matrix.transport == 'spawn' }} run: | - if [ ! -f build-split/libMobileGLServer.so ]; then + if [ ! -f "${SPLIT_LIB_DIR}/libMobileGLServer.so" ]; then echo "::error::the split runtime carries no libMobileGLServer.so, so transport=spawn has no server to launch. ConfigLoader would resolve Spawn, LaunchServer would refuse BY NAME, and this job would red on a message about an unresolvable image rather than about a packaging gap." exit 1 fi - cp build-split/libMobileGLServer.so build-linux/libMobileGLServer.so + if [ "${SPLIT_LIB_DIR}" != build-linux ]; then + cp "${SPLIT_LIB_DIR}/libMobileGLServer.so" build-linux/libMobileGLServer.so + fi chmod +x build-linux/libMobileGLServer.so echo "server image: $(readlink -f build-linux/libMobileGLServer.so)" @@ -3351,7 +3534,6 @@ jobs: - name: Test CTest runtime metadata preparation run: python3 scripts/ci/test_prepare_test_runtime.py - - name: Regenerate the MGPipe interface (G1-G7) run: | python3 scripts/gen_pipe.py @@ -3447,6 +3629,7 @@ jobs: python3 tools/trace_replay/test_compare_actuals.py python3 tools/trace_replay/test_run_trace_case.py python3 scripts/ci/build_fingerprint.py --self-test + python3 MobileGL/MG_IntegrationTest/Harness/test_split_log_paths.py # Warning only for now: the disaggregation documents are still being written, and a # lint that fails a rewrite in progress teaches people to ignore it. It becomes diff --git a/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py index 064c2762..96c9d726 100644 --- a/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py +++ b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py @@ -306,6 +306,72 @@ def require_green(document, junit_path, required): print(f"SplitLogPaths require-green: {len(by_name)} PASS, 0 skip, 0 failed, complete discovery") +# The client's one-line IPC configuration (ConfigLoader, every non-monolith transport, INFO) and +# the knobs an entry may pin in its own ENVIRONMENT instead of inheriting the lane's. +IPC_LINE = "Config: IPC " +IPC_PINS = {"strict": "MOBILEGL_IPC_STRICT_ERRORS", "role-split-state": "MOBILEGL_IPC_ROLE_SPLIT_STATE", + "run-ahead": "MOBILEGL_IPC_RUN_AHEAD"} + + +def entry_environment(test): + env = {} + for prop in test.get("properties", []): + if prop.get("name") == "ENVIRONMENT": + for item in prop.get("value") or []: + key, _, value = item.partition("=") + env[key] = value + return env + + +def ipc_config(document, junit_path, expected, assumed=()): + """Prove, from each passed entry's own client log, the IPC knobs the run resolved. + + Every entry that passed and has a private log must show `key=value` on its last "Config: IPC" + line, where value is the entry's own ENVIRONMENT pin when it has one and the expected value + otherwise; monolith-pinned entries have no such line and are left out. Returns (code, summary): + 0 proven; 3 when the only mismatches are on `assumed` keys of entries that do not pin them (a + default that changed); 1 for anything else, including no evidence at all.""" + passed = set() + for case in ET.parse(junit_path).getroot().iter("testcase"): + failed = case.find("failure") is not None or case.find("error") is not None + skipped = case.find("skipped") is not None or case.get("status") in ("notrun", "disabled") + if not failed and not skipped: + passed.add(case.get("name")) + problems, defaults, checked = [], [], [] + for test in document.get("tests", []): + name = test["name"] + env = entry_environment(test) + transport = env.get("MOBILEGL_TRANSPORT", "") + base = env.get("MOBILEGL_LOG_FILE_PATH", "") + if name not in passed or transport == "monolith" or not base: + continue + client = Path(role_paths(base)[0]) + lines = [line for line in (client.read_text(errors="replace") if client.is_file() else "").splitlines() + if IPC_LINE in line] + if not lines: + if transport == "inproc": + problems.append(f"{name}: passed under inproc but {client} carries no '{IPC_LINE.strip()}' line") + continue + checked.append(name) + for key, value in expected.items(): + want = env.get(IPC_PINS.get(key, ""), value) + found = re.search(r"(?:^| )" + re.escape(key) + r"=(\S*)", lines[-1]) + got = found.group(1) if found else "" + if got == want: + continue + row = f"{name}: {key}={got}, expected {want}" + if key in assumed and IPC_PINS.get(key) not in env: + defaults.append(row) + else: + problems.append(row) + if not checked: + problems.append("no passed entry left a 'Config: IPC' line, so nothing proves the configuration") + summary = {"checked": len(checked), "expected": expected, "assumed": sorted(assumed), + "problems": problems, "default_mismatches": defaults} + code = 1 if problems else 3 if defaults else 0 + return code, summary + + VERIFY_ARMED_LINE = "MGPipe: verify armed" @@ -616,6 +682,23 @@ def main(): if mode == "require-green": require_green(json.loads(Path(sys.argv[2]).read_text()), sys.argv[3], sys.argv[4:]) return + if mode == "ipc-config": + # key=value... [--assumed k1,k2] + args = sys.argv[5:] + assumed = set() + if "--assumed" in args: + at = args.index("--assumed") + assumed = {key for key in args[at + 1].split(",") if key} + args = args[:at] + args[at + 2:] + expected = dict(arg.split("=", 1) for arg in args) + code, summary = ipc_config(json.loads(Path(sys.argv[2]).read_text()), sys.argv[3], expected, assumed) + Path(sys.argv[4]).write_text(json.dumps(summary, indent=1) + "\n") + for row in summary["problems"] + summary["default_mismatches"]: + print(f"SplitLogPaths ipc-config: {row}") + print(f"SplitLogPaths ipc-config: {summary['checked']} entr(ies) checked against " + f"{' '.join(f'{k}={v}' for k, v in expected.items())}: " + f"{['proven', '', '', 'a default differs'][code] if code != 1 else 'FAILED'}") + sys.exit(code) selected = paths(json.loads(Path(sys.argv[2]).read_text())) selected = {name: path for name, path in selected.items() if re.search(sys.argv[3], name)} # The SAME exclusion run_control hands ctest -E. Without it this helper and ctest disagree diff --git a/MobileGL/MG_IntegrationTest/Harness/test_split_log_paths.py b/MobileGL/MG_IntegrationTest/Harness/test_split_log_paths.py index 948793a2..3002fa9b 100644 --- a/MobileGL/MG_IntegrationTest/Harness/test_split_log_paths.py +++ b/MobileGL/MG_IntegrationTest/Harness/test_split_log_paths.py @@ -262,5 +262,79 @@ class VerifySplitArmProofTest(unittest.TestCase): self.proof() +class IpcConfigProofTest(unittest.TestCase): + """ipc-config reads each passed entry's own client log for the knobs the run resolved.""" + + LINE = "Config: IPC ring=8MiB run-ahead={ra} present-credit=2 strict={st} audit=0 role-split-state={rs} surface=x" + + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + self.xml = self.root / "run.xml" + + def tearDown(self): + self.temp.cleanup() + + def lane(self, rows): + """rows: (name, status, pins, log line or None).""" + tests, suite = [], ET.Element("testsuite") + for i, (name, status, pins, line) in enumerate(rows): + base = self.root / f"{i}.log" + if line is not None: + (self.root / f"{i}.client.log").write_text("Config: MOBILEGL_TRANSPORT=inproc\n" + line + "\n") + env = ["MOBILEGL_LOG_FILE_PATH=" + str(base)] + pins + tests.append({"name": name, "properties": [{"name": "ENVIRONMENT", "value": env}]}) + case = ET.SubElement(suite, "testcase", name=name, status="run") + if status == "skip": + ET.SubElement(case, "skipped") + if status == "fail": + ET.SubElement(case, "failure") + ET.ElementTree(suite).write(self.xml) + return {"tests": tests} + + def check(self, rows, expected=None, assumed=("role-split-state", "run-ahead")): + expected = expected or {"strict": "0", "role-split-state": "1", "run-ahead": "1"} + with contextlib.redirect_stdout(io.StringIO()): + return helper.ipc_config(self.lane(rows), self.xml, expected, set(assumed))[0] + + def inproc(self, ra="1", st="0", rs="1"): + return ["MOBILEGL_TRANSPORT=inproc"], self.LINE.format(ra=ra, st=st, rs=rs) + + def test_the_defaults_are_proven(self): + pins, line = self.inproc() + self.assertEqual(self.check([("A", "pass", pins, line), ("B", "skip", pins, None)]), 0) + + def test_a_changed_default_is_exit_three(self): + pins, line = self.inproc(rs="0") + self.assertEqual(self.check([("A", "pass", pins, line)]), 3) + + def test_an_unassumed_key_that_differs_is_red(self): + pins, line = self.inproc(st="1") + self.assertEqual(self.check([("A", "pass", pins, line)]), 1) + + def test_a_passed_inproc_entry_without_the_line_is_red(self): + pins, line = self.inproc() + self.assertEqual(self.check([("A", "pass", pins, line), ("B", "pass", pins, None)]), 1) + + def test_no_evidence_is_red(self): + pins, _ = self.inproc() + self.assertEqual(self.check([("A", "skip", pins, None)]), 1) + + def test_an_entry_pin_overrides_the_expectation(self): + pins, line = self.inproc(ra="0") + self.assertEqual(self.check([("A", "pass", pins + ["MOBILEGL_IPC_RUN_AHEAD=0"], line)]), 0) + self.assertEqual(self.check([("A", "pass", pins + ["MOBILEGL_IPC_RUN_AHEAD=1"], line)]), 1, + "a pinned value the run did not honour is red, not a changed default") + + def test_the_value_is_matched_whole(self): + pins, line = self.inproc(ra="10") + self.assertEqual(self.check([("A", "pass", pins, line)]), 3) + + def test_monolith_pinned_entries_are_left_out(self): + pins, line = self.inproc() + rows = [("A", "pass", pins, line), ("M", "pass", ["MOBILEGL_TRANSPORT=monolith"], None)] + self.assertEqual(self.check(rows), 0) + + if __name__ == "__main__": unittest.main() diff --git a/docs/ci/ci-cleanup-log.md b/docs/ci/ci-cleanup-log.md index 56208f28..98d9bce8 100644 --- a/docs/ci/ci-cleanup-log.md +++ b/docs/ci/ci-cleanup-log.md @@ -46,6 +46,9 @@ them wrong. | 2026-10-17 | `Negative control A (server-side read)` and `Negative control B (omitted stamp)` | Test / `integration-verify-split` | the first re-ran the two VerifySplitReadCorrupted entries the lane had just run and grepped what they already assert in-test (at least two where=read reports in the server half); the second re-ran with the pair the in-lane VerifySplitPoisonOmitted entries already pin, and accepted any red | `Verify-split control entries`: `--require-entry-passed` on VerifySplitArming, VerifySplitCorrupted, VerifySplitReadCorrupted and VerifySplitPoisonOmitted of both backends | | 2026-10-17 | the pre-flip arm of `Strict split lane` (red-by-design while `kMGPipeP5eRunAheadReady` was false) and the "GREEN with rows outstanding" rule of `Dual-block census lane` | Test / `integration-split-strict` | dead: the constant is true, and `dualblock-expected-fatals.txt` has no rows, so neither branch could run | the hard-green arm and its two-sided marker census, and the empty-census rule (rows == 0 requires ctest success), both kept | | 2026-10-17 | `scripts/ci/tcp_log_forwarding_control.py` | none | no caller outside the archived planning notes | none needed | +| 2026-10-17 | the compile of `build-linux-split` on the disaggregation branches (when its guard proves the configurations equal) | Test / `build-linux-split` | it built the library `build-linux` builds there, from a configuration that differs only by the benchmarks (211 paired runs never disagreed); it delayed every split consumer by up to 9 minutes | `build_fingerprint.py` guards in both build jobs (cache, compile commands, link statements, CTest files, toolchain; benchmarks excluded); the consumers take `build-linux`'s artifacts and red if `build-linux`'s own guard disagrees; the link ratchet runs in `build-linux`; on any difference `build-linux-split` builds as before, and dev is unchanged | +| 2026-10-17 | `Transport A/B (monolith vs inproc)` (skip-census lanes `arm-monolith`, `arm-inproc` now optional) | Test / `integration-split` | with `build-linux` reused it repeats the integration job's two legs on the same library (79 paired runs, no one-sided red) | the integration legs; `Transport and default-knob resolution check` now reads 'Split lanes' own run and proves, from every passed entry's client log, strict=0, role-split-state=1 and run-ahead=1 unless pinned (`split_log_paths.py ipc-config`); A/B still runs when a default differs or the build is not reused | +| 2026-10-17 | `Unit tests on the split runtime` (skip-census lane `unit-split` now optional) | Test / `integration-split` | with `build-linux` reused the test job runs the same unit lane on the same build (182 paired runs; the one recurring one-sided red was a fixed test race) | the test job, which now carries `MOBILEGL_ITEST_REQUIRE_GPU=1` and `libegl-mesa0` on the three branches; the strict unit step is unchanged | ## Kept after investigation diff --git a/scripts/data/skip_census_baseline.json b/scripts/data/skip_census_baseline.json index 14dd051f..f716c370 100644 --- a/scripts/data/skip_census_baseline.json +++ b/scripts/data/skip_census_baseline.json @@ -10544,7 +10544,8 @@ "DirectVulkan.ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero", "DirectVulkan.WireMipShapeScenario.Depth1DChainWithoutANativeBlitDeclinesByName" ], - "names_from": "junit-integration-inproc-acceptance-inproc/integration" + "names_from": "junit-integration-inproc-acceptance-inproc/integration", + "optional": "runs only when build-linux-split builds its own library, or the split lane's logs do not prove the IPC defaults" }, "junit-integration-split/arm-monolith": { "skipped": [ @@ -10972,7 +10973,8 @@ "DirectVulkan.WireIndirectDrawScenario.ShaderWrittenCommandsWithoutAnApplicationBarrierAreOrdered", "DirectVulkan.WireMipShapeScenario.Depth1DChainWithoutANativeBlitDeclinesByName" ], - "names_from": "junit-integration-inproc-acceptance-inproc/integration" + "names_from": "junit-integration-inproc-acceptance-inproc/integration", + "optional": "runs only when build-linux-split builds its own library, or the split lane's logs do not prove the IPC defaults" }, "junit-integration-split/control": { "skipped": [], @@ -12378,7 +12380,8 @@ "ResourceEmit.TheLiveHostWritesWireFiresOnTheCallAPersistentMapProducerWouldSetItOn", "XfbFrontendOrderInvarianceTest.DumpBSpirvForDisassembly" ], - "names_from": "junit-integration-split-strict/unit-strict" + "names_from": "junit-integration-split-strict/unit-strict", + "optional": "runs only when build-linux-split builds its own library; the test job runs the same unit lane on the reused build" }, "junit-integration-verify-split/verify-split": { "skipped": [