From 8f64fa76867e2f585e9d420bfe8bd78934cc727d Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 18 Oct 2026 05:42:48 -0400 Subject: [PATCH] [CI] (.github/workflows, android-plugin): boot retrace emulators from a cached snapshot - the AVD job boots the new AVD once per GPU mode and saves a snapshot into the cache - retrace jobs load their mode's snapshot, log whether it loaded, and skip SDK setup on a hit --- .github/workflows/apk.yml | 69 ++++++++++--- android-plugin/run-avd-ci.sh | 182 ++++++++++++++++++++++++++++------- 2 files changed, 201 insertions(+), 50 deletions(-) diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml index c039f117..e58b5ccb 100644 --- a/.github/workflows/apk.yml +++ b/.github/workflows/apk.yml @@ -379,7 +379,7 @@ jobs: ${{ 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-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }} + key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v3-${{ hashFiles('android-plugin/run-avd-ci.sh') }} - name: Create AVD if: steps.android-avd-cache.outputs.cache-hit != 'true' @@ -391,6 +391,33 @@ jobs: --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 per emulator GPU mode of the retrace matrix (the backends' `gpu` in + # tools/trace_replay/trace_cases.py) and keep a snapshot of each booted device in the cached + # AVD, so a retrace job loads one instead of cold-booting. A mode whose snapshot cannot be + # saved still leaves the AVD past its first-boot setup. + - name: Pre-boot AVD and save snapshots + if: steps.android-avd-cache.outputs.cache-hit != 'true' + run: | + for gpu in software lavapipe; do + log="${RUNNER_TEMP}/avd-snapshot-${gpu}.log" + pid="${RUNNER_TEMP}/avd-snapshot-${gpu}.pid" + if ! sh android-plugin/run-avd-ci.sh snapshot --avd-name "${AVD_NAME}" --gpu "${gpu}" \ + --name "ci-${gpu}" --emulator-log "${log}" --pid-file "${pid}" --boot-timeout 300; then + echo "::warning::no ci-${gpu} snapshot was saved; retrace jobs in that GPU mode cold-boot" + 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 + fi + done + du -sh "${ANDROID_AVD_HOME}" + retrace: name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }}) runs-on: ubuntu-26.04 @@ -440,8 +467,29 @@ jobs: 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-v3-${{ 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 @@ -460,20 +508,9 @@ jobs: 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: 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-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }} - - name: Download retrace APK uses: actions/download-artifact@v8 with: @@ -503,7 +540,8 @@ jobs: --gpu "${{ matrix.backend.gpu }}" \ --emulator-log "${EMULATOR_LOG}" \ --pid-file "${EMULATOR_PID_FILE}" \ - --boot-timeout 300 + --boot-timeout 300 \ + --snapshot "ci-${{ matrix.backend.gpu }}" - name: Retrace and validate env: @@ -610,7 +648,8 @@ jobs: --gpu "${{ matrix.backend.gpu }}" \ --emulator-log "${EMULATOR_LOG}" \ --pid-file "${EMULATOR_PID_FILE}" \ - --boot-timeout 300 + --boot-timeout 300 \ + --snapshot "ci-${{ matrix.backend.gpu }}" run_retrace elif [ "${retrace_status}" -ne 0 ]; then exit "${retrace_status}" diff --git a/android-plugin/run-avd-ci.sh b/android-plugin/run-avd-ci.sh index 9fc3c8eb..df9936a3 100644 --- a/android-plugin/run-avd-ci.sh +++ b/android-plugin/run-avd-ci.sh @@ -16,6 +16,15 @@ Usage: --gpu GPU \ --emulator-log FILE \ --pid-file FILE \ + --boot-timeout SECONDS \ + [--snapshot NAME] + + sh android-plugin/run-avd-ci.sh snapshot \ + --avd-name NAME \ + --gpu GPU \ + --name SNAPSHOT \ + --emulator-log FILE \ + --pid-file FILE \ --boot-timeout SECONDS sh android-plugin/run-avd-ci.sh stop \ @@ -118,48 +127,34 @@ create_avd() { echo no | "${avdmanager_bin}" create avd --force --name "${avd_name}" --package "${system_image}" --device "${profile}" } -start_avd() { - avd_name="" - gpu="" - emulator_log="" - pid_file="" - boot_timeout="" +# The guest property a snapshot carries: `snapshot` sets it just before saving, and it lives only +# in guest RAM, so after `start` it reads the snapshot's name exactly when that snapshot loaded. +snapshot_marker_prop="debug.ci.boot_snapshot" - while [ "$#" -gt 0 ]; do - case "$1" in - --avd-name) avd_name="$(next_arg "$@")"; shift 2 ;; - --gpu) gpu="$(next_arg "$@")"; shift 2 ;; - --emulator-log) emulator_log="$(next_arg "$@")"; shift 2 ;; - --pid-file) pid_file="$(next_arg "$@")"; shift 2 ;; - --boot-timeout) boot_timeout="$(next_arg "$@")"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) die "unknown start argument: $1" ;; - esac - done - - require_value "${avd_name}" "--avd-name" - require_value "${gpu}" "--gpu" - require_value "${emulator_log}" "--emulator-log" - require_value "${pid_file}" "--pid-file" - require_value "${boot_timeout}" "--boot-timeout" - - adb_bin="$(find_android_tool adb platform-tools/adb)" - emulator_bin="$(find_android_tool emulator emulator/emulator)" - ensure_android_avd_home - - mkdir -p "$(dirname "${emulator_log}")" "$(dirname "${pid_file}")" - "${emulator_bin}" -avd "${avd_name}" \ +# launch_emulator AVD GPU LOG PID_FILE SNAPSHOT_FLAG...: the flags a snapshot is saved and loaded +# with must match, so both commands launch through here. +launch_emulator() { + launch_avd="$1" + launch_gpu="$2" + launch_log="$3" + launch_pid_file="$4" + shift 4 + mkdir -p "$(dirname "${launch_log}")" "$(dirname "${launch_pid_file}")" + "${emulator_bin}" -avd "${launch_avd}" \ -no-window \ - -gpu "${gpu}" \ - -no-snapshot \ + -gpu "${launch_gpu}" \ + "$@" \ -noaudio \ -no-boot-anim \ -camera-back none \ -camera-front none \ - > "${emulator_log}" 2>&1 & - emulator_pid="$!" - echo "${emulator_pid}" > "${pid_file}" + > "${launch_log}" 2>&1 & + echo "$!" > "${launch_pid_file}" +} +wait_for_boot() { + emulator_log="$1" + boot_timeout="$2" if ! timeout "${boot_timeout}" "${adb_bin}" wait-for-device; then print_diagnostics "${emulator_log}" die "emulator did not connect to adb within ${boot_timeout}s" @@ -185,6 +180,122 @@ start_avd() { "${adb_bin}" shell input keyevent 82 || true } +start_avd() { + avd_name="" + gpu="" + emulator_log="" + pid_file="" + boot_timeout="" + snapshot="" + + while [ "$#" -gt 0 ]; do + case "$1" in + --avd-name) avd_name="$(next_arg "$@")"; shift 2 ;; + --gpu) gpu="$(next_arg "$@")"; shift 2 ;; + --emulator-log) emulator_log="$(next_arg "$@")"; shift 2 ;; + --pid-file) pid_file="$(next_arg "$@")"; shift 2 ;; + --boot-timeout) boot_timeout="$(next_arg "$@")"; shift 2 ;; + --snapshot) snapshot="$(next_arg "$@")"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown start argument: $1" ;; + esac + done + + require_value "${avd_name}" "--avd-name" + require_value "${gpu}" "--gpu" + require_value "${emulator_log}" "--emulator-log" + require_value "${pid_file}" "--pid-file" + require_value "${boot_timeout}" "--boot-timeout" + + adb_bin="$(find_android_tool adb platform-tools/adb)" + emulator_bin="$(find_android_tool emulator emulator/emulator)" + ensure_android_avd_home + + # A snapshot only shortens the boot: the image, the AVD and the GPU mode are the same, and the + # emulator cold-boots by itself when the snapshot does not load. + started_at="$(date +%s)" + if [ -n "${snapshot}" ] && [ -d "${ANDROID_AVD_HOME}/${avd_name}.avd/snapshots/${snapshot}" ]; then + launch_emulator "${avd_name}" "${gpu}" "${emulator_log}" "${pid_file}" -snapshot "${snapshot}" -no-snapshot-save + else + launch_emulator "${avd_name}" "${gpu}" "${emulator_log}" "${pid_file}" -no-snapshot + fi + wait_for_boot "${emulator_log}" "${boot_timeout}" + + seconds="$(( $(date +%s) - started_at ))" + marker="$("${adb_bin}" shell getprop "${snapshot_marker_prop}" 2>/dev/null | tr -d '\r' || true)" + if [ -n "${snapshot}" ] && [ "${marker}" = "${snapshot}" ]; then + echo "run-avd-ci.sh: booted in ${seconds}s from snapshot ${snapshot}" + elif [ -n "${snapshot}" ]; then + echo "run-avd-ci.sh: booted in ${seconds}s by cold boot; snapshot ${snapshot} did not load" + else + echo "run-avd-ci.sh: booted in ${seconds}s by cold boot" + fi +} + +# Boot the AVD cold once in GPU mode GPU and save snapshot NAME of the booted device, then shut it +# down cleanly so the AVD can be cached. The first call also does the AVD's first-boot setup, +# which a cold boot after a cache restore then no longer repeats. +snapshot_avd() { + avd_name="" + gpu="" + snapshot_name="" + emulator_log="" + pid_file="" + boot_timeout="" + + while [ "$#" -gt 0 ]; do + case "$1" in + --avd-name) avd_name="$(next_arg "$@")"; shift 2 ;; + --gpu) gpu="$(next_arg "$@")"; shift 2 ;; + --name) snapshot_name="$(next_arg "$@")"; shift 2 ;; + --emulator-log) emulator_log="$(next_arg "$@")"; shift 2 ;; + --pid-file) pid_file="$(next_arg "$@")"; shift 2 ;; + --boot-timeout) boot_timeout="$(next_arg "$@")"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown snapshot argument: $1" ;; + esac + done + + require_value "${avd_name}" "--avd-name" + require_value "${gpu}" "--gpu" + require_value "${snapshot_name}" "--name" + require_value "${emulator_log}" "--emulator-log" + require_value "${pid_file}" "--pid-file" + require_value "${boot_timeout}" "--boot-timeout" + + adb_bin="$(find_android_tool adb platform-tools/adb)" + emulator_bin="$(find_android_tool emulator emulator/emulator)" + ensure_android_avd_home + + started_at="$(date +%s)" + launch_emulator "${avd_name}" "${gpu}" "${emulator_log}" "${pid_file}" -no-snapshot-load -no-snapshot-save + wait_for_boot "${emulator_log}" "${boot_timeout}" + echo "run-avd-ci.sh: ${gpu} cold boot took $(( $(date +%s) - started_at ))s" + + "${adb_bin}" shell setprop "${snapshot_marker_prop}" "${snapshot_name}" + saved="$("${adb_bin}" emu avd snapshot save "${snapshot_name}" 2>&1 | tr -d '\r' || true)" + echo "run-avd-ci.sh: snapshot save ${snapshot_name}: ${saved}" + + "${adb_bin}" emu kill >/dev/null 2>&1 || true + emulator_pid="$(cat "${pid_file}")" + for _ in $(seq 1 120); do + kill -0 "${emulator_pid}" 2>/dev/null || break + sleep 1 + done + if kill -0 "${emulator_pid}" 2>/dev/null; then + kill "${emulator_pid}" >/dev/null 2>&1 || true + die "emulator did not exit after saving snapshot ${snapshot_name}" + fi + + snapshot_dir="${ANDROID_AVD_HOME}/${avd_name}.avd/snapshots/${snapshot_name}" + case "${saved}" in + *OK*) ;; + *) rm -rf "${snapshot_dir}"; die "snapshot ${snapshot_name} was not saved: ${saved}" ;; + esac + [ -d "${snapshot_dir}" ] || die "snapshot ${snapshot_name} reported OK but ${snapshot_dir} does not exist" + du -sh "${snapshot_dir}" +} + stop_avd() { avd_name="" emulator_log="" @@ -226,6 +337,7 @@ shift case "${command}" in create) create_avd "$@" ;; start) start_avd "$@" ;; + snapshot) snapshot_avd "$@" ;; stop) stop_avd "$@" ;; -h|--help) usage; exit 0 ;; *) die "unknown command: ${command}" ;;