mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
- APK is the later of the two runs (64 against 38 min), so its jobs no longer leave runners idle
871 lines
38 KiB
YAML
871 lines
38 KiB
YAML
name: MobileGL APK
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- dev
|
|
- Feat/Backend-Direct-GLES
|
|
- Feat/Backend-Direct-Vulkan
|
|
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
|
|
# branch runs the full lane on every push so a phase's landing is not gated on
|
|
# someone remembering to dispatch the workflow by hand.
|
|
- feat/disaggregated
|
|
- codex/fix-github-ci
|
|
- cleanup
|
|
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
|
|
# workflow keeps running for docs: it hosts the doc citation lint.
|
|
paths-ignore:
|
|
- 'docs/**'
|
|
- '**/*.md'
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
|
|
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
|
|
|
|
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
|
|
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'push' && github.ref != 'refs/heads/dev' }}
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-26.04
|
|
permissions:
|
|
actions: write
|
|
contents: read
|
|
env:
|
|
CCACHE_BASEDIR: ${{ github.workspace }}
|
|
CCACHE_COMPRESS: "true"
|
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
|
CCACHE_MAXSIZE: 4G
|
|
CCACHE_NOHASHDIR: "true"
|
|
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
|
|
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
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: Set artifact metadata
|
|
run: |
|
|
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
|
|
|
|
- name: Set up JDK
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
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:
|
|
gradle-version: 8.10.2
|
|
|
|
- name: Restore ccache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
path: .ccache
|
|
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
|
restore-keys: |
|
|
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
|
|
|
- name: Install ccache
|
|
timeout-minutes: 15
|
|
run: |
|
|
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 update
|
|
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 install -y ccache
|
|
ccache --version
|
|
|
|
- name: Setup Android SDK
|
|
id: netsetup84
|
|
continue-on-error: true
|
|
timeout-minutes: 15
|
|
uses: android-actions/setup-android@v4
|
|
with:
|
|
accept-android-sdk-licenses: false
|
|
packages: platform-tools
|
|
cmdline-tools-version: 13114758
|
|
|
|
- name: Setup Android SDK (retry)
|
|
timeout-minutes: 15
|
|
if: ${{ steps.netsetup84.outcome == 'failure' }}
|
|
uses: android-actions/setup-android@v4
|
|
with:
|
|
accept-android-sdk-licenses: false
|
|
packages: platform-tools
|
|
cmdline-tools-version: 13114758
|
|
|
|
- name: Accept Android SDK licenses
|
|
run: yes | sdkmanager --licenses >/dev/null
|
|
|
|
- name: Install Android NDK
|
|
timeout-minutes: 30
|
|
run: |
|
|
for attempt in 1 2 3; do
|
|
sdkmanager "ndk;27.3.13750724" && break
|
|
[ "$attempt" = 3 ] && exit 1
|
|
echo "sdkmanager attempt $attempt failed; retrying"; sleep 20
|
|
done
|
|
echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties
|
|
|
|
- name: Update glslang external sources
|
|
working-directory: 3rdparty/glslang
|
|
run: python update_glslang_sources.py
|
|
|
|
- name: Build plugin APK
|
|
run: >-
|
|
gradle --no-daemon -p android-plugin :app:assemblePluginRelease
|
|
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
|
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
|
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
|
--parallel --max-workers "$(nproc)"
|
|
env:
|
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
|
|
|
- name: Download ANGLE x86_64 libraries
|
|
run: |
|
|
angle_dir="android-plugin/app/src/trace/jniLibs/x86_64"
|
|
rm -rf "${angle_dir}"
|
|
mkdir -p "${angle_dir}"
|
|
|
|
package_angle_variant() {
|
|
variant="$1"
|
|
commit="$2"
|
|
egl_sha="$3"
|
|
gles_sha="$4"
|
|
source_dir="${RUNNER_TEMP}/mobilegl-angle-${variant}"
|
|
base="https://raw.githubusercontent.com/FCL-Team/FoldCraftLauncher/${commit}/FCLauncher/src/main/jniLibs/x86_64"
|
|
mkdir -p "${source_dir}"
|
|
curl -L --fail --retry 3 -o "${source_dir}/libEGL_angle.so" "${base}/libEGL_angle.so"
|
|
curl -L --fail --retry 3 -o "${source_dir}/libGLESv2_angle.so" "${base}/libGLESv2_angle.so"
|
|
echo "${egl_sha} ${source_dir}/libEGL_angle.so" | sha256sum -c -
|
|
echo "${gles_sha} ${source_dir}/libGLESv2_angle.so" | sha256sum -c -
|
|
for library in libEGL_angle libGLESv2_angle; do
|
|
filename="${library}_${variant}.so"
|
|
cp "${source_dir}/${library}.so" "${angle_dir}/${filename}"
|
|
done
|
|
}
|
|
|
|
package_angle_variant \
|
|
ec889e6ea831 \
|
|
f2a3d510dffd8f6540a52e1a7d0c5787d151075b \
|
|
c41828768d089899fa058ec0bee711a91be88347f29bdb935223da6be1149c40 \
|
|
e4f820d99f94365c66df868c7740fef142fe5c0cd7c941790a9e30638857ca4d
|
|
package_angle_variant \
|
|
90a62123d794 \
|
|
bdcc96ac11c79001018ae4375eb73cb54a9f682f \
|
|
d0f4298ccc770cc801fc52e21733521646161e8a4adb3bd0052d9a1b57ee0ca8 \
|
|
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
|
|
|
|
- name: Build retrace APK
|
|
run: >-
|
|
gradle --no-daemon -p android-plugin :app:assembleTraceRelease
|
|
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all
|
|
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
|
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
|
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
|
--parallel --max-workers "$(nproc)"
|
|
env:
|
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
|
|
|
- name: Show ccache stats
|
|
if: always()
|
|
run: ccache --show-stats
|
|
|
|
# Rewrite one rolling entry per job and branch. The upload stays cumulative - it
|
|
# carries every object restored at the top of this run plus the few TUs that
|
|
# changed - but Actions cache keys are immutable, so this branch's superseded
|
|
# blob is released first; --ref keeps the delete off other branches' entries.
|
|
# Running after the build means a failed build leaves the entry untouched.
|
|
- name: Release superseded ccache entry
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
CACHE_KEY: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
|
run: gh cache delete "${CACHE_KEY}" --ref "${GITHUB_REF}" || true
|
|
|
|
- name: Save ccache
|
|
continue-on-error: true
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: .ccache
|
|
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
|
|
|
- name: Verify APK metadata and packaging
|
|
run: |
|
|
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
|
|
plugin_apk="android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk"
|
|
trace_apk="android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
|
test -f "${plugin_apk}"
|
|
test -f "${trace_apk}"
|
|
bash .github/scripts/validate-plugin-apks.sh "$AAPT2" "$plugin_apk" "$trace_apk"
|
|
|
|
- name: Verify signed APKs
|
|
run: |
|
|
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner -type f | sort -V | tail -n 1)"
|
|
mapfile -t APKS < <(printf '%s\n' \
|
|
"android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk" \
|
|
"android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk")
|
|
for APK in "${APKS[@]}"; do
|
|
if [[ ! -f "$APK" ]]; then
|
|
echo "::error::Expected release APK was not produced: $APK"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
for APK in "${APKS[@]}"; do
|
|
if [[ "$APK" == *-unsigned.apk ]]; then
|
|
echo "::error::Unsigned release APK produced: $APK"
|
|
exit 1
|
|
fi
|
|
"$APKSIGNER" verify --verbose "$APK"
|
|
done
|
|
|
|
- name: Upload plugin APK
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: MobileGL-plugin-${{ env.date_today }}-${{ github.sha }}
|
|
path: android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${{ github.sha }}.apk
|
|
archive: false
|
|
if-no-files-found: error
|
|
|
|
- name: Upload retrace APK
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: MobileGL-retrace-apk-${{ env.date_today }}-${{ github.sha }}
|
|
path: android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${{ github.sha }}.apk
|
|
archive: false
|
|
if-no-files-found: error
|
|
|
|
trace-cases:
|
|
name: trace case matrix
|
|
runs-on: ubuntu-26.04
|
|
env:
|
|
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
|
|
outputs:
|
|
android: ${{ steps.trace-cases.outputs.android }}
|
|
names: ${{ steps.trace-cases.outputs.names }}
|
|
steps:
|
|
- name: Checkout repo
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Load trace cases
|
|
id: trace-cases
|
|
run: |
|
|
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:
|
|
name: trace fixture (${{ matrix.case }})
|
|
runs-on: ubuntu-26.04
|
|
needs: trace-cases
|
|
strategy:
|
|
fail-fast: false
|
|
max-parallel: 4
|
|
matrix:
|
|
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
|
|
steps:
|
|
- name: Checkout repo
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Derive trace fixture cache key
|
|
id: fixture-key
|
|
run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'
|
|
|
|
- name: Restore trace fixture cache
|
|
id: fixture-cache
|
|
if: steps.fixture-key.outputs.cacheable == 'true'
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
path: ${{ steps.fixture-key.outputs.paths }}
|
|
key: ${{ steps.fixture-key.outputs.key }}
|
|
|
|
- name: Verify restored trace fixture
|
|
id: fixture-verify
|
|
if: steps.fixture-cache.outputs.cache-hit == 'true'
|
|
run: |
|
|
if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
|
|
echo "ok=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "ok=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
|
|
bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
|
|
fi
|
|
|
|
- name: Fetch trace fixture
|
|
if: steps.fixture-verify.outputs.ok != 'true'
|
|
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
|
|
|
|
- name: Save trace fixture cache
|
|
if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: ${{ steps.fixture-key.outputs.paths }}
|
|
key: ${{ steps.fixture-key.outputs.key }}
|
|
|
|
- name: Stage trace fixture
|
|
run: |
|
|
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
|
stage_dir="trace-fixtures/${safe_case}"
|
|
mkdir -p "${stage_dir}"
|
|
python3 tools/trace_replay/trace_cases.py --format fixture-files --case '${{ matrix.case }}' |
|
|
while IFS= read -r file; do
|
|
cp "${file}" "${stage_dir}/"
|
|
done
|
|
|
|
- name: Upload trace fixture
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: MobileGL-trace-fixture-${{ matrix.case }}
|
|
path: trace-fixtures/**
|
|
if-no-files-found: error
|
|
|
|
android-avd:
|
|
name: android avd image
|
|
runs-on: ubuntu-26.04
|
|
env:
|
|
AVD_NAME: mobilegl-ci
|
|
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
|
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
|
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
|
|
steps:
|
|
- name: Checkout repo
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Setup Android SDK
|
|
id: netsetup336
|
|
continue-on-error: true
|
|
timeout-minutes: 15
|
|
uses: android-actions/setup-android@v4
|
|
with:
|
|
accept-android-sdk-licenses: false
|
|
packages: platform-tools
|
|
cmdline-tools-version: 13114758
|
|
|
|
- name: Setup Android SDK (retry)
|
|
timeout-minutes: 15
|
|
if: ${{ steps.netsetup336.outcome == 'failure' }}
|
|
uses: android-actions/setup-android@v4
|
|
with:
|
|
accept-android-sdk-licenses: false
|
|
packages: platform-tools
|
|
cmdline-tools-version: 13114758
|
|
|
|
- name: Accept Android SDK licenses
|
|
run: yes | sdkmanager --licenses >/dev/null
|
|
|
|
- name: Restore Android AVD cache
|
|
id: android-avd-cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: |
|
|
${{ env.ANDROID_AVD_HOME }}
|
|
${{ env.ANDROID_SDK_ROOT }}/emulator
|
|
${{ env.ANDROID_SDK_ROOT }}/platform-tools
|
|
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
|
|
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
|
|
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v4-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
|
|
|
- name: Create AVD
|
|
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
sh android-plugin/run-avd-ci.sh create \
|
|
--api-level 35 \
|
|
--target google_apis \
|
|
--arch x86_64 \
|
|
--profile pixel_6 \
|
|
--avd-name "${AVD_NAME}"
|
|
|
|
- name: Enable KVM
|
|
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
sudo udevadm control --reload-rules
|
|
sudo udevadm trigger --name-match=kvm
|
|
|
|
# Boot the new AVD once so the cached AVD is past its first-boot setup (measured 37 s first
|
|
# boot, 26 s for the next cold boot). A quick-boot snapshot was tried instead: it doubled the
|
|
# cache restore and tripled "device offline" retries, so retrace jobs cold-boot. If the
|
|
# pre-boot fails, the AVD is created afresh so the cache never holds a half-booted one.
|
|
- name: Pre-boot AVD
|
|
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
log="${RUNNER_TEMP}/avd-preboot.log"
|
|
pid="${RUNNER_TEMP}/avd-preboot.pid"
|
|
if ! sh android-plugin/run-avd-ci.sh preboot --avd-name "${AVD_NAME}" --gpu software \
|
|
--emulator-log "${log}" --pid-file "${pid}" --boot-timeout 300; then
|
|
echo "::warning::the AVD pre-boot failed; caching a never-booted AVD instead"
|
|
tail -50 "${log}" || true
|
|
sh android-plugin/run-avd-ci.sh stop --avd-name "${AVD_NAME}" --emulator-log "${log}" --pid-file "${pid}"
|
|
timeout 60 tail --pid="$(cat "${pid}")" -f /dev/null || true
|
|
sh android-plugin/run-avd-ci.sh create --api-level 35 --target google_apis --arch x86_64 \
|
|
--profile pixel_6 --avd-name "${AVD_NAME}"
|
|
fi
|
|
du -sh "${ANDROID_AVD_HOME}"
|
|
|
|
retrace:
|
|
name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }})
|
|
runs-on: ubuntu-26.04
|
|
needs:
|
|
- build
|
|
- android-avd
|
|
- trace-cases
|
|
- trace-fixtures
|
|
if: ${{ !cancelled() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
|
|
timeout-minutes: 75
|
|
env:
|
|
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
|
|
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
|
|
strategy:
|
|
fail-fast: false
|
|
# The account runs at most 20 jobs at once, shared with Test. These ~230 emulator jobs are
|
|
# the longer of the two runs, so they may take the whole pool: what is minimised is when the
|
|
# later of APK and Test finishes, not Test alone.
|
|
max-parallel: 20
|
|
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
|
steps:
|
|
- name: Set swap space
|
|
uses: pierotofy/set-swap-space@v1.0
|
|
with:
|
|
swap-size-gb: 8
|
|
|
|
- name: Checkout repo
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Download trace fixture
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: MobileGL-trace-fixture-${{ matrix.case.name }}
|
|
path: trace-fixture-download
|
|
|
|
- name: Install trace fixture
|
|
run: |
|
|
mkdir -p tools/trace_replay/fixtures
|
|
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
|
|
|
|
- name: Set artifact metadata
|
|
run: |
|
|
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
|
|
echo "EMULATOR_LOG=${RUNNER_TEMP}/mobilegl-emulator.log" >> "$GITHUB_ENV"
|
|
echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"
|
|
|
|
- name: Restore Android AVD cache
|
|
id: android-avd-cache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
path: |
|
|
${{ env.ANDROID_AVD_HOME }}
|
|
${{ env.ANDROID_SDK_ROOT }}/emulator
|
|
${{ env.ANDROID_SDK_ROOT }}/platform-tools
|
|
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
|
|
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
|
|
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v4-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
|
|
|
# The cache holds everything a retrace job runs (emulator, adb, image, AVD); the SDK manager
|
|
# is only needed to create the AVD when the cache missed.
|
|
- name: Use the cached SDK tools
|
|
if: steps.android-avd-cache.outputs.cache-hit == 'true'
|
|
run: |
|
|
echo "${ANDROID_SDK_ROOT}/platform-tools" >> "$GITHUB_PATH"
|
|
echo "${ANDROID_SDK_ROOT}/emulator" >> "$GITHUB_PATH"
|
|
|
|
- name: Setup Android SDK
|
|
id: netsetup415
|
|
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
|
continue-on-error: true
|
|
timeout-minutes: 15
|
|
uses: android-actions/setup-android@v4
|
|
with:
|
|
accept-android-sdk-licenses: false
|
|
packages: platform-tools
|
|
cmdline-tools-version: 13114758
|
|
|
|
- name: Setup Android SDK (retry)
|
|
timeout-minutes: 15
|
|
if: ${{ steps.netsetup415.outcome == 'failure' }}
|
|
uses: android-actions/setup-android@v4
|
|
with:
|
|
accept-android-sdk-licenses: false
|
|
packages: platform-tools
|
|
cmdline-tools-version: 13114758
|
|
|
|
- name: Accept Android SDK licenses
|
|
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
|
run: yes | sdkmanager --licenses >/dev/null
|
|
|
|
- name: Download retrace APK
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: MobileGL-plugin-trace-release-${{ github.sha }}.apk
|
|
path: android-retrace-apks
|
|
|
|
- name: Enable KVM
|
|
run: |
|
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
sudo udevadm control --reload-rules
|
|
sudo udevadm trigger --name-match=kvm
|
|
|
|
- name: Create AVD
|
|
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
sh android-plugin/run-avd-ci.sh create \
|
|
--api-level 35 \
|
|
--target google_apis \
|
|
--arch x86_64 \
|
|
--profile pixel_6 \
|
|
--avd-name "${AVD_NAME}"
|
|
|
|
- name: Launch emulator
|
|
run: |
|
|
sh android-plugin/run-avd-ci.sh start \
|
|
--avd-name "${AVD_NAME}" \
|
|
--gpu "${{ matrix.backend.gpu }}" \
|
|
--emulator-log "${EMULATOR_LOG}" \
|
|
--pid-file "${EMULATOR_PID_FILE}" \
|
|
--boot-timeout 300
|
|
|
|
- name: Retrace and validate
|
|
env:
|
|
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
|
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
|
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
|
MOBILEGL_ITRP_FIX: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && 'ForceOn' || 'Auto' }}
|
|
run: |
|
|
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
|
test -f "${apk_file}"
|
|
extra_retrace_args=()
|
|
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;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
|
|
--require-inproc
|
|
)
|
|
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
|
|
# THE SAME CLIENT KNOBS AS inproc, so the two acceptance lanes differ in exactly one
|
|
# thing: which PROCESS the server role runs in. They are scrubbed out of the CHILD's
|
|
# environment by the launcher and read by the client, which is where they belong.
|
|
#
|
|
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
|
|
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
|
|
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
|
|
# value would override that. --use-pbuffer is NOT optional here: a spawned
|
|
# server cannot be handed the client's window, because an ANativeWindow* is a pointer
|
|
# into the CLIENT's process and SetWindowHandle is refused on the way across with
|
|
# Fatal{UnmigratedSurface, "AndroidNativeWindow:use-MOBILEGL_IPC_SURFACE=server"} (Rule H, permanent;
|
|
# a windowed client uses MOBILEGL_IPC_SURFACE=server instead).
|
|
extra_retrace_args+=(
|
|
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
|
|
--use-pbuffer
|
|
--require-spawn
|
|
)
|
|
else
|
|
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}")
|
|
fi
|
|
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
|
|
# min-filter downgrading on ANGLE llvmpipe.
|
|
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
|
|
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
|
|
fi
|
|
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.avoid_angle_llvmpipe_explicit_lod_bias || false }}" = "true" ]; then
|
|
extra_retrace_args+=(--avoid-angle-llvmpipe-explicit-lod-bias)
|
|
fi
|
|
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
|
|
extra_retrace_args+=(--coherent-as-flush)
|
|
fi
|
|
|
|
run_retrace() {
|
|
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
|
|
--apk-file "${apk_file}" \
|
|
--package top.mobilegl.plugin.trace \
|
|
--backend "${{ matrix.backend.name }}" \
|
|
--result-root android-retrace-result \
|
|
--fixture-root android-retrace-fixture \
|
|
--case "${{ matrix.case.name }}" \
|
|
--trace-archive "${{ matrix.case.trace_archive }}" \
|
|
--trace-file "${{ matrix.case.trace_file }}" \
|
|
--golden "${{ matrix.case.golden }}" \
|
|
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
|
|
--target-call "${{ matrix.case.target_call }}" \
|
|
--width "${{ matrix.case.width }}" \
|
|
--height "${{ matrix.case.height }}" \
|
|
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
|
|
--crop-x "${{ matrix.case.crop_x }}" \
|
|
--crop-y "${{ matrix.case.crop_y }}" \
|
|
--crop-width "${{ matrix.case.crop_width }}" \
|
|
--crop-height "${{ matrix.case.crop_height }}" \
|
|
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
|
|
"${extra_retrace_args[@]}"
|
|
}
|
|
|
|
retrace_status=0
|
|
run_retrace || retrace_status=$?
|
|
if [ "${retrace_status}" -eq 75 ]; then
|
|
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
|
|
# Surface-lost is retried rather than failed, so it would otherwise
|
|
# be invisible. Report it per job - a healthy run prints nothing and
|
|
# a rate spike shows up as a row per affected case.
|
|
reason_file="android-retrace-result/infrastructure-failure-reason.txt"
|
|
surface_lost_retries=0
|
|
if [ -f "${reason_file}" ]; then
|
|
surface_lost_retries="$(grep -c 'angle-surface-lost' "${reason_file}" || true)"
|
|
fi
|
|
if [ "${surface_lost_retries}" -gt 0 ]; then
|
|
echo "surface-lost retries: ${surface_lost_retries} (${{ matrix.backend.name }}, ${{ matrix.case.name }})" \
|
|
>> "${GITHUB_STEP_SUMMARY}"
|
|
fi
|
|
# The restart truncates EMULATOR_LOG, and the attempt that lost the
|
|
# emulator is the one worth reading - the retry usually only shows
|
|
# the wreckage. Keep the first attempt's log before it is clobbered.
|
|
if [ -f "${EMULATOR_LOG}" ]; then
|
|
cp "${EMULATOR_LOG}" "${EMULATOR_LOG}.first-attempt" || true
|
|
fi
|
|
sh android-plugin/run-avd-ci.sh stop \
|
|
--avd-name "${AVD_NAME}" \
|
|
--emulator-log "${EMULATOR_LOG}" \
|
|
--pid-file "${EMULATOR_PID_FILE}"
|
|
adb kill-server || true
|
|
sleep 2
|
|
sh android-plugin/run-avd-ci.sh start \
|
|
--avd-name "${AVD_NAME}" \
|
|
--gpu "${{ matrix.backend.gpu }}" \
|
|
--emulator-log "${EMULATOR_LOG}" \
|
|
--pid-file "${EMULATOR_PID_FILE}" \
|
|
--boot-timeout 300
|
|
run_retrace
|
|
elif [ "${retrace_status}" -ne 0 ]; then
|
|
exit "${retrace_status}"
|
|
fi
|
|
|
|
- name: Collect retrace summary inputs
|
|
if: always()
|
|
run: |
|
|
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
|
|
if [ -n "${{ matrix.case.alternate_golden || '' }}" ] && [ -s "${{ matrix.case.alternate_golden || '' }}" ]; then
|
|
cp "${{ matrix.case.alternate_golden || '' }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-alternate-golden.png"
|
|
fi
|
|
|
|
- name: Collect emulator diagnostics
|
|
if: always()
|
|
run: |
|
|
mkdir -p android-retrace-result/diagnostics
|
|
adb devices -l > android-retrace-result/diagnostics/adb-devices.txt || true
|
|
timeout 30 adb logcat -d -t 1000 > android-retrace-result/diagnostics/logcat.txt || true
|
|
if [ -f "${EMULATOR_LOG}" ]; then
|
|
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
|
|
fi
|
|
if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
|
|
cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
|
|
fi
|
|
# A vanished emulator looks identical whether the host OOM killer took
|
|
# qemu or the renderer faulted. These two say which.
|
|
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
|
|
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
|
|
|
|
- name: Stop emulator
|
|
if: always()
|
|
run: |
|
|
sh android-plugin/run-avd-ci.sh stop \
|
|
--avd-name "${AVD_NAME}" \
|
|
--emulator-log "${EMULATOR_LOG}" \
|
|
--pid-file "${EMULATOR_PID_FILE}"
|
|
|
|
- name: Upload Android retrace result
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
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
|
|
|
|
retrace-summary:
|
|
name: retrace summary
|
|
runs-on: ubuntu-26.04
|
|
needs: retrace
|
|
if: ${{ !cancelled() }}
|
|
env:
|
|
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
|
|
|
|
- name: Set artifact metadata
|
|
run: |
|
|
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Download Android monolith control results
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: MobileGL-android-retrace-result-monolith-control-monolith-*
|
|
path: retrace-artifacts/monolith-control
|
|
|
|
- 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: Download Android spawn acceptance results
|
|
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: MobileGL-android-retrace-result-spawn-acceptance-spawn-*
|
|
path: retrace-artifacts/spawn-acceptance
|
|
|
|
- name: Render monolith control summary
|
|
run: |
|
|
node tools/trace_replay/render_retrace_summary.mjs \
|
|
--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: 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: Render spawn acceptance summary
|
|
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
|
run: |
|
|
node tools/trace_replay/render_retrace_summary.mjs --input retrace-artifacts/spawn-acceptance --output-dir android-retrace-summary/spawn-acceptance --title "MobileGL Android spawn acceptance" --group-label "Android Emulator / spawn acceptance" --html mobilegl-android-spawn-acceptance.html
|
|
|
|
- name: Upload Android monolith control summary
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
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
|
|
|
|
- name: Upload Android spawn acceptance summary
|
|
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: MobileGL-android-spawn-acceptance-summary-${{ github.sha }}
|
|
path: android-retrace-summary/spawn-acceptance/mobilegl-android-spawn-acceptance.html
|
|
archive: false
|
|
if-no-files-found: error
|
|
|
|
remove-artifact-clutter:
|
|
name: remove artifact clutter
|
|
runs-on: ubuntu-26.04
|
|
needs: retrace-summary
|
|
if: always()
|
|
permissions:
|
|
actions: write
|
|
steps:
|
|
- name: Delete intermediate Android retrace artifacts
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
# Fail before deletion if either paginated inventory cannot be read.
|
|
# 50 rows per page: the jobs endpoint answers HTTP 502 for 90+ rows on a re-run attempt of a run this size.
|
|
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=50" \
|
|
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name' \
|
|
> "${RUNNER_TEMP}/cleanup-jobs.txt"
|
|
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
|
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv' \
|
|
> "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
|
declare -A failed_cases=()
|
|
while IFS= read -r job_name; do
|
|
case_name="${job_name#retrace (*, }"
|
|
case_name="${case_name%)}"
|
|
failed_cases["${case_name}"]=1
|
|
done < "${RUNNER_TEMP}/cleanup-jobs.txt"
|
|
|
|
if ((${#failed_cases[@]})); then
|
|
echo "Retaining fixtures and results for failed retrace case(s):"
|
|
printf ' %s\n' "${!failed_cases[@]}"
|
|
else
|
|
echo "All retrace jobs succeeded; nothing needs to be retained."
|
|
fi
|
|
|
|
deleted=0
|
|
retained=0
|
|
while IFS=$'\t' read -r artifact_id artifact_name; do
|
|
keep=0
|
|
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
|
|
case_name="${artifact_name#MobileGL-trace-fixture-}"
|
|
if [[ -v "failed_cases[${case_name}]" ]]; then
|
|
keep=1
|
|
fi
|
|
elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
|
|
# The result artifact carries mobilegl.log, retrace.log, logcat,
|
|
# the emulator log and the actual/diff images - the only record of
|
|
# why a retrace failed. Its name ends in -<backend>-<case>, so a
|
|
# suffix match on the case name keeps both backends' results for a
|
|
# case that failed on either of them, which is what a comparison
|
|
# needs. The match is anchored at the end, so a case name that is a
|
|
# prefix of a longer one does not retain the longer one's results.
|
|
for case_name in "${!failed_cases[@]}"; do
|
|
if [[ "${artifact_name}" == *-"${case_name}" ]]; then
|
|
keep=1
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if ((keep)); then
|
|
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
|
((retained += 1))
|
|
continue
|
|
fi
|
|
|
|
echo "Deleting ${artifact_name} (${artifact_id})"
|
|
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
|
|
((deleted += 1))
|
|
done < "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
|
|
|
echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
|