Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>()
private val bitmapPool = mutableListOf<Bitmap>()
private val directBuffers = mutableListOf<ByteBuffer>()
private val mappedBuffers = mutableListOf<MappedByteBuffer>()
private val webViews = mutableListOf<WebView>()
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()
}
}
1 change: 1 addition & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ android {

defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
}

buildTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,29 @@ 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
import org.junit.Rule
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()
Expand All @@ -43,19 +50,27 @@ class StartupBenchmark {
@Test
fun startupBaselineProfile() = startup(CompilationMode.DEFAULT)

@Test
fun startupFullCompilation() = startup(CompilationMode.Full())

@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),
PowerMetric(PowerMetric.Type.Power()),
MemoryUsageMetric(MemoryUsageMetric.Mode.Max, subMetrics = MemoryUsageMetric.SubMetric.entries),
),
experimentalConfig = ExperimentalConfig(
memoryProfilingConfig = MemoryProfilingConfig(
isSampleArtHeapEnabled = true,
isSampleNativeHeapEnabled = true,
),
),
iterations = 10,
setupBlock = {
killProcess()
},
iterations = 5,
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ pluginManagement {
}
mavenCentral()
gradlePluginPortal()
maven { url = uri("https://androidx.dev/snapshots/builds/16349408/artifacts/repository") }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://androidx.dev/snapshots/builds/16349408/artifacts/repository") }
maven {
url = uri("https://jitpack.io")
content {
Expand Down
Loading