Implement memory ballooning in Terminal App
This change implements memory ballooning in the Terminal App to
dynamically adjust the memory allocated to the VM.
The Application class now includes an ApplicationLifecycleObserver which
binds to the VmLauncherService and sends lifecycle events (onStart,
onStop). The VmLauncherService then calls setMemoryBalloonByPercent() on
the VirtualMachine instance to adjust the memory. On application start
(onStart), the memory balloon is deflated (0%), maximizing available
memory for the app. On application stop (onStop), the memory balloon is
inflated (10%), allowing the system to reclaim memory while the app is
in the background.
Bug: b/392791968
Test: Maunual test memory balloon works in Terminal App
Test: Verify setMemoryBalloon() is not called if memory balloon disabled
Change-Id: I26e9bd944b29c27831f54be8b4ddba8c29020a5a
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
index efe651e..9f4909d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
@@ -18,12 +18,21 @@
import android.app.Application as AndroidApplication
import android.app.NotificationChannel
import android.app.NotificationManager
+import android.content.ComponentName
import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
public class Application : AndroidApplication() {
override fun onCreate() {
super.onCreate()
setupNotificationChannels()
+ val lifecycleObserver = ApplicationLifecycleObserver()
+ ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
}
private fun setupNotificationChannels() {
@@ -52,4 +61,53 @@
fun getInstance(c: Context): Application = c.getApplicationContext() as Application
}
+
+ /**
+ * Observes application lifecycle events and interacts with the VmLauncherService to manage
+ * virtual machine state based on application lifecycle transitions. This class binds to the
+ * VmLauncherService and notifies it of application lifecycle events (onStart, onStop), allowing
+ * the service to manage the VM accordingly.
+ */
+ inner class ApplicationLifecycleObserver() : DefaultLifecycleObserver {
+ private var vmLauncherService: VmLauncherService? = null
+ private val connection =
+ object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
+ val binder = service as VmLauncherService.VmLauncherServiceBinder
+ vmLauncherService = binder.getService()
+ }
+
+ override fun onServiceDisconnected(arg0: ComponentName) {
+ vmLauncherService = null
+ }
+ }
+
+ override fun onCreate(owner: LifecycleOwner) {
+ super.onCreate(owner)
+ bindToVmLauncherService()
+ }
+
+ override fun onStart(owner: LifecycleOwner) {
+ super.onStart(owner)
+ vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_START)
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_STOP)
+ super.onStop(owner)
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ if (vmLauncherService != null) {
+ this@Application.unbindService(connection)
+ vmLauncherService = null
+ }
+ super.onDestroy(owner)
+ }
+
+ fun bindToVmLauncherService() {
+ val intent = Intent(this@Application, VmLauncherService::class.java)
+ this@Application.bindService(intent, connection, 0) // No BIND_AUTO_CREATE
+ }
+ }
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
new file mode 100644
index 0000000..4e26c3c
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.virtualization.terminal
+
+enum class ApplicationLifeCycleEvent {
+ APP_ON_START,
+ APP_ON_STOP,
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 6301da4..4bfad62 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -62,6 +62,12 @@
import java.util.concurrent.Executors
class VmLauncherService : Service() {
+ inner class VmLauncherServiceBinder : android.os.Binder() {
+ fun getService(): VmLauncherService = this@VmLauncherService
+ }
+
+ private val binder = VmLauncherServiceBinder()
+
// TODO: using lateinit for some fields to avoid null
private var executorService: ExecutorService? = null
private var virtualMachine: VirtualMachine? = null
@@ -79,7 +85,32 @@
}
override fun onBind(intent: Intent?): IBinder? {
- return null
+ return binder
+ }
+
+ /**
+ * Processes application lifecycle events and adjusts the virtual machine's memory balloon
+ * accordingly.
+ *
+ * @param event The application lifecycle event.
+ */
+ fun processAppLifeCycleEvent(event: ApplicationLifeCycleEvent) {
+ when (event) {
+ // When the app starts, reset the memory balloon to 0%.
+ // This gives the app maximum available memory.
+ ApplicationLifeCycleEvent.APP_ON_START -> {
+ virtualMachine?.setMemoryBalloonByPercent(0)
+ }
+ ApplicationLifeCycleEvent.APP_ON_STOP -> {
+ // When the app stops, inflate the memory balloon to 10%.
+ // This allows the system to reclaim memory while the app is in the background.
+ // TODO(b/400590341) Inflate the balloon while the application remains Stop status.
+ virtualMachine?.setMemoryBalloonByPercent(10)
+ }
+ else -> {
+ Log.e(TAG, "unrecognized lifecycle event: $event")
+ }
+ }
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
index 0445fcb..40050c0 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
@@ -288,17 +288,7 @@
percent = 50;
}
- synchronized (mLock) {
- try {
- if (mVirtualMachine != null) {
- long bytes = mConfig.getMemoryBytes();
- mVirtualMachine.setMemoryBalloon(bytes * percent / 100);
- }
- } catch (Exception e) {
- /* Caller doesn't want our exceptions. Log them instead. */
- Log.w(TAG, "TrimMemory failed: ", e);
- }
- }
+ setMemoryBalloonByPercent(percent);
}
}
@@ -1392,6 +1382,24 @@
}
}
+ /** @hide */
+ public void setMemoryBalloonByPercent(int percent) {
+ if (percent < 0 || percent > 100) {
+ Log.e(TAG, String.format("Invalid percent value: %d", percent));
+ return;
+ }
+ synchronized (mLock) {
+ try {
+ if (mVirtualMachine != null && mVirtualMachine.isMemoryBalloonEnabled()) {
+ long bytes = mConfig.getMemoryBytes();
+ mVirtualMachine.setMemoryBalloon(bytes * percent / 100);
+ }
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.w(TAG, "Cannot setMemoryBalloon", e);
+ }
+ }
+ }
+
private boolean writeEventsToSock(ParcelFileDescriptor sock, List<InputEvent> evtList) {
ByteBuffer byteBuffer =
ByteBuffer.allocate(8 /* (type: u16 + code: u16 + value: i32) */ * evtList.size());