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
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose.ui)

// App Startup: single ContentProvider for all SDK initializers (no per-SDK provider).
implementation(libs.androidx.startup.runtime)
// Coroutines: Dispatchers.IO for background SDK work, Dispatchers.Main for the app scope.
implementation(libs.kotlinx.coroutines.android)

// Installs Baseline Profiles at first launch (ART pre-compilation).
implementation(libs.androidx.profileinstaller)

Expand Down
32 changes: 30 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,45 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidPerfLab">

<!--
App Startup: InitializationProvider consolidates all Initializer components
into a single ContentProvider, replacing the pattern where each SDK ships its
own provider (which adds ~2–5ms per provider to cold-start time).

Only CrashReportingInitializer runs here (before Application.onCreate).
Non-critical SDKs are NOT listed — they are triggered from Application.onCreate()
on background threads via AppInitializer.initializeComponent().
-->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.aquib.androidperflab.startup.CrashReportingInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.aquib.androidperflab.startup.FeatureFlagsInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.aquib.androidperflab.startup.PerfMonitorInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.aquib.androidperflab.startup.RemoteConfigInitializer"
android:value="androidx.startup" />
</provider>

<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.AndroidPerfLab">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,61 @@ 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
import androidx.startup.AppInitializer
import com.aquib.androidperflab.startup.AnalyticsInitializer
import com.aquib.androidperflab.startup.FeatureFlagsInitializer
import com.aquib.androidperflab.startup.PerfMonitorInitializer
import com.aquib.androidperflab.startup.RemoteConfigInitializer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class AndroidPerfLabApplication : Application() {

// Initialized at property-declaration time (before Application.onCreate and before
// ContentProviders start). CrashReportingInitializer accesses this scope to fire
// its background upload job — which is why it must be a property, not set in onCreate.
// SupervisorJob: a failing child coroutine does not cancel sibling coroutines.
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

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")
// CrashReportingInitializer ran synchronously via InitializationProvider BEFORE
// this method. The exception handler is already registered; its upload coroutine
// is already running on Dispatchers.IO. Nothing to do here for crash reporting.
val appInit = AppInitializer.getInstance(this)

// Non-critical SDKs (Analytics, PerfMonitor): launch immediately on IO.
// AppInitializer.initializeComponent() is idempotent — safe to call even if
// a dependency was already initialized via the manifest.
// Both SDKs initialize in ~180 ms + ~100 ms but never touch the main thread.
applicationScope.launch(Dispatchers.IO) {
appInit.initializeComponent(AnalyticsInitializer::class.java)
// PerfMonitor declares Analytics as a dependency in the initializer graph,
// so AppInitializer will invoke Analytics.create() first automatically —
// the explicit sequence here is just for clarity.
appInit.initializeComponent(PerfMonitorInitializer::class.java)
}

// Lazy SDKs (FeatureFlags, RemoteConfig): deferred by 500 ms so they never
// compete with Compose's first layout-and-draw pass. Both SDKs return safe
// defaults until their background coroutines complete, so the UI is never gated
// on their initialization.
applicationScope.launch(Dispatchers.IO) {
delay(500L)
appInit.initializeComponent(FeatureFlagsInitializer::class.java)
appInit.initializeComponent(RemoteConfigInitializer::class.java)
}

Log.d(TAG, "Application.onCreate() returned in ${System.currentTimeMillis() - t0} ms " +
"— all SDKs initializing in background")
}

Log.d("AppStartup", "Application.onCreate() complete — total blocked=${System.currentTimeMillis() - t0}ms")
companion object {
private const val TAG = "AppStartup"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,32 @@ 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
// ── Fast path — called synchronously from CrashReportingInitializer ──────────
//
// Only registers the UncaughtExceptionHandler. Safe to call from any thread.
// Separated from uploadPendingReports() so the handler is installed before any
// other SDK work begins — matching the original "must be first" requirement —
// without blocking the main thread for the full 120 ms upload simulation.

fun registerHandler(context: Context) {
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
Log.e("FakeCrashReportingSdk", "Uncaught exception on ${thread.name}", throwable)
previousHandler?.uncaughtException(thread, throwable)
}
Log.d("FakeCrashReportingSdk", "registerHandler complete")
}

// ── Slow path — called from Dispatchers.IO via CrashReportingInitializer ─────
//
// Simulates: scanning the cache directory for pending crash dumps, parsing them,
// writing a session sentinel, and uploading reports to the backend. All I/O and
// the blocking upload sleep are safe on a background thread.

fun uploadPendingReports(context: Context) {
val cacheDir = context.cacheDir

// Simulate: scanning cache dir for pending crash dump files
val crashDumps = cacheDir.listFiles { f -> f.name.startsWith("crash_") }
?.toList()
?: emptyList()
Expand All @@ -25,21 +45,17 @@ object FakeCrashReportingSdk {
}
}

// Simulate: writing a session sentinel file so the next launch can detect
// whether this session ended cleanly
// Simulate: writing a session sentinel so the next launch can detect clean exits
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
// Simulate: blocking upload of pending reports to the backend
Thread.sleep(120L)

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

import android.content.Context
import android.util.Log
import androidx.startup.Initializer
import com.aquib.androidperflab.AndroidPerfLabApplication
import com.aquib.androidperflab.sdk.FakeAnalyticsSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
* NOT registered in AndroidManifest.xml — triggered manually from Application.onCreate()
* via AppInitializer.initializeComponent() on Dispatchers.IO.
*
* create() returns immediately after launching the background coroutine; the ~180 ms
* SDK init never touches the main thread. CrashReportingInitializer is declared as a
* dependency so the exception handler is guaranteed to be registered before any SDK
* code runs (AppInitializer resolves the dependency graph automatically).
*/
class AnalyticsInitializer : Initializer<Unit> {
override fun create(context: Context) {
val app = context.applicationContext as AndroidPerfLabApplication
app.applicationScope.launch(Dispatchers.IO) {
FakeAnalyticsSdk.init(context.applicationContext)
Log.d("AppStartup", "Analytics ready")
}
Log.d("AppStartup", "AnalyticsInitializer.create() returned")
}

override fun dependencies(): List<Class<out Initializer<*>>> =
listOf(CrashReportingInitializer::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.aquib.androidperflab.startup

import android.content.Context
import android.util.Log
import androidx.startup.Initializer
import com.aquib.androidperflab.AndroidPerfLabApplication
import com.aquib.androidperflab.sdk.FakeCrashReportingSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
* Runs via InitializationProvider BEFORE Application.onCreate().
*
* Critical path (main thread, synchronous):
* FakeCrashReportingSdk.registerHandler() — installs the UncaughtExceptionHandler.
* Cost: < 1 ms.
*
* Non-critical path (Dispatchers.IO, fire-and-forget):
* FakeCrashReportingSdk.uploadPendingReports() — scans crash dumps and simulates upload.
* Cost: ~120 ms off the main thread.
*
* applicationScope is initialized at property-declaration time in AndroidPerfLabApplication,
* so it exists before this Initializer runs (the Application object is created before
* ContentProviders are started).
*/
class CrashReportingInitializer : Initializer<Unit> {

override fun create(context: Context) {
val app = context.applicationContext as AndroidPerfLabApplication

// Synchronous — registers the exception handler before any other SDK work.
FakeCrashReportingSdk.registerHandler(context.applicationContext)

// Asynchronous — upload is I/O bound and does not need to complete before
// the first frame; move it off the critical path entirely.
app.applicationScope.launch(Dispatchers.IO) {
FakeCrashReportingSdk.uploadPendingReports(context.applicationContext)
Log.d("AppStartup", "CrashReporting upload complete")
}

Log.d("AppStartup", "CrashReportingInitializer.create() returned")
}

// No dependencies — CrashReporting must be the root of the initializer graph.
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.aquib.androidperflab.startup

import android.content.Context
import android.util.Log
import androidx.startup.Initializer
import com.aquib.androidperflab.AndroidPerfLabApplication
import com.aquib.androidperflab.sdk.FakeFeatureFlagsSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
* Lazy initializer for the Feature Flags SDK.
*
* NOT registered in AndroidManifest.xml. Application.onCreate() defers triggering
* this initializer by 500 ms so it never competes with first-frame rendering or the
* immediately-needed Analytics / PerfMonitor SDKs.
*
* FakeFeatureFlagsSdk.isEnabled() returns false (the default) for any flag until the
* background coroutine completes — the UI is never gated on this SDK being ready.
*/
class FeatureFlagsInitializer : Initializer<Unit> {

override fun create(context: Context) {
val app = context.applicationContext as AndroidPerfLabApplication
app.applicationScope.launch(Dispatchers.IO) {
FakeFeatureFlagsSdk.init(context.applicationContext)
Log.d("AppStartup", "FeatureFlags ready")
}
Log.d("AppStartup", "FeatureFlagsInitializer.create() returned")
}

override fun dependencies(): List<Class<out Initializer<*>>> =
listOf(CrashReportingInitializer::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.aquib.androidperflab.startup

import android.content.Context
import android.util.Log
import androidx.startup.Initializer
import com.aquib.androidperflab.AndroidPerfLabApplication
import com.aquib.androidperflab.sdk.FakePerformanceMonitorSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
* NOT registered in AndroidManifest.xml — triggered from Application.onCreate() on
* Dispatchers.IO, sequenced after AnalyticsInitializer.
*
* Declaring AnalyticsInitializer as a dependency preserves the original intent: the
* performance monitor should start after the other SDKs have launched so it can
* baseline their initialization overhead. Note that the dependency guarantees ordering
* of create() calls, not completion of the background coroutines — PerfMonitor may
* start its actual work while Analytics is still initializing.
*/
class PerfMonitorInitializer : Initializer<Unit> {

override fun create(context: Context) {
val app = context.applicationContext as AndroidPerfLabApplication
app.applicationScope.launch(Dispatchers.IO) {
FakePerformanceMonitorSdk.init(context.applicationContext)
Log.d("AppStartup", "PerfMonitor ready")
}
Log.d("AppStartup", "PerfMonitorInitializer.create() returned")
}

override fun dependencies(): List<Class<out Initializer<*>>> =
listOf(AnalyticsInitializer::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.aquib.androidperflab.startup

import android.content.Context
import android.util.Log
import androidx.startup.Initializer
import com.aquib.androidperflab.AndroidPerfLabApplication
import com.aquib.androidperflab.sdk.FakeRemoteConfigSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
* Lazy initializer for the Remote Config SDK.
*
* NOT registered in AndroidManifest.xml. Triggered from Application.onCreate() after
* a 500 ms delay (alongside FeatureFlagsInitializer) so first-frame rendering is
* never competed with by config disk / network I/O.
*
* FakeRemoteConfigSdk.getString() returns the previously cached blob from SharedPreferences
* until the background coroutine completes — the app always has a usable config value.
*/
class RemoteConfigInitializer : Initializer<Unit> {

override fun create(context: Context) {
val app = context.applicationContext as AndroidPerfLabApplication
app.applicationScope.launch(Dispatchers.IO) {
FakeRemoteConfigSdk.init(context.applicationContext)
Log.d("AppStartup", "RemoteConfig ready")
}
Log.d("AppStartup", "RemoteConfigInitializer.create() returned")
}

override fun dependencies(): List<Class<out Initializer<*>>> =
listOf(CrashReportingInitializer::class.java)
}
Loading
Loading