Skip to content

Commit e280380

Browse files
committed
Merge branch 'main' into multi-model
2 parents 96fa34c + aa16547 commit e280380

20 files changed

Lines changed: 1318 additions & 43 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
name: Android Emulator Tests
8+
9+
on:
10+
pull_request:
11+
branches: [main]
12+
paths:
13+
- 'dl3/android/**'
14+
- 'mv3/android/**'
15+
- '.github/workflows/android-emulator.yml'
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
instrumentation-test:
23+
runs-on: 8-core-ubuntu
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
demo:
28+
- path: 'dl3/android/DeepLabV3Demo'
29+
name: 'DeepLabV3Demo'
30+
- path: 'mv3/android/MV3Demo'
31+
name: 'MV3Demo'
32+
env:
33+
API_LEVEL: 34
34+
ARCH: x86_64
35+
DEMO_PATH: ${{ matrix.demo.path }}
36+
37+
name: Instrumentation Test ${{ matrix.demo.name }}
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@v4
41+
42+
- name: Determine demo name
43+
id: demo-info
44+
run: |
45+
DEMO_NAME=$(basename "${{ env.DEMO_PATH }}")
46+
echo "demo_name=$DEMO_NAME" >> $GITHUB_OUTPUT
47+
echo "Testing: $DEMO_NAME"
48+
echo "=== Test Configuration ==="
49+
echo "Demo: $DEMO_NAME"
50+
echo "Model will be downloaded by the test if not present"
51+
52+
- name: Enable KVM group perms
53+
run: |
54+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
55+
sudo udevadm control --reload-rules
56+
sudo udevadm trigger --name-match=kvm
57+
58+
- name: Set up JDK 17
59+
uses: actions/setup-java@v4
60+
with:
61+
java-version: '17'
62+
distribution: 'temurin'
63+
64+
- name: Setup Gradle
65+
uses: gradle/actions/setup-gradle@v4
66+
67+
- name: AVD cache
68+
uses: actions/cache@v4
69+
id: avd-cache
70+
with:
71+
path: |
72+
~/.android/avd/*
73+
~/.android/adb*
74+
key: avd-${{ env.API_LEVEL }}-${{ env.ARCH }}-ram8G-disk8G-${{ steps.demo-info.outputs.demo_name }}-v1
75+
76+
- name: Create AVD and generate snapshot for caching
77+
if: steps.avd-cache.outputs.cache-hit != 'true'
78+
uses: reactivecircus/android-emulator-runner@v2
79+
with:
80+
api-level: ${{ env.API_LEVEL }}
81+
arch: ${{ env.ARCH }}
82+
ram-size: 8192M
83+
disk-size: 8192M
84+
force-avd-creation: true
85+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save -memory 8192
86+
disable-animations: false
87+
working-directory: ${{ env.DEMO_PATH }}
88+
script: echo "Generated AVD snapshot for caching."
89+
90+
- name: Run instrumentation tests
91+
uses: reactivecircus/android-emulator-runner@v2
92+
with:
93+
api-level: ${{ env.API_LEVEL }}
94+
arch: ${{ env.ARCH }}
95+
ram-size: 8192M
96+
disk-size: 8192M
97+
force-avd-creation: true
98+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save -memory 8192
99+
disable-animations: true
100+
working-directory: ${{ env.DEMO_PATH }}
101+
script: |
102+
set -ex
103+
DEMO_NAME=$(basename "$PWD")
104+
LOGCAT_FILE="/tmp/logcat-${DEMO_NAME}.txt"
105+
echo "=== Emulator Memory Info ==="
106+
adb shell cat /proc/meminfo | head -5
107+
echo "=== Emulator Disk Space ==="
108+
adb shell df -h /data
109+
adb logcat -c
110+
adb logcat > "$LOGCAT_FILE" &
111+
LOGCAT_PID=$!
112+
echo "=== Starting Gradle ==="
113+
./gradlew connectedCheck
114+
TEST_EXIT_CODE=$?
115+
if [ -n "$LOGCAT_PID" ]; then kill $LOGCAT_PID 2>/dev/null || true; fi
116+
echo "=== Test completion status ==="
117+
if [ $TEST_EXIT_CODE -eq 0 ]; then echo "✅ Tests passed successfully"; else echo "❌ Tests failed with exit code $TEST_EXIT_CODE"; fi
118+
echo "=== Checking for test results in logcat ==="
119+
grep -E "SanityCheck|UIWorkflowTest" "$LOGCAT_FILE" || echo "No test logs found"
120+
exit $TEST_EXIT_CODE
121+
122+
- name: Upload logcat
123+
if: always()
124+
uses: actions/upload-artifact@v4
125+
with:
126+
name: logcat-${{ steps.demo-info.outputs.demo_name }}
127+
path: /tmp/logcat-*.txt
128+
retention-days: 7

dl3/android/DeepLabV3Demo/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,20 @@ The app detects all 21 PASCAL VOC classes with distinct color overlays:
102102
```
103103

104104
### Using Android Studio
105-
Open `app/src/androidTest/java/org/pytorch/executorchexamples/dl3/SanityCheck.kt` and click the Play button.
105+
Open `app/src/androidTest/java/org/pytorch/executorchexamples/dl3/SanityCheck.kt` or `UIWorkflowTest.kt` and click the Play button.
106+
107+
### Test Files
108+
- **SanityCheck.kt**: Basic module forward pass test
109+
- Downloads model automatically if not present
110+
- Tests model loading from app's private storage
111+
- Validates model output shape (batch_size × classes × width × height)
112+
113+
- **UIWorkflowTest.kt**: Compose UI workflow tests including:
114+
- Initial UI state verification
115+
- Download button functionality (with and without model present)
116+
- Model run/segmentation testing with inference time display
117+
- Next button to cycle through sample images
118+
- Reset button functionality
119+
- Complete end-to-end workflow (Next → Run → Reset)
120+
- Multiple consecutive runs to test model reusability
121+

dl3/android/DeepLabV3Demo/app/build.gradle.kts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ plugins {
1111
id("org.jetbrains.kotlin.android")
1212
}
1313

14+
val useLocalAar: Boolean? = (project.findProperty("useLocalAar") as? String)?.toBoolean()
15+
1416
android {
1517
namespace = "org.pytorch.executorchexamples.dl3"
1618
compileSdk = 34
@@ -52,7 +54,12 @@ dependencies {
5254
implementation("androidx.compose.material3:material3")
5355
implementation("com.google.android.material:material:1.12.0")
5456
implementation("androidx.appcompat:appcompat:1.7.0")
55-
implementation("org.pytorch:executorch-android:1.0.0")
57+
if (useLocalAar == true) {
58+
implementation(files("libs/executorch.aar"))
59+
implementation("com.facebook.fbjni:fbjni:0.5.1")
60+
} else {
61+
implementation("org.pytorch:executorch-android:1.0.1")
62+
}
5663
testImplementation("junit:junit:4.13.2")
5764
androidTestImplementation("androidx.test.ext:junit:1.1.5")
5865
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

dl3/android/DeepLabV3Demo/app/src/androidTest/java/org/pytorch/executorchexamples/dl3/SanityCheck.kt

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,85 @@
88

99
package org.pytorch.executorchexamples.dl3
1010

11+
import android.content.Context
12+
import android.util.Log
13+
import androidx.test.core.app.ApplicationProvider
1114
import androidx.test.filters.SmallTest
1215
import org.junit.Assert.assertEquals
16+
import org.junit.Before
1317
import org.junit.Test
1418
import org.pytorch.executorch.Module
1519
import org.pytorch.executorch.Tensor
20+
import java.io.File
21+
import java.io.FileOutputStream
22+
import java.net.HttpURLConnection
23+
import java.net.URL
1624

25+
/**
26+
* Sanity check test for model loading.
27+
*
28+
* This test downloads the model if not available and validates model functionality.
29+
* The model is stored in the app's private storage (same location as MainActivity uses).
30+
*/
1731
@SmallTest
1832
class SanityCheck {
1933

34+
companion object {
35+
private const val MODEL_FILENAME = "dl3_xnnpack_fp32.pte"
36+
private const val MODEL_URL = "https://ossci-android.s3.amazonaws.com/executorch/models/snapshot-20260116/dl3_xnnpack_fp32.pte"
37+
private const val TAG = "SanityCheck"
38+
}
39+
40+
private lateinit var modelPath: String
41+
private lateinit var context: Context
42+
43+
@Before
44+
fun setUp() {
45+
// Use the app's private files directory (same as MainActivity)
46+
context = ApplicationProvider.getApplicationContext()
47+
modelPath = "${context.filesDir.absolutePath}/$MODEL_FILENAME"
48+
49+
// Download model if not present
50+
val modelFile = File(modelPath)
51+
if (!modelFile.exists()) {
52+
Log.i(TAG, "Model not found at $modelPath, downloading...")
53+
downloadModel()
54+
} else {
55+
Log.i(TAG, "Model found at $modelPath")
56+
}
57+
}
58+
59+
private fun downloadModel() {
60+
try {
61+
val url = URL(MODEL_URL)
62+
val connection = url.openConnection() as HttpURLConnection
63+
connection.requestMethod = "GET"
64+
connection.instanceFollowRedirects = true
65+
connection.connect()
66+
67+
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
68+
throw RuntimeException("Server returned HTTP ${connection.responseCode}")
69+
}
70+
71+
connection.inputStream.use { input ->
72+
FileOutputStream(modelPath).use { output ->
73+
val buffer = ByteArray(4096)
74+
var bytesRead: Int
75+
while (input.read(buffer).also { bytesRead = it } != -1) {
76+
output.write(buffer, 0, bytesRead)
77+
}
78+
}
79+
}
80+
Log.i(TAG, "Model downloaded successfully to $modelPath")
81+
} catch (e: Exception) {
82+
Log.e(TAG, "Failed to download model", e)
83+
throw RuntimeException("Failed to download model: ${e.message}", e)
84+
}
85+
}
86+
2087
@Test
2188
fun testModuleForward() {
22-
val module = Module.load("/data/local/tmp/dl3_xnnpack_fp32.pte")
89+
val module = Module.load(modelPath)
2390
// Test with sample inputs (ones) and make sure there is no crash.
2491
val outputTensor: Tensor = module.forward()[0].toTensor()
2592
val shape = outputTensor.shape()

0 commit comments

Comments
 (0)