TerminalApp: Use sparse file for debian rootfs if storage balloon enabled
When storage ballooning is enabled, we can use a larger sparse disk
for debian rootfs.
Bug: 382174138
Test: Run a VM with the flag enabled
Change-Id: I9d9ee79558ab0fd75f4b92b1248777de80d2d155
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
index 7acc5f3..23bf48d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
@@ -25,9 +25,6 @@
import java.io.FileReader
import java.io.IOException
import java.io.RandomAccessFile
-import java.lang.IllegalArgumentException
-import java.lang.NumberFormatException
-import java.lang.RuntimeException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
@@ -91,6 +88,13 @@
}
@Throws(IOException::class)
+ fun getPhysicalSize(): Long {
+ val stat = RandomAccessFile(rootPartition.toFile(), "rw").use { raf -> Os.fstat(raf.fd) }
+ // The unit of st_blocks is 512 byte in Android.
+ return 512L * stat.st_blocks
+ }
+
+ @Throws(IOException::class)
fun getSmallestSizePossible(): Long {
runE2fsck(rootPartition)
val p: String = rootPartition.toAbsolutePath().toString()
@@ -128,6 +132,17 @@
return getSize()
}
+ @Throws(IOException::class)
+ fun truncate(size: Long) {
+ try {
+ RandomAccessFile(rootPartition.toFile(), "rw").use { raf -> Os.ftruncate(raf.fd, size) }
+ Log.d(TAG, "Truncated space to: $size bytes")
+ } catch (e: ErrnoException) {
+ Log.e(TAG, "Failed to allocate space", e)
+ throw IOException("Failed to allocate space", e)
+ }
+ }
+
companion object {
private const val INSTALL_DIRNAME = "linux"
private const val ROOTFS_FILENAME = "root_part"
@@ -147,10 +162,9 @@
@Throws(IOException::class)
private fun allocateSpace(path: Path, sizeInBytes: Long) {
try {
- val raf = RandomAccessFile(path.toFile(), "rw")
- val fd = raf.fd
- Os.posix_fallocate(fd, 0, sizeInBytes)
- raf.close()
+ RandomAccessFile(path.toFile(), "rw").use { raf ->
+ Os.posix_fallocate(raf.fd, 0, sizeInBytes)
+ }
Log.d(TAG, "Allocated space to: $sizeInBytes bytes")
} catch (e: ErrnoException) {
Log.e(TAG, "Failed to allocate space", e)
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 487b7e8..f4306bf 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -49,7 +49,7 @@
import androidx.viewpager2.widget.ViewPager2
import com.android.internal.annotations.VisibleForTesting
import com.android.microdroid.test.common.DeviceProperties
-import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
+import com.android.system.virtualmachine.flags.Flags
import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -126,9 +126,9 @@
}
displayMenu?.also {
- it.visibility = if (terminalGuiSupport()) View.VISIBLE else View.GONE
+ it.visibility = if (Flags.terminalGuiSupport()) View.VISIBLE else View.GONE
it.setEnabled(false)
- if (terminalGuiSupport()) {
+ if (Flags.terminalGuiSupport()) {
it.setOnClickListener {
val intent = Intent(this, DisplayActivity::class.java)
intent.flags =
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 067d540..f426ce6 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -31,6 +31,7 @@
import android.os.Parcel
import android.os.Parcelable
import android.os.ResultReceiver
+import android.os.StatFs
import android.os.SystemProperties
import android.system.virtualmachine.VirtualMachine
import android.system.virtualmachine.VirtualMachineCustomImageConfig
@@ -139,6 +140,30 @@
return START_NOT_STICKY
}
+ private fun truncateDiskIfNecessary(image: InstalledImage) {
+ val curSize = image.getSize()
+ val physicalSize = image.getPhysicalSize()
+
+ // Change the rootfs disk's apparent size to GUEST_SPARSE_DISK_SIZE_PERCENTAGE of the total
+ // disk size.
+ // Note that the physical size is not changed.
+ val statFs = StatFs(filesDir.absolutePath)
+ val hostSize = statFs.totalBytes
+ val expectedSize = hostSize * GUEST_SPARSE_DISK_SIZE_PERCENTAGE / 100
+ Log.d(
+ TAG,
+ "rootfs apparent size=$curSize, physical size=$physicalSize, expectedSize=$expectedSize",
+ )
+
+ if (curSize != expectedSize) {
+ try {
+ image.truncate(expectedSize)
+ } catch (e: IOException) {
+ throw RuntimeException("Failed to truncate a disk", e)
+ }
+ }
+ }
+
@WorkerThread
private fun doStart(
notification: Notification,
@@ -150,7 +175,15 @@
val json = ConfigJson.from(this, image.configPath)
val configBuilder = json.toConfigBuilder(this)
val customImageConfigBuilder = json.toCustomImageConfigBuilder(this)
- image.resize(diskSize)
+
+ if (Flags.terminalStorageBalloon()) {
+ // When storage ballooning flag is enabled, convert rootfs disk into a sparse file.
+ truncateDiskIfNecessary(image)
+ } else {
+ // Note: this doesn't always do the resizing. If the current image size is the same as
+ // the requested size which is rounded up to the page alignment, resizing is not done.
+ image.resize(diskSize)
+ }
customImageConfigBuilder.setAudioConfig(
AudioConfig.Builder().setUseSpeaker(true).setUseMicrophone(true).build()
@@ -445,6 +478,8 @@
private const val KEY_TERMINAL_IPADDRESS = "address"
private const val KEY_TERMINAL_PORT = "port"
+ private const val GUEST_SPARSE_DISK_SIZE_PERCENTAGE = 95
+
private val VM_BOOT_TIMEOUT_SECONDS: Int =
{
val deviceName = SystemProperties.get("ro.product.vendor.device", "")