Refactor Memory Balloon control routines
* Created MemBalloonController as the single place for all routines
* Stop using binder; it actually wasn't a binder object.
* The periodic inflation is handled using scheduled executor service
Bug: 402025547
Bug: 392791968
Test: intentionally set the period to 5s (for testing purpose), start
the VM, go to background, and then come back. The log is as follows:
03-13 15:59:30.631 8298 8361 V VmTerminalApp: app resumed. deflating mem balloon to the minimum
03-13 15:59:51.863 8298 8361 V VmTerminalApp: inflating mem balloon to 10 %
03-13 15:59:56.863 8298 8361 V VmTerminalApp: inflating mem balloon to 15 %
03-13 16:00:01.864 8298 8361 V VmTerminalApp: inflating mem balloon to 20 %
03-13 16:00:06.864 8298 8361 V VmTerminalApp: inflating mem balloon to 25 %
03-13 16:00:08.315 8298 8361 V VmTerminalApp: app resumed. deflating mem balloon to the minimum
03-13 16:00:22.002 8298 8361 V VmTerminalApp: inflating mem balloon to 10 %
03-13 16:00:27.003 8298 8361 V VmTerminalApp: inflating mem balloon to 15 %
03-13 16:00:32.003 8298 8361 V VmTerminalApp: inflating mem balloon to 20 %
03-13 16:00:37.003 8298 8361 V VmTerminalApp: inflating mem balloon to 25 %
03-13 16:00:42.003 8298 8361 V VmTerminalApp: inflating mem balloon to 30 %
Change-Id: I05e1f8d3635d14924fc1b7b4222d7bbd7e57b6c5
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
index c427337..efe651e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
@@ -18,21 +18,12 @@
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() {
@@ -61,45 +52,4 @@
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)
- }
-
- 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
deleted file mode 100644
index 4e26c3c..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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/MemBalloonController.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
new file mode 100644
index 0000000..e2f9add
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 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
+
+import android.content.Context
+import android.system.virtualmachine.VirtualMachine
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import com.android.virtualization.terminal.MainActivity.Companion.TAG
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+/**
+ * MemBalloonController is responsible for adjusting the memory ballon size of a VM depending on
+ * whether the app is visible or running in the background
+ */
+class MemBalloonController(val context: Context, val vm: VirtualMachine) {
+ companion object {
+ private const val INITIAL_PERCENT = 10
+ private const val MAX_PERCENT = 50
+ private const val INFLATION_STEP_PERCENT = 5
+ private const val INFLATION_PERIOD_SEC = 60L
+ }
+
+ private val executor =
+ Executors.newSingleThreadScheduledExecutor(
+ TerminalThreadFactory(context.getApplicationContext())
+ )
+
+ private val observer =
+ object : DefaultLifecycleObserver {
+
+ // If the app is started or resumed, give deflate the balloon to 0 to give maximum
+ // available memory to the virtual machine
+ override fun onResume(owner: LifecycleOwner) {
+ ongoingInflation?.cancel(false)
+ executor.submit({
+ Log.v(TAG, "app resumed. deflating mem balloon to the minimum")
+ vm.setMemoryBalloonByPercent(0)
+ })
+ }
+
+ // If the app goes into background, progressively inflate the balloon from
+ // INITIAL_PERCENT until it reaches MAX_PERCENT
+ override fun onStop(owner: LifecycleOwner) {
+ ongoingInflation?.cancel(false)
+ balloonPercent = INITIAL_PERCENT
+ ongoingInflation =
+ executor.scheduleAtFixedRate(
+ {
+ if (balloonPercent <= MAX_PERCENT) {
+ Log.v(TAG, "inflating mem balloon to ${balloonPercent} %")
+ vm.setMemoryBalloonByPercent(balloonPercent)
+ balloonPercent += INFLATION_STEP_PERCENT
+ } else {
+ Log.v(TAG, "mem balloon is inflated to its max (${MAX_PERCENT} %)")
+ ongoingInflation!!.cancel(false)
+ }
+ },
+ 0 /* initialDelay */,
+ INFLATION_PERIOD_SEC,
+ TimeUnit.SECONDS,
+ )
+ }
+ }
+
+ private var balloonPercent = 0
+ private var ongoingInflation: ScheduledFuture<*>? = null
+
+ fun start() {
+ ProcessLifecycleOwner.get().lifecycle.addObserver(observer)
+ }
+
+ fun stop() {
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(observer)
+ executor.shutdown()
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 2d7468d..1857175 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -38,7 +38,6 @@
import android.system.virtualmachine.VirtualMachineException
import android.util.Log
import android.widget.Toast
-import com.android.internal.annotations.GuardedBy
import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
import com.android.virtualization.terminal.MainActivity.Companion.TAG
import com.android.virtualization.terminal.Runner.Companion.create
@@ -55,7 +54,6 @@
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
-import java.lang.Math.min
import java.lang.RuntimeException
import java.net.InetSocketAddress
import java.net.SocketAddress
@@ -66,12 +64,6 @@
import java.util.concurrent.TimeUnit
class VmLauncherService : Service() {
- inner class VmLauncherServiceBinder : android.os.Binder() {
- fun getService(): VmLauncherService = this@VmLauncherService
- }
-
- private val binder = VmLauncherServiceBinder()
-
private lateinit var executorService: ExecutorService
// TODO: using lateinit for some fields to avoid null
@@ -80,42 +72,6 @@
private var server: Server? = null
private var debianService: DebianServiceImpl? = null
private var portNotifier: PortNotifier? = null
- private var mLock = Object()
- @GuardedBy("mLock") private var currentMemBalloonPercent = 0
-
- @GuardedBy("mLock") private val inflateMemBalloonHandler = Handler(Looper.getMainLooper())
- private val inflateMemBalloonTask: Runnable =
- object : Runnable {
- override fun run() {
- synchronized(mLock) {
- if (
- currentMemBalloonPercent < INITIAL_MEM_BALLOON_PERCENT ||
- currentMemBalloonPercent > MAX_MEM_BALLOON_PERCENT
- ) {
- Log.e(
- TAG,
- "currentBalloonPercent=$currentMemBalloonPercent is invalid," +
- " should be in range: " +
- "$INITIAL_MEM_BALLOON_PERCENT~$MAX_MEM_BALLOON_PERCENT",
- )
- return
- }
- // Increases the balloon size by MEM_BALLOON_PERCENT_STEP% every time
- if (currentMemBalloonPercent < MAX_MEM_BALLOON_PERCENT) {
- currentMemBalloonPercent =
- min(
- MAX_MEM_BALLOON_PERCENT,
- currentMemBalloonPercent + MEM_BALLOON_PERCENT_STEP,
- )
- virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
- inflateMemBalloonHandler.postDelayed(
- this,
- MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
- )
- }
- }
- }
- }
interface VmLauncherServiceCallback {
fun onVmStart()
@@ -128,45 +84,7 @@
}
override fun onBind(intent: Intent?): IBinder? {
- 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 -> {
- synchronized(mLock) {
- inflateMemBalloonHandler.removeCallbacks(inflateMemBalloonTask)
- currentMemBalloonPercent = 0
- virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
- }
- }
- ApplicationLifeCycleEvent.APP_ON_STOP -> {
- // When the app stops, inflate the memory balloon to INITIAL_MEM_BALLOON_PERCENT.
- // Inflate the balloon by MEM_BALLOON_PERCENT_STEP every
- // MEM_BALLOON_INFLATE_INTERVAL_MILLIS milliseconds until reaching
- // MAX_MEM_BALLOON_PERCENT of total memory. This allows the system to reclaim
- // memory while the app is in the background.
- synchronized(mLock) {
- currentMemBalloonPercent = INITIAL_MEM_BALLOON_PERCENT
- virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
- inflateMemBalloonHandler.postDelayed(
- inflateMemBalloonTask,
- MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
- )
- }
- }
- else -> {
- Log.e(TAG, "unrecognized lifecycle event: $event")
- }
- }
+ return null
}
override fun onCreate() {
@@ -220,7 +138,11 @@
ResultReceiver::class.java,
)
+ val mbc = MemBalloonController(this, virtualMachine!!)
+ mbc.start()
+
runner.exitStatus.thenAcceptAsync { success: Boolean ->
+ mbc.stop()
resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
stopSelf()
}
@@ -492,11 +414,6 @@
}
}()
- private const val INITIAL_MEM_BALLOON_PERCENT = 10
- private const val MAX_MEM_BALLOON_PERCENT = 50
- private const val MEM_BALLOON_INFLATE_INTERVAL_MILLIS = 60000L
- private const val MEM_BALLOON_PERCENT_STEP = 5
-
private fun getMyIntent(context: Context): Intent {
return Intent(context.getApplicationContext(), VmLauncherService::class.java)
}