Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/experiment-replay-pixelcopy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# EXPERIMENT (do not merge): does PixelCopy return real frames on GH Actions emulators?
name: 'Experiment: Replay PixelCopy on GH emulators'
on:
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
pixelcopy:
name: API ${{ matrix.api-level }} ${{ matrix.target }} gpu=${{ matrix.gpu }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
# Config used by PR #5639
- api-level: 35
target: google_apis
gpu: auto
# Same image, software GPU, to isolate the -gpu flag
- api-level: 35
target: google_apis
gpu: swiftshader_indirect
# Config used by agp-matrix.yml
- api-level: 30
target: aosp_atd
gpu: swiftshader_indirect

steps:
- name: Git checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: 'Set up Java: 21'
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: 'temurin'
java-version: '21'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1

- 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: Run ReplaySnapshotTest
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # pin@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
channel: canary
arch: x86_64
disable-animations: true
disable-spellchecker: true
disk-size: 4096M
emulator-options: -memory 4096 -no-window -gpu ${{ matrix.gpu }} -noaudio -no-boot-anim -camera-back none -no-snapshot-save
script: ./scripts/experiment-replay-pixelcopy.sh replay-experiment

- name: Upload frames and logcat
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: pixelcopy-api${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.gpu }}
path: |
replay-experiment/
**/build/outputs/androidTest-results/**/*.xml
18 changes: 18 additions & 0 deletions scripts/experiment-replay-pixelcopy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh
# EXPERIMENT (do not merge): run ReplaySnapshotTest on a GH Actions emulator and collect
# the captured replay frames and logcat, to check whether PixelCopy works there.
set -eu

out_dir=$1
mkdir -p "$out_dir"

rc=0
./gradlew :sentry-android-integration-tests:sentry-uitest-android:connectedReleaseAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=io.sentry.uitest.android.ReplaySnapshotTest \
-Denvironment=github || rc=$?

adb pull /sdcard/Download/sauce_labs_custom_screenshots "$out_dir/snapshots" ||
printf 'No replay snapshots to pull\n'
adb logcat -d >"$out_dir/logcat.txt" || printf 'Could not dump logcat\n'

exit "$rc"
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.sentry.uitest.android

import android.graphics.Bitmap
import android.os.Environment
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.launchActivity
import io.sentry.SentryReplayOptions
Expand All @@ -12,17 +13,12 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.Test
import kotlin.test.assertTrue
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assume.assumeThat
import org.junit.Before

class ReplaySnapshotTest : BaseUiTest() {

@Before
fun setup() {
// GH Actions emulators don't support capturing screenshots for replay
@Suppress("KotlinConstantConditions")
assumeThat(BuildConfig.ENVIRONMENT != "github", `is`(true))
// crash on swallowed Compose masking errors (e.g. broken obfuscated internals) so regressions
// fail this on-device test instead of silently under-masking (see SentryReplayDebug)
System.setProperty("io.sentry.replay.compose.fail-fast", "true")
Expand All @@ -41,6 +37,7 @@ class ReplaySnapshotTest : BaseUiTest() {
}
val frameReceived = CountDownLatch(1)
val capturedScreens = CopyOnWriteArraySet<String>()
val distinctColorsPerFrame = CopyOnWriteArraySet<Int>()

val activityScenario = launchActivity<ComposeActivity>()
activityScenario.moveToState(Lifecycle.State.RESUMED)
Expand All @@ -53,6 +50,18 @@ class ReplaySnapshotTest : BaseUiTest() {
hint.getAs(TypeCheckHint.REPLAY_FRAME_BITMAP, Bitmap::class.java)
?: return@ReplayFrameObserver
val name = screenName ?: "unknown"
// EXPERIMENT: a failed or blank PixelCopy yields a single-color bitmap
val colors = HashSet<Int>()
for (x in 0 until bitmap.width step 8) {
for (y in 0 until bitmap.height step 8) {
colors.add(bitmap.getPixel(x, y))
}
}
distinctColorsPerFrame.add(colors.size)
Log.i(
"PixelCopyExperiment",
"frame screen=$name size=${bitmap.width}x${bitmap.height} distinctColors=${colors.size}",
)
if (capturedScreens.add(name)) {
val file = File(snapshotsDir, "${name}_$frameTimestamp.png")
file.outputStream().use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) }
Expand All @@ -64,6 +73,10 @@ class ReplaySnapshotTest : BaseUiTest() {

assertTrue(frameReceived.await(10, TimeUnit.SECONDS), "Expected at least one replay frame")
assertTrue(capturedScreens.isNotEmpty(), "Expected at least one screen captured")
assertTrue(
distinctColorsPerFrame.any { it > 1 },
"Expected a non-blank frame, distinct colors per frame: $distinctColorsPerFrame",
)

val files = snapshotsDir.listFiles()?.filter { it.extension == "png" } ?: emptyList()
assertTrue(files.isNotEmpty(), "Expected snapshot PNG files on disk")
Expand Down
Loading