diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml index fcd2481c..c5b6c2eb 100644 --- a/.github/workflows/apk.yml +++ b/.github/workflows/apk.yml @@ -44,6 +44,9 @@ jobs: distribution: zulu java-version: '17' + - name: Android replay lifecycle regression + run: python3 tools/trace_replay/test_android_lifecycle.py + - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 with: @@ -275,6 +278,8 @@ jobs: name: trace case matrix runs-on: ubuntu-latest needs: build + env: + MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }} outputs: android: ${{ steps.trace-cases.outputs.android }} names: ${{ steps.trace-cases.outputs.names }} @@ -285,7 +290,12 @@ jobs: - name: Load trace cases id: trace-cases run: | - echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT" + python3 tools/trace_replay/trace_cases.py --self-test-transport-matrices + matrix_format=github-apk-matrix + if [ "${MOBILEGL_CI_DISAGGREGATED}" = "ON" ]; then + matrix_format=github-apk-transport-matrix + fi + echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format "${matrix_format}")" >> "$GITHUB_OUTPUT" echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT" trace-fixtures: @@ -398,7 +408,7 @@ jobs: --avd-name "${AVD_NAME}" retrace: - name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }}) + name: retrace (${{ matrix.backend.name }} / ${{ matrix.lane || 'monolith-control' }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }}) runs-on: ubuntu-latest needs: - build @@ -408,7 +418,8 @@ jobs: if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }} timeout-minutes: 75 env: - MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }} + MOBILEGL_CI_TRANSPORT: ${{ matrix.transport || 'monolith' }} + MOBILEGL_CI_LANE: ${{ matrix.lane || 'monolith-control' }} AVD_NAME: mobilegl-ci ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd ANDROID_HOME: ${{ github.workspace }}/.android/sdk @@ -508,11 +519,13 @@ jobs: apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk" test -f "${apk_file}" extra_retrace_args=() - if [ "${MOBILEGL_CI_DISAGGREGATED}" = "ON" ]; then + if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then extra_retrace_args+=( --env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1" --require-inproc ) + else + extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith") fi # Bliss needs the newer signed ANGLE variant plus sampler mipmap # min-filter downgrading on ANGLE llvmpipe. @@ -595,6 +608,12 @@ jobs: safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')" result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}" mkdir -p "${result_dir}" + python3 - "${result_dir}/lane.json" <<'PY' + import json, os, sys + from pathlib import Path + Path(sys.argv[1]).write_text(json.dumps({"lane": os.environ["MOBILEGL_CI_LANE"], + "transport": os.environ["MOBILEGL_CI_TRANSPORT"]}, indent=2)) + PY if [ -s "${{ matrix.case.golden }}" ]; then cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png" fi @@ -631,7 +650,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }} + name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }} path: android-retrace-result/** if-no-files-found: warn @@ -640,6 +659,8 @@ jobs: runs-on: ubuntu-latest needs: retrace if: always() + env: + MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }} steps: - name: Checkout repo @@ -654,25 +675,52 @@ jobs: with: node-version: '22' - - name: Download Android retrace results + - name: Download Android monolith control results uses: actions/download-artifact@v8 with: - pattern: MobileGL-android-retrace-result-* - path: retrace-artifacts + pattern: MobileGL-android-retrace-result-monolith-control-monolith-* + path: retrace-artifacts/monolith-control - - name: Render retrace summary + - name: Download Android inproc acceptance results + if: env.MOBILEGL_CI_DISAGGREGATED == 'ON' + uses: actions/download-artifact@v8 + with: + pattern: MobileGL-android-retrace-result-inproc-acceptance-inproc-* + path: retrace-artifacts/inproc-acceptance + + - name: Render monolith control summary run: | node tools/trace_replay/render_retrace_summary.mjs \ - --input retrace-artifacts \ - --output-dir android-retrace-summary \ - --title "MobileGL Android retrace overview" \ - --group-label "Android Emulator" \ - --html mobilegl-android-retrace-overview.html + --input retrace-artifacts/monolith-control \ + --output-dir android-retrace-summary/monolith-control \ + --title "MobileGL Android monolith control" \ + --group-label "Android Emulator / monolith control" \ + --html mobilegl-android-monolith-control.html - - name: Upload Android retrace summary + - name: Render inproc acceptance summary + if: env.MOBILEGL_CI_DISAGGREGATED == 'ON' + run: | + node tools/trace_replay/render_retrace_summary.mjs \ + --input retrace-artifacts/inproc-acceptance \ + --output-dir android-retrace-summary/inproc-acceptance \ + --title "MobileGL Android inproc acceptance" \ + --group-label "Android Emulator / inproc acceptance" \ + --html mobilegl-android-inproc-acceptance.html + + - name: Upload Android monolith control summary uses: actions/upload-artifact@v7 with: - path: android-retrace-summary/mobilegl-android-retrace-overview.html + name: MobileGL-android-monolith-control-summary-${{ github.sha }} + path: android-retrace-summary/monolith-control/mobilegl-android-monolith-control.html + archive: false + if-no-files-found: error + + - name: Upload Android inproc acceptance summary + if: env.MOBILEGL_CI_DISAGGREGATED == 'ON' + uses: actions/upload-artifact@v7 + with: + name: MobileGL-android-inproc-acceptance-summary-${{ github.sha }} + path: android-retrace-summary/inproc-acceptance/mobilegl-android-inproc-acceptance.html archive: false if-no-files-found: error diff --git a/build.gradle b/build.gradle index 9267f890..ca3cc67b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,14 @@ def mobileGlLogActiveLevel = { } def standalonePluginBuild = rootProject.name == 'MobileGLPlugin' +// Match the standalone app's declared ABI set. The library module otherwise +// configures unrelated NDK ABIs even when the app only packages these two. +// Embedded FCL builds retain their own ABI policy. +def mobileGlAbiFilters = { + def value = (rootProject.findProperty('mobilegl.abis') ?: System.getenv('MOBILEGL_ABIS') ?: 'arm64-v8a') as String + value.equalsIgnoreCase('all') ? ['arm64-v8a', 'x86_64'] : value.split(',').collect { it.trim() }.findAll { it } +} + // P0 spike A opt-in. OFF by default: the spike binary is dead weight in every trace APK // that is not running the spike, and a flavour that silently carries an extra executable // is exactly the kind of thing nobody notices until it ships. Turn it on for the spike @@ -34,6 +42,12 @@ android { defaultConfig { minSdk 26 + if (standalonePluginBuild) { + ndk { + abiFilters.addAll(mobileGlAbiFilters()) + } + } + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { diff --git a/tools/trace_replay/trace_cases.py b/tools/trace_replay/trace_cases.py index 204fa5b4..e2c8e105 100644 --- a/tools/trace_replay/trace_cases.py +++ b/tools/trace_replay/trace_cases.py @@ -217,6 +217,58 @@ def github_apk_matrix(cases): } +def github_transport_matrix(cases, *, apk=False): + """Run the entire original matrix as both control and inproc acceptance. + + The two transports use the same D/P artifact and each case's existing backend + list. The legacy split:true subset does not limit this main acceptance matrix. + Nested APK case/backend metadata is deliberately left untouched. + """ + build = github_apk_matrix if apk else github_test_matrix + if not cases: + raise ValueError("transport matrix needs at least one trace case") + return {"include": [ + {**row, "lane": lane, "transport": transport} + for selected, lane, transport in ( + (cases, "monolith-control", "monolith"), + (cases, "inproc-acceptance", "inproc"), + ) + for row in build(selected)["include"] + ]} + + +def self_test_transport_matrices(): + cases = ci_trace_cases(load_trace_cases()) + for apk in (False, True): + build = github_apk_matrix if apk else github_test_matrix + original = build(cases)["include"] + mixed = github_transport_matrix(cases, apk=apk)["include"] + controls = [row for row in mixed if row["transport"] == "monolith"] + acceptance = [row for row in mixed if row["transport"] == "inproc"] + strip_lane = lambda rows: [{key: value for key, value in row.items() + if key not in ("lane", "transport")} for row in rows] + assert strip_lane(controls) == original, "control lost original case/backend/parameters" + assert strip_lane(acceptance) == original, "acceptance lost original case/backend/parameters" + assert all(row["lane"] == "monolith-control" for row in controls) + assert all(row["lane"] == "inproc-acceptance" for row in acceptance) + identities = [(row["case"]["name"] if apk else row["case"], + row["backend"]["name"] if apk else row["backend"], row["transport"]) + for row in mixed] + assert len(identities) == len(set(identities)), "duplicate lane identity" + # A restricted case keeps exactly its original backend and golden data. + restricted = [{**cases[0], "split": False, "ci_backends": ["DirectGLES"]}] + rows = github_transport_matrix(restricted, apk=apk)["include"] + assert len(rows) == 2 and strip_lane(rows[:1]) == strip_lane(rows[1:]) + try: + github_transport_matrix([], apk=apk) + except ValueError: + pass + else: + raise AssertionError("an empty acceptance lane silently passed") + print(f"transport matrix {'APK' if apk else 'Linux'}: {len(controls)} unchanged controls, " + f"{len(acceptance)} full acceptance entries; exact metadata and negative controls OK") + + def cmake_quote(value): return '"' + str(value).replace("\\", "/").replace('"', '\\"') + '"' @@ -265,16 +317,19 @@ def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--case", dest="case_name", help="Trace case name.") parser.add_argument("--ci", action="store_true", help="Only include cases enabled for CI.") + parser.add_argument("--self-test-transport-matrices", action="store_true") parser.add_argument("--fixture-root", default="tools/trace_replay/fixtures") parser.add_argument( "--format", choices=( "names", "github-test-matrix", + "github-test-transport-matrix", "github-verify-matrix", "github-split-matrix", "github-apk", "github-apk-matrix", + "github-apk-transport-matrix", "fixture-files", "cmake", ), @@ -285,6 +340,9 @@ def parse_args(): def main(): args = parse_args() + if args.self_test_transport_matrices: + self_test_transport_matrices() + return 0 cases = load_trace_cases() if args.ci: cases = ci_trace_cases(cases) @@ -292,6 +350,8 @@ def main(): print(json.dumps([case["name"] for case in cases], separators=(",", ":"))) elif args.format == "github-test-matrix": print(json.dumps(github_test_matrix(cases), separators=(",", ":"))) + elif args.format == "github-test-transport-matrix": + print(json.dumps(github_transport_matrix(cases), separators=(",", ":"))) elif args.format == "github-verify-matrix": print(json.dumps(github_verify_matrix(cases), separators=(",", ":"))) elif args.format == "github-split-matrix": @@ -300,6 +360,8 @@ def main(): print(json.dumps([github_apk_case(case) for case in cases], separators=(",", ":"))) elif args.format == "github-apk-matrix": print(json.dumps(github_apk_matrix(cases), separators=(",", ":"))) + elif args.format == "github-apk-transport-matrix": + print(json.dumps(github_transport_matrix(cases, apk=True), separators=(",", ":"))) elif args.format == "fixture-files": if not args.case_name: print("--case is required for --format fixture-files", file=sys.stderr)