Skip to content
Merged
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
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".AndroidPerfLabApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.aquib.androidperflab

import android.app.Application
import android.util.Log
import com.aquib.androidperflab.sdk.FakeAnalyticsSdk
import com.aquib.androidperflab.sdk.FakeCrashReportingSdk
import com.aquib.androidperflab.sdk.FakeFeatureFlagsSdk
import com.aquib.androidperflab.sdk.FakePerformanceMonitorSdk
import com.aquib.androidperflab.sdk.FakeRemoteConfigSdk

class AndroidPerfLabApplication : Application() {

override fun onCreate() {
super.onCreate()

// Intentionally bad: all five SDKs are initialised synchronously on the main thread.
// Each blocks via Thread.sleep() to simulate the I/O, disk, and network work that
// real SDKs perform. Total cold-start penalty ≈ 750 ms before the first frame.

val t0 = System.currentTimeMillis()

FakeCrashReportingSdk.init(this) // ~120 ms — must be first to catch early crashes
Log.d("AppStartup", "CrashReporting ready +${System.currentTimeMillis() - t0}ms")

FakeAnalyticsSdk.init(this) // ~180 ms
Log.d("AppStartup", "Analytics ready +${System.currentTimeMillis() - t0}ms")

FakeFeatureFlagsSdk.init(this) // ~150 ms
Log.d("AppStartup", "FeatureFlags ready +${System.currentTimeMillis() - t0}ms")

FakeRemoteConfigSdk.init(this) // ~200 ms
Log.d("AppStartup", "RemoteConfig ready +${System.currentTimeMillis() - t0}ms")

FakePerformanceMonitorSdk.init(this) // ~100 ms — last so it can baseline the others
Log.d("AppStartup", "PerfMonitor ready +${System.currentTimeMillis() - t0}ms")

Log.d("AppStartup", "Application.onCreate() complete — total blocked=${System.currentTimeMillis() - t0}ms")
}
}
31 changes: 2 additions & 29 deletions app/src/main/java/com/aquib/androidperflab/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.aquib.androidperflab.ui.HomeScreen
import com.aquib.androidperflab.ui.theme.AndroidPerfLabTheme

class MainActivity : ComponentActivity() {
Expand All @@ -19,29 +13,8 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
AndroidPerfLabTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
HomeScreen()
}
}
}
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AndroidPerfLabTheme {
Greeting("Android")
}
}
42 changes: 42 additions & 0 deletions app/src/main/java/com/aquib/androidperflab/sdk/FakeAnalyticsSdk.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.aquib.androidperflab.sdk

import android.content.Context
import android.os.Build
import android.util.Log

object FakeAnalyticsSdk {

// Simulates: opening a local SQLite event queue, reading persisted user identity from
// SharedPreferences, building a device fingerprint, and doing a blocking fetch of
// the remote analytics endpoint — all on the main thread.
fun init(context: Context) {
// Simulate: hydrating the in-memory event schema (heavy map allocation)
val eventSchema = HashMap<String, String>(512)
repeat(500) { i -> eventSchema["event_type_$i"] = "schema_v2_$i" }

// Simulate: reading / writing user identity from SharedPreferences
val prefs = context.getSharedPreferences("fake_analytics", Context.MODE_PRIVATE)
val userId = prefs.getString("user_id", null) ?: run {
val generated = System.nanoTime().toString(16)
// commit() instead of apply() — blocks until disk write completes
prefs.edit().putString("user_id", generated).commit()
generated
}
val sessionIndex = prefs.getInt("session_index", 0) + 1
prefs.edit().putInt("session_index", sessionIndex).commit()

// Simulate: device fingerprinting (string concat over multiple Build fields)
@Suppress("DEPRECATION")
val fingerprint = buildString {
append(Build.MANUFACTURER); append('_')
append(Build.MODEL); append('_')
append(Build.DEVICE); append('_')
append(Build.FINGERPRINT.takeLast(12))
}

// Simulate: blocking endpoint resolution + SDK handshake over network
Thread.sleep(180L)

Log.d("FakeAnalyticsSdk", "init complete — user=$userId session=$sessionIndex fp=$fingerprint schema=${eventSchema.size} entries")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.aquib.androidperflab.sdk

import android.content.Context
import android.util.Log
import java.io.File

object FakeCrashReportingSdk {

// Simulates: scanning the cache directory for pending crash dumps left by a previous
// session, installing an UncaughtExceptionHandler, and uploading any found dumps
// synchronously before the app is considered "ready".
fun init(context: Context) {
// Simulate: scanning cache dir for pending crash dump files
val cacheDir = context.cacheDir
val crashDumps = cacheDir.listFiles { f -> f.name.startsWith("crash_") }
?.toList()
?: emptyList()

// Simulate: parsing each crash dump (string + I/O work per file)
val parsedReports = crashDumps.map { file ->
buildString {
append("report["); append(file.name); append("]: ")
append(file.length()); append(" bytes, modified=")
append(file.lastModified())
}
}

// Simulate: writing a session sentinel file so the next launch can detect
// whether this session ended cleanly
val sentinel = File(cacheDir, "crash_sentinel_${System.currentTimeMillis()}.tmp")
sentinel.createNewFile()

// Simulate: registering the uncaught exception handler (involves thread locking)
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
Log.e("FakeCrashReportingSdk", "Uncaught exception on ${thread.name}", throwable)
previousHandler?.uncaughtException(thread, throwable)
}

// Simulate: blocking upload of any pending crash reports to the backend
Thread.sleep(120L)

Log.d("FakeCrashReportingSdk", "init complete — found ${crashDumps.size} pending dumps, parsed ${parsedReports.size} reports")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.aquib.androidperflab.sdk

import android.content.Context
import android.util.Log

object FakeFeatureFlagsSdk {

private val flags = HashMap<String, Boolean>()

// Simulates: parsing a large bundled JSON payload of flag definitions, evaluating
// per-user targeting rules against locally stored user attributes, and persisting
// the resolved flag state — all synchronously on the main thread.
fun init(context: Context) {
// Simulate: deserialising bundled flag defaults (CPU-bound JSON parsing)
val rawPayload = buildString {
repeat(200) { i ->
append("""{"flag":"feature_$i","enabled":${i % 3 != 0},"rollout":${(i * 7) % 100}},""")
}
}

// Simulate: evaluating targeting rules for each flag
val prefs = context.getSharedPreferences("fake_feature_flags", Context.MODE_PRIVATE)
val userSegment = prefs.getInt("user_segment", (System.nanoTime() % 100).toInt())
prefs.edit().putInt("user_segment", userSegment).commit()

repeat(200) { i ->
val rollout = (i * 7) % 100
flags["feature_$i"] = (i % 3 != 0) && (userSegment < rollout)
}

val enabledCount = flags.values.count { it }

// Simulate: blocking network sync to fetch any server-side flag overrides
Thread.sleep(150L)

Log.d("FakeFeatureFlagsSdk", "init complete — ${flags.size} flags resolved, $enabledCount enabled for segment=$userSegment payload=${rawPayload.length} chars")
}

fun isEnabled(flag: String): Boolean = flags[flag] ?: false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.aquib.androidperflab.sdk

import android.app.ActivityManager
import android.content.Context
import android.os.Debug
import android.os.Process
import android.util.Log

object FakePerformanceMonitorSdk {

// Simulates: snapshotting baseline memory stats, reading /proc/self/status for CPU
// accounting, and installing a frame-timing callback — all synchronously on the main
// thread before the first frame is drawn.
fun init(context: Context) {
// Simulate: collecting baseline memory snapshot
val runtime = Runtime.getRuntime()
val totalMemory = runtime.totalMemory()
val freeMemory = runtime.freeMemory()
val nativeHeap = Debug.getNativeHeapSize()

// Simulate: reading ActivityManager memory info
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memInfo)

// Simulate: reading per-process CPU stats from procfs (string parsing)
val pid = Process.myPid()
val procStatLines = buildString {
repeat(80) { i -> append("cpu_stat_field_${i}=${pid + i}\n") }
}.lines()

// Simulate: building baseline metric registry (100 named counters)
val metrics = HashMap<String, Long>(128)
repeat(100) { i ->
metrics["metric_$i"] = System.nanoTime() + i
}

// Simulate: GC pause measurement — forces a GC and measures its cost
val gcBefore = Debug.getRuntimeStat("art.gc.gc-count")
System.gc()
val gcAfter = Debug.getRuntimeStat("art.gc.gc-count")

// Simulate: installing frame-timing infrastructure (method tracing warmup)
Thread.sleep(100L)

Log.d(
"FakePerfMonitorSdk",
"init complete — heap=${totalMemory / 1024}KB free=${freeMemory / 1024}KB " +
"nativeHeap=${nativeHeap / 1024}KB availMem=${memInfo.availMem / 1024}KB " +
"procStatLines=${procStatLines.size} metrics=${metrics.size} gc=$gcBefore→$gcAfter",
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.aquib.androidperflab.sdk

import android.content.Context
import android.util.Log

object FakeRemoteConfigSdk {

private val config = HashMap<String, String>()

// Simulates: reading the last-fetched remote config blob from SharedPreferences,
// validating an HMAC checksum, deserialising key-value pairs, and resolving
// parameter overrides — synchronously blocking the main thread.
fun init(context: Context) {
val prefs = context.getSharedPreferences("fake_remote_config", Context.MODE_PRIVATE)

// Simulate: reading + validating a previously cached config blob
val cachedBlob = prefs.getString("config_blob", null)
val storedChecksum = prefs.getString("config_checksum", "")

if (cachedBlob != null) {
// Simulate: checksum validation (string hashing)
val computedChecksum = cachedBlob.fold(0L) { acc, c -> acc * 31 + c.code }.toString(16)
val valid = computedChecksum == storedChecksum
Log.d("FakeRemoteConfigSdk", "cache checksum valid=$valid")
}

// Simulate: building a new in-memory config with 150 keys
val newBlob = buildString {
repeat(150) { i ->
val key = "config_key_$i"
val value = "value_${i}_${System.nanoTime() % 1000}"
config[key] = value
append("$key=$value;")
}
}
val checksum = newBlob.fold(0L) { acc, c -> acc * 31 + c.code }.toString(16)
prefs.edit()
.putString("config_blob", newBlob)
.putString("config_checksum", checksum)
.commit()

// Simulate: blocking fetch of fresh config values from the remote endpoint
Thread.sleep(200L)

Log.d("FakeRemoteConfigSdk", "init complete — ${config.size} keys loaded, checksum=$checksum")
}

fun getString(key: String, default: String = ""): String = config[key] ?: default
}
7 changes: 7 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ benchmarkMacro = "1.5.0-alpha05"
profileinstaller = "1.4.1"
uiautomator = "2.3.0"

# Coil
coil = "3.0.4"

# Test
junit = "4.13.2"
junitVersion = "1.2.1"
Expand Down Expand Up @@ -50,6 +53,10 @@ androidx-test-uiautomator = { group = "androidx.test.uiautomator", name =
# Baseline Profiles — consumer side (installed into the APK)
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }

# Coil
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }

# Test
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
Expand Down
2 changes: 2 additions & 0 deletions ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose.ui)
implementation(libs.androidx.core.ktx)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)

debugImplementation(libs.androidx.compose.ui.tooling)

Expand Down
Loading
Loading