From ca8e2f826b190c155c0363689af1380c4ab49a0f Mon Sep 17 00:00:00 2001 From: Ajesh R Date: Wed, 16 Sep 2026 12:36:57 +0530 Subject: [PATCH 1/2] Added memory metrics like Swap and Bitmap memory --- .../androidify/benchmark/PromptBenchmark.kt | 5 ++++- .../androidify/benchmark/StartupBenchmark.kt | 16 +++++++++------- gradle/libs.versions.toml | 6 +++--- settings.gradle.kts | 2 ++ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt index cfe51917..da682b95 100644 --- a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt +++ b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt @@ -58,13 +58,16 @@ class PromptBenchmark { metrics = listOf( StartupTimingMetric(), FrameTimingMetric(), - MemoryUsageMetric(MemoryUsageMetric.Mode.Max), + MemoryUsageMetric(MemoryUsageMetric.Mode.Max, subMetrics = MemoryUsageMetric.SubMetric.entries), PowerMetric(PowerMetric.Type.Power()), jitCompilationMetric, classInitMetric, ), startupMode = StartupMode.COLD, iterations = 5, + setupBlock = { + killProcess() + } ) { uiAutomator { startApp(packageName = packageName) diff --git a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt index f10bcebf..640e4357 100644 --- a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt +++ b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt @@ -27,6 +27,8 @@ import androidx.benchmark.macro.StartupTimingMetric import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.uiautomator.uiAutomator +import com.android.developers.androidify.classInitMetric +import com.android.developers.androidify.jitCompilationMetric import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -42,20 +44,20 @@ class StartupBenchmark { @Test fun startupBaselineProfile() = startup(CompilationMode.DEFAULT) - - @Test - fun startupFullCompilation() = startup(CompilationMode.Full()) - @OptIn(ExperimentalMetricApi::class) private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( packageName = "com.android.developers.androidify", metrics = listOf( StartupTimingMetric(), FrameTimingMetric(), - MemoryUsageMetric(MemoryUsageMetric.Mode.Max), - PowerMetric(PowerMetric.Type.Power()), + MemoryUsageMetric(MemoryUsageMetric.Mode.Max, subMetrics = MemoryUsageMetric.SubMetric.entries), + jitCompilationMetric, + classInitMetric, ), - iterations = 10, + setupBlock = { + killProcess() + }, + iterations = 5, compilationMode = compilationMode, startupMode = StartupMode.COLD, ) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d7da77e..4c2adc58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,8 +16,8 @@ activityCompose = "1.13.0" adaptive = "1.2.0" apksig = "9.2.1" appcompat = "1.7.1" -baselineprofile = "1.5.0-alpha06" -benchmarkMacroJunit4 = "1.4.1" +baselineprofile = "1.6.0-SNAPSHOT" +benchmarkMacroJunit4 = "1.6.0-SNAPSHOT" camerax = "1.6.1" coilCompose = "3.4.0" coilGif = "3.4.0" @@ -57,7 +57,7 @@ okhttp = "5.3.2" playServicesWearable = "20.0.1" playServicesBaseTesting = "16.2.0" poseDetection = "18.0.0-beta5" -profileinstaller = "1.4.1" +profileinstaller = "1.5.0-SNAPSHOT" retrofit = "3.0.0" robolectric = "4.16.1" spotless = "8.6.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index da670135..f6f6d236 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ pluginManagement { } mavenCentral() gradlePluginPortal() + maven { url = uri("https://androidx.dev/snapshots/builds/16349408/artifacts/repository") } } } dependencyResolutionManagement { @@ -16,6 +17,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://androidx.dev/snapshots/builds/16349408/artifacts/repository") } maven { url = uri("https://jitpack.io") content { From a124443d736a4afd0c316f6b341d46093f6d2db4 Mon Sep 17 00:00:00 2001 From: Ajesh R Date: Thu, 1 Oct 2026 22:39:29 +0530 Subject: [PATCH 2/2] Added memory metrics issues --- .../developers/androidify/MainActivity.kt | 14 ++ .../androidify/util/MemorySimulator.kt | 163 ++++++++++++++++++ benchmark/build.gradle.kts | 1 + .../androidify/benchmark/PromptBenchmark.kt | 2 +- .../androidify/benchmark/StartupBenchmark.kt | 23 ++- .../androidify/camera/CameraZoomToolbar.kt | 3 - 6 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/android/developers/androidify/util/MemorySimulator.kt diff --git a/app/src/main/java/com/android/developers/androidify/MainActivity.kt b/app/src/main/java/com/android/developers/androidify/MainActivity.kt index 9d2e5e24..9bc0bfe7 100644 --- a/app/src/main/java/com/android/developers/androidify/MainActivity.kt +++ b/app/src/main/java/com/android/developers/androidify/MainActivity.kt @@ -65,6 +65,20 @@ class MainActivity : ComponentActivity() { } } + override fun onResume() { + super.onResume() + // Replicate a high foreground footprint of 350MB + com.android.developers.androidify.util.MemorySimulator.startNativeBitmapBloat(targetMegaBytes = 350) + // Simulate Java Heap Swap bloat + com.android.developers.androidify.util.MemorySimulator.startJavaHeapSwapBloat(targetMegaBytes = 200) + } + + override fun onPause() { + super.onPause() + // Prevent background LMK (Low Memory Kills) during local testing + com.android.developers.androidify.util.MemorySimulator.reset() + } + override fun onAttachedToWindow() { super.onAttachedToWindow() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { diff --git a/app/src/main/java/com/android/developers/androidify/util/MemorySimulator.kt b/app/src/main/java/com/android/developers/androidify/util/MemorySimulator.kt new file mode 100644 index 00000000..a5295498 --- /dev/null +++ b/app/src/main/java/com/android/developers/androidify/util/MemorySimulator.kt @@ -0,0 +1,163 @@ +package com.android.developers.androidify.util + +import android.content.Context +import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import android.webkit.WebView +import java.io.File +import java.io.RandomAccessFile +import java.nio.ByteBuffer +import java.nio.MappedByteBuffer +import java.nio.channels.FileChannel +import java.util.UUID + +object MemorySimulator { + private val javaHeapLeaker = mutableListOf() + private val bitmapPool = mutableListOf() + private val directBuffers = mutableListOf() + private val mappedBuffers = mutableListOf() + private val webViews = mutableListOf() + private val mainHandler = Handler(Looper.getMainLooper()) + + /** + * Toggles massive Java Heap string allocations. + * Excellent for forcing the OS to swap compressed pages to zRAM (Swap). + */ + fun startJavaHeapSwapBloat(targetMegaBytes: Int, intervalMs: Long = 100) { + val bytesPerAlloc = 1024 * 1024 // 1 MB increments + val totalLoops = targetMegaBytes + + var allocated = 0 + mainHandler.post(object : Runnable { + override fun run() { + if (allocated < totalLoops) { + val stringChunk = StringBuilder().apply { + repeat(bytesPerAlloc / 2) { append(UUID.randomUUID().toString().take(2)) } + }.toString() + + javaHeapLeaker.add(stringChunk) + allocated++ + mainHandler.postDelayed(this, intervalMs) + } + } + }) + } + + /** + * Toggles Native Bitmap Allocation (Anon RSS). + * Directly inflates the private dirty native heap without exhausting JVM heap limits. + */ + fun startNativeBitmapBloat(targetMegaBytes: Int, intervalMs: Long = 200) { + val width = 512 + val height = 512 + var allocated = 0 + + mainHandler.post(object : Runnable { + override fun run() { + if (allocated < targetMegaBytes) { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + bitmap.eraseColor(android.graphics.Color.RED) + bitmapPool.add(bitmap) + allocated++ + mainHandler.postDelayed(this, intervalMs) + } + } + }) + } + + /** + * Toggles Direct Native Allocations (C/C++ malloc equivalent). + * Bypasses the JVM heap completely and allocates raw native memory. + */ + fun startDirectNativeBloat(targetMegaBytes: Int, intervalMs: Long = 100) { + var allocated = 0 + mainHandler.post(object : Runnable { + override fun run() { + if (allocated < targetMegaBytes) { + // Allocate 1MB of direct native memory + val buffer = ByteBuffer.allocateDirect(1024 * 1024) + // Touch the memory so the OS maps it to physical RAM + for (i in 0 until buffer.capacity() step 4096) { + buffer.put(i, 1) + } + directBuffers.add(buffer) + allocated++ + mainHandler.postDelayed(this, intervalMs) + } + } + }) + } + + /** + * Toggles the WebView "Hidden Tab" trick. + * Rapidly consumes massive amounts of memory via Chromium rendering processes. + */ + fun startWebViewBloat(context: Context, targetInstances: Int = 10, intervalMs: Long = 500) { + var allocated = 0 + mainHandler.post(object : Runnable { + override fun run() { + if (allocated < targetInstances) { + val webView = WebView(context).apply { + settings.javaScriptEnabled = true + // Load a heavy WebGL/rendering page (or just a heavy DOM) + loadUrl("https://threejs.org/examples/#webgl_animation_keyframes") + } + webViews.add(webView) + allocated++ + mainHandler.postDelayed(this, intervalMs) + } + } + }) + } + + /** + * Memory maps massive files to inflate the virtual memory footprint. + */ + fun startFileMmapBloat(context: Context, targetMegaBytes: Int) { + Thread { + try { + // Create a huge dummy file + val dummyFile = File(context.cacheDir, "huge_dummy_file.tmp") + val raf = RandomAccessFile(dummyFile, "rw") + val targetBytes = targetMegaBytes * 1024L * 1024L + raf.setLength(targetBytes) + + // mmap the file into memory + val channel = raf.channel + val mappedBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, targetBytes) + + // Touch the bytes to force physical allocation + for (i in 0 until targetBytes step 4096) { + mappedBuffer.put(i.toInt(), 1.toByte()) + } + + mappedBuffers.add(mappedBuffer) + } catch (e: Exception) { + e.printStackTrace() + } + }.start() + } + + /** + * Clears all simulated allocations to allow the OS to reclaim space. + */ + fun reset() { + mainHandler.removeCallbacksAndMessages(null) + javaHeapLeaker.clear() + + bitmapPool.forEach { it.recycle() } + bitmapPool.clear() + + directBuffers.clear() + mappedBuffers.clear() + + webViews.forEach { + it.stopLoading() + it.destroy() + } + webViews.clear() + + System.gc() + } +} diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index cb2fff38..a990396b 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -47,6 +47,7 @@ android { defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR" } buildTypes { diff --git a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt index da682b95..85940d0b 100644 --- a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt +++ b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/PromptBenchmark.kt @@ -67,7 +67,7 @@ class PromptBenchmark { iterations = 5, setupBlock = { killProcess() - } + }, ) { uiAutomator { startApp(packageName = packageName) diff --git a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt index 640e4357..97debe8c 100644 --- a/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt +++ b/benchmark/src/main/kotlin/com/android/developers/androidify/benchmark/StartupBenchmark.kt @@ -17,15 +17,20 @@ package com.android.developers.androidify.benchmark import android.os.Build import androidx.annotation.RequiresApi +import androidx.benchmark.ExperimentalBenchmarkConfigApi +import androidx.benchmark.ExperimentalConfig +import androidx.benchmark.MemoryProfilingConfig +import androidx.benchmark.macro.ArtMetric import androidx.benchmark.macro.CompilationMode import androidx.benchmark.macro.ExperimentalMetricApi import androidx.benchmark.macro.FrameTimingMetric import androidx.benchmark.macro.MemoryUsageMetric -import androidx.benchmark.macro.PowerMetric import androidx.benchmark.macro.StartupMode import androidx.benchmark.macro.StartupTimingMetric import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress import androidx.test.uiautomator.uiAutomator import com.android.developers.androidify.classInitMetric import com.android.developers.androidify.jitCompilationMetric @@ -34,7 +39,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@RequiresApi(Build.VERSION_CODES.Q) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) class StartupBenchmark { @get:Rule val benchmarkRule = MacrobenchmarkRule() @@ -44,15 +49,23 @@ class StartupBenchmark { @Test fun startupBaselineProfile() = startup(CompilationMode.DEFAULT) - @OptIn(ExperimentalMetricApi::class) + + @OptIn( + ExperimentalMetricApi::class, ExperimentalBenchmarkConfigApi::class, + ExperimentalPerfettoCaptureApi::class, + ) private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( packageName = "com.android.developers.androidify", metrics = listOf( StartupTimingMetric(), FrameTimingMetric(), MemoryUsageMetric(MemoryUsageMetric.Mode.Max, subMetrics = MemoryUsageMetric.SubMetric.entries), - jitCompilationMetric, - classInitMetric, + ), + experimentalConfig = ExperimentalConfig( + memoryProfilingConfig = MemoryProfilingConfig( + isSampleArtHeapEnabled = true, + isSampleNativeHeapEnabled = true, + ), ), setupBlock = { killProcess() diff --git a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraZoomToolbar.kt b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraZoomToolbar.kt index de4837f2..fe6a3444 100644 --- a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraZoomToolbar.kt +++ b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraZoomToolbar.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonGroup import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.material3.ToggleButton import androidx.compose.material3.ToggleButtonDefaults @@ -34,8 +33,6 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.android.developers.androidify.theme.AndroidifyTheme