Terminal: Resize sparse disk when storage balloon is turned off
When the storage balloon feature is enabled via an aconfig flag,
the rootfs is converted to a sparse file, increasing its apparent
size. When the flag is disabled again afterwards (e.g. rollback
or local testing), the disk needs to be converted back to
non-sparse. Otherwise, the disk resize settings screen would
display its apparent size which is too big.
This CL addresses this issue by shrinking the disk size with
resize2fs if the rootfs is a sparse file while the storage balloon
feature is disabled
Bug: 382174138
Test: enable 'terminal_storage_balloon' and disable it. Check disk
size setting.
Change-Id: Ifadaa709b8f74dc99b68c9088d6a4fda06805a98
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
index 23bf48d..a6765ba 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
@@ -83,7 +83,7 @@
}
@Throws(IOException::class)
- fun getSize(): Long {
+ fun getApparentSize(): Long {
return Files.size(rootPartition)
}
@@ -118,7 +118,7 @@
@Throws(IOException::class)
fun resize(desiredSize: Long): Long {
val roundedUpDesiredSize = roundUp(desiredSize)
- val curSize = getSize()
+ val curSize = getApparentSize()
if (roundedUpDesiredSize == curSize) {
return roundedUpDesiredSize
@@ -129,7 +129,21 @@
allocateSpace(rootPartition, roundedUpDesiredSize)
}
resizeFilesystem(rootPartition, roundedUpDesiredSize)
- return getSize()
+ return getApparentSize()
+ }
+
+ @Throws(IOException::class)
+ fun shrinkToMinimumSize(): Long {
+ // Fix filesystem before resizing.
+ runE2fsck(rootPartition)
+
+ val p: String = rootPartition.toAbsolutePath().toString()
+ runCommand("/system/bin/resize2fs", "-M", p)
+ Log.d(TAG, "resize2fs -M completed: $rootPartition")
+
+ // resize2fs may result in an inconsistent filesystem state. Fix with e2fsck.
+ runE2fsck(rootPartition)
+ return getApparentSize()
}
@Throws(IOException::class)
@@ -212,7 +226,7 @@
}
}
- private fun roundUp(bytes: Long): Long {
+ internal fun roundUp(bytes: Long): Long {
// Round up every diskSizeStep MB
return ceil((bytes.toDouble()) / RESIZE_STEP_BYTES).toLong() * RESIZE_STEP_BYTES
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index f4306bf..ddb0dcd 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -50,6 +50,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.microdroid.test.common.DeviceProperties
import com.android.system.virtualmachine.flags.Flags
+import com.android.virtualization.terminal.ErrorActivity.Companion.start
import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -355,7 +356,7 @@
)
.build()
- val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getSize())
+ val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getApparentSize())
val intent =
VmLauncherService.getIntentForStart(
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index af1ae95..144db40 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -71,7 +71,7 @@
diskSizeStepMb = 1L shl resources.getInteger(R.integer.disk_size_round_up_step_size_in_mb)
val image = InstalledImage.getDefault(this)
- diskSizeMb = bytesToMb(image.getSize())
+ diskSizeMb = bytesToMb(image.getApparentSize())
val minDiskSizeMb = bytesToMb(image.getSmallestSizePossible()).coerceAtMost(diskSizeMb)
val usableSpaceMb =
bytesToMb(Environment.getDataDirectory().getUsableSpace()) and
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index f426ce6..d43a8d1 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -41,6 +41,7 @@
import android.widget.Toast
import androidx.annotation.WorkerThread
import com.android.system.virtualmachine.flags.Flags
+import com.android.virtualization.terminal.InstalledImage.Companion.roundUp
import com.android.virtualization.terminal.MainActivity.Companion.PREFIX
import com.android.virtualization.terminal.MainActivity.Companion.TAG
import io.grpc.Grpc
@@ -120,7 +121,7 @@
// 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.
- val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getSize())
+ val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getApparentSize())
mainWorkerThread.submit({
doStart(notification, displayInfo, diskSize, resultReceiver)
@@ -140,16 +141,18 @@
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.
+ private fun calculateSparseDiskSize(): Long {
+ // With storage ballooning enabled, we create a sparse file with 95% of the total size.
val statFs = StatFs(filesDir.absolutePath)
val hostSize = statFs.totalBytes
- val expectedSize = hostSize * GUEST_SPARSE_DISK_SIZE_PERCENTAGE / 100
+ return roundUp(hostSize * GUEST_SPARSE_DISK_SIZE_PERCENTAGE / 100)
+ }
+
+ private fun truncateDiskIfNecessary(image: InstalledImage) {
+ val curSize = image.getApparentSize()
+ val physicalSize = image.getPhysicalSize()
+
+ val expectedSize = calculateSparseDiskSize()
Log.d(
TAG,
"rootfs apparent size=$curSize, physical size=$physicalSize, expectedSize=$expectedSize",
@@ -164,6 +167,38 @@
}
}
+ // Convert the rootfs disk to a non-sparse file.
+ private fun convertToNonSparseDiskIfNecessary(image: InstalledImage) {
+ try {
+ val curApparentSize = image.getApparentSize()
+ val curPhysicalSize = image.getPhysicalSize()
+ Log.d(TAG, "Current disk size: apparent=$curApparentSize, physical=$curPhysicalSize")
+
+ // If storage ballooning was enabled via Flags.terminalStorageBalloon() before but it's
+ // now disabled, the disk is still a sparse file whose apparent size is too large.
+ // We need to shrink it to the minimum size.
+ //
+ // The disk file is considered sparse if its apparent disk size matches the expected
+ // sparse disk size.
+ // In addition, we consider it sparse if the physical size is clearly smaller than its
+ // apparent size. This additional condition is a fallback for cases
+ // where the logic of calculating the expected sparse disk size since the disk is
+ // created.
+ if (
+ curApparentSize == calculateSparseDiskSize() ||
+ curPhysicalSize <
+ curApparentSize * EXPECTED_PHYSICAL_SIZE_PERCENTAGE_FOR_NON_SPARSE / 100
+ ) {
+ Log.d(TAG, "A sparse disk is detected. Shrink it to the minimum size.")
+ val newSize = image.shrinkToMinimumSize()
+ Log.d(TAG, "Shrink the disk image: $curApparentSize -> $newSize")
+ }
+ } catch (e: IOException) {
+ throw RuntimeException("Failed to shrink rootfs disk", e)
+ return
+ }
+ }
+
@WorkerThread
private fun doStart(
notification: Notification,
@@ -180,6 +215,10 @@
// When storage ballooning flag is enabled, convert rootfs disk into a sparse file.
truncateDiskIfNecessary(image)
} else {
+ // Convert rootfs disk into a sparse file if storage ballooning flag had been enabled
+ // and then disabled.
+ convertToNonSparseDiskIfNecessary(image)
+
// 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)
@@ -479,6 +518,7 @@
private const val KEY_TERMINAL_PORT = "port"
private const val GUEST_SPARSE_DISK_SIZE_PERCENTAGE = 95
+ private const val EXPECTED_PHYSICAL_SIZE_PERCENTAGE_FOR_NON_SPARSE = 90
private val VM_BOOT_TIMEOUT_SECONDS: Int =
{