Skip to content

Commit 171560b

Browse files
committed
Increase Android emulator RAM to 4 GB
1 parent 74a82a2 commit 171560b

File tree

3 files changed

+144
-10
lines changed

3 files changed

+144
-10
lines changed

Android/README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,6 @@ require adding your user to a group, or changing your udev rules. On GitHub
103103
Actions, the test script will do this automatically using the commands shown
104104
[here](https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/).
105105

106-
The test suite can usually be run on a device with 2 GB of RAM, but this is
107-
borderline, so you may need to increase it to 4 GB. As of Android
108-
Studio Koala, 2 GB is the default for all emulators, although the user interface
109-
may indicate otherwise. Locate the emulator's directory under `~/.android/avd`,
110-
and find `hw.ramSize` in both config.ini and hardware-qemu.ini. Either set these
111-
manually to the same value, or use the Android Studio Device Manager, which will
112-
update both files.
113-
114106
You can run the test suite either:
115107

116108
* Within the CPython repository, after doing a build as described above. On

Android/testbed/app/build.gradle.kts

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ val KNOWN_ABIS = mapOf(
1818
"x86_64-linux-android" to "x86_64",
1919
)
2020

21+
val osArch = System.getProperty("os.arch")
22+
val NATIVE_ABI = mapOf(
23+
"aarch64" to "arm64-v8a",
24+
"amd64" to "x86_64",
25+
"arm64" to "arm64-v8a",
26+
"x86_64" to "x86_64",
27+
)[osArch] ?: throw GradleException("Unknown os.arch '$osArch'")
28+
2129
// Discover prefixes.
2230
val prefixes = ArrayList<File>()
2331
if (inSourceTree) {
@@ -149,6 +157,9 @@ android {
149157
testOptions {
150158
managedDevices {
151159
localDevices {
160+
// systemImageSource should use what its documentation calls an
161+
// "explicit source", i.e. the sdkmanager package name format, because
162+
// that will be required in CreateEmulatorTask below.
152163
create("minVersion") {
153164
device = "Small Phone"
154165

@@ -157,13 +168,13 @@ android {
157168

158169
// ATD devices are smaller and faster, but have a minimum
159170
// API level of 30.
160-
systemImageSource = if (apiLevel >= 30) "aosp-atd" else "aosp"
171+
systemImageSource = if (apiLevel >= 30) "aosp_atd" else "default"
161172
}
162173

163174
create("maxVersion") {
164175
device = "Small Phone"
165176
apiLevel = defaultConfig.targetSdk!!
166-
systemImageSource = "aosp-atd"
177+
systemImageSource = "aosp_atd"
167178
}
168179
}
169180

@@ -189,6 +200,136 @@ dependencies {
189200
}
190201

191202

203+
afterEvaluate {
204+
// Every new emulator has a maximum of 2 GB RAM, regardless of its hardware profile
205+
// (https://cs.android.com/android-studio/platform/tools/base/+/refs/tags/studio-2025.3.2:sdklib/src/main/java/com/android/sdklib/internal/avd/EmulatedProperties.java;l=68).
206+
// This is barely enough to test Python, and not enough to test Pandas
207+
// (https://github.com/python/cpython/pull/137186#issuecomment-3136301023,
208+
// https://github.com/pandas-dev/pandas/pull/63405#issuecomment-3667846159).
209+
// So we'll increase it by editing the emulator configuration files.
210+
//
211+
// If the emulator doesn't exist yet, we want to edit it after it's created, but
212+
// before it starts for the first time. Otherwise it'll need to be cold-booted
213+
// again, which would slow down the first run, which is likely the only run in CI
214+
// environments. But the Setup task both creates and starts the emulator if it
215+
// doesn't already exist. So we create it ourselves before the Setup task runs.
216+
for (device in android.testOptions.managedDevices.localDevices) {
217+
val createTask = tasks.register<CreateEmulatorTask>("${device.name}Create") {
218+
this.device = device.device
219+
apiLevel = device.apiLevel
220+
systemImageSource = device.systemImageSource
221+
abi = NATIVE_ABI
222+
}
223+
tasks.named("${device.name}Setup") {
224+
dependsOn(createTask)
225+
}
226+
}
227+
}
228+
229+
abstract class CreateEmulatorTask : DefaultTask() {
230+
@get:Input abstract val device: Property<String>
231+
@get:Input abstract val apiLevel: Property<Int>
232+
@get:Input abstract val systemImageSource: Property<String>
233+
@get:Input abstract val abi: Property<String>
234+
@get:Inject abstract val execOps: ExecOperations
235+
236+
private val avdName by lazy {
237+
listOf(
238+
"dev${apiLevel.get()}",
239+
systemImageSource.get(),
240+
abi.get(),
241+
device.get().replace(' ', '_'),
242+
).joinToString("_")
243+
}
244+
245+
private val avdDir by lazy {
246+
val userHome =
247+
System.getenv("ANDROID_USER_HOME") ?:
248+
(System.getProperty("user.home")!! + "/.android")
249+
File("$userHome/avd/gradle-managed", "$avdName.avd")
250+
}
251+
252+
@TaskAction
253+
fun run() {
254+
if (!avdDir.exists()) {
255+
createAvd()
256+
}
257+
updateAvd()
258+
}
259+
260+
fun createAvd() {
261+
val systemImage = listOf(
262+
"system-images",
263+
"android-${apiLevel.get()}",
264+
systemImageSource.get(),
265+
abi.get(),
266+
).joinToString(";")
267+
268+
runCmdlineTool("sdkmanager", systemImage)
269+
runCmdlineTool(
270+
"avdmanager", "create", "avd",
271+
"--name", avdName,
272+
"--path", avdDir,
273+
"--device", device.get().lowercase().replace(" ", "_"),
274+
"--package", systemImage,
275+
)
276+
277+
val iniName = "$avdName.ini"
278+
if (!File(avdDir.parentFile.parentFile, iniName).renameTo(
279+
File(avdDir.parentFile, iniName)
280+
)) {
281+
throw GradleException("Failed to rename $iniName")
282+
}
283+
}
284+
285+
fun updateAvd() {
286+
for (filename in listOf(
287+
"config.ini", // Created by avdmanager; always exists
288+
"hardware-qemu.ini", // Created on first run; might not exist
289+
)) {
290+
val iniFile = File(avdDir, filename)
291+
if (!iniFile.exists()) {
292+
if (filename == "config.ini") {
293+
throw GradleException("$iniFile does not exist")
294+
}
295+
continue
296+
}
297+
298+
val iniText = iniFile.readText()
299+
val pattern = Regex(
300+
"""^\s*hw.ramSize\s*=\s*(.+?)\s*$""", RegexOption.MULTILINE
301+
)
302+
val matches = pattern.findAll(iniText).toList()
303+
if (matches.size != 1) {
304+
throw GradleException(
305+
"Found ${matches.size} instances of $pattern in $iniFile; expected 1"
306+
)
307+
}
308+
309+
val expectedRam = "4096"
310+
if (matches[0].groupValues[1] != expectedRam) {
311+
iniFile.writeText(
312+
iniText.replace(pattern, "hw.ramSize = $expectedRam")
313+
)
314+
}
315+
}
316+
}
317+
318+
fun runCmdlineTool(tool: String, vararg args: Any) {
319+
val androidHome = System.getenv("ANDROID_HOME")!!
320+
val exeSuffix =
321+
if (System.getProperty("os.name").lowercase().startsWith("win")) ".exe"
322+
else ""
323+
val command =
324+
listOf("$androidHome/cmdline-tools/latest/bin/$tool$exeSuffix", *args)
325+
println(command.joinToString(" "))
326+
execOps.exec {
327+
commandLine(command)
328+
}
329+
}
330+
}
331+
332+
192333
// Create some custom tasks to copy Python and its standard library from
193334
// elsewhere in the repository.
194335
androidComponents.onVariants { variant ->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The Android testbed's emulator RAM has been increased from 2 GB to 4 GB.

0 commit comments

Comments
 (0)