Skip to content

Commit 7fa29cc

Browse files
authored
Merge pull request #3 from mohdaquib/feature/after-optimized
Feature/after optimized
2 parents a0520d4 + 9a5b2d2 commit 7fa29cc

15 files changed

Lines changed: 687 additions & 181 deletions

File tree

app/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ dependencies {
6666
implementation(platform(libs.androidx.compose.bom))
6767
implementation(libs.bundles.compose.ui)
6868

69+
// App Startup: single ContentProvider for all SDK initializers (no per-SDK provider).
70+
implementation(libs.androidx.startup.runtime)
71+
// Coroutines: Dispatchers.IO for background SDK work, Dispatchers.Main for the app scope.
72+
implementation(libs.kotlinx.coroutines.android)
73+
6974
// Installs Baseline Profiles at first launch (ART pre-compilation).
7075
implementation(libs.androidx.profileinstaller)
7176

app/src/main/AndroidManifest.xml

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,45 @@
1414
android:roundIcon="@mipmap/ic_launcher_round"
1515
android:supportsRtl="true"
1616
android:theme="@style/Theme.AndroidPerfLab">
17+
18+
<!--
19+
App Startup: InitializationProvider consolidates all Initializer components
20+
into a single ContentProvider, replacing the pattern where each SDK ships its
21+
own provider (which adds ~2–5ms per provider to cold-start time).
22+
23+
Only CrashReportingInitializer runs here (before Application.onCreate).
24+
Non-critical SDKs are NOT listed — they are triggered from Application.onCreate()
25+
on background threads via AppInitializer.initializeComponent().
26+
-->
27+
<provider
28+
android:name="androidx.startup.InitializationProvider"
29+
android:authorities="${applicationId}.androidx-startup"
30+
android:exported="false"
31+
tools:node="merge">
32+
<meta-data
33+
android:name="com.aquib.androidperflab.startup.CrashReportingInitializer"
34+
android:value="androidx.startup" />
35+
<meta-data
36+
android:name="com.aquib.androidperflab.startup.FeatureFlagsInitializer"
37+
android:value="androidx.startup" />
38+
<meta-data
39+
android:name="com.aquib.androidperflab.startup.PerfMonitorInitializer"
40+
android:value="androidx.startup" />
41+
<meta-data
42+
android:name="com.aquib.androidperflab.startup.RemoteConfigInitializer"
43+
android:value="androidx.startup" />
44+
</provider>
45+
1746
<activity
1847
android:name=".MainActivity"
1948
android:exported="true"
2049
android:label="@string/app_name"
2150
android:theme="@style/Theme.AndroidPerfLab">
2251
<intent-filter>
2352
<action android:name="android.intent.action.MAIN" />
24-
2553
<category android:name="android.intent.category.LAUNCHER" />
2654
</intent-filter>
2755
</activity>
2856
</application>
2957

30-
</manifest>
58+
</manifest>

app/src/main/java/com/aquib/androidperflab/AndroidPerfLabApplication.kt

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,61 @@ package com.aquib.androidperflab
22

33
import android.app.Application
44
import android.util.Log
5-
import com.aquib.androidperflab.sdk.FakeAnalyticsSdk
6-
import com.aquib.androidperflab.sdk.FakeCrashReportingSdk
7-
import com.aquib.androidperflab.sdk.FakeFeatureFlagsSdk
8-
import com.aquib.androidperflab.sdk.FakePerformanceMonitorSdk
9-
import com.aquib.androidperflab.sdk.FakeRemoteConfigSdk
5+
import androidx.startup.AppInitializer
6+
import com.aquib.androidperflab.startup.AnalyticsInitializer
7+
import com.aquib.androidperflab.startup.FeatureFlagsInitializer
8+
import com.aquib.androidperflab.startup.PerfMonitorInitializer
9+
import com.aquib.androidperflab.startup.RemoteConfigInitializer
10+
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.SupervisorJob
13+
import kotlinx.coroutines.delay
14+
import kotlinx.coroutines.launch
1015

1116
class AndroidPerfLabApplication : Application() {
1217

18+
// Initialized at property-declaration time (before Application.onCreate and before
19+
// ContentProviders start). CrashReportingInitializer accesses this scope to fire
20+
// its background upload job — which is why it must be a property, not set in onCreate.
21+
// SupervisorJob: a failing child coroutine does not cancel sibling coroutines.
22+
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
23+
1324
override fun onCreate() {
1425
super.onCreate()
15-
16-
// Intentionally bad: all five SDKs are initialised synchronously on the main thread.
17-
// Each blocks via Thread.sleep() to simulate the I/O, disk, and network work that
18-
// real SDKs perform. Total cold-start penalty ≈ 750 ms before the first frame.
19-
2026
val t0 = System.currentTimeMillis()
2127

22-
FakeCrashReportingSdk.init(this) // ~120 ms — must be first to catch early crashes
23-
Log.d("AppStartup", "CrashReporting ready +${System.currentTimeMillis() - t0}ms")
24-
25-
FakeAnalyticsSdk.init(this) // ~180 ms
26-
Log.d("AppStartup", "Analytics ready +${System.currentTimeMillis() - t0}ms")
27-
28-
FakeFeatureFlagsSdk.init(this) // ~150 ms
29-
Log.d("AppStartup", "FeatureFlags ready +${System.currentTimeMillis() - t0}ms")
30-
31-
FakeRemoteConfigSdk.init(this) // ~200 ms
32-
Log.d("AppStartup", "RemoteConfig ready +${System.currentTimeMillis() - t0}ms")
33-
34-
FakePerformanceMonitorSdk.init(this) // ~100 ms — last so it can baseline the others
35-
Log.d("AppStartup", "PerfMonitor ready +${System.currentTimeMillis() - t0}ms")
28+
// CrashReportingInitializer ran synchronously via InitializationProvider BEFORE
29+
// this method. The exception handler is already registered; its upload coroutine
30+
// is already running on Dispatchers.IO. Nothing to do here for crash reporting.
31+
val appInit = AppInitializer.getInstance(this)
32+
33+
// Non-critical SDKs (Analytics, PerfMonitor): launch immediately on IO.
34+
// AppInitializer.initializeComponent() is idempotent — safe to call even if
35+
// a dependency was already initialized via the manifest.
36+
// Both SDKs initialize in ~180 ms + ~100 ms but never touch the main thread.
37+
applicationScope.launch(Dispatchers.IO) {
38+
appInit.initializeComponent(AnalyticsInitializer::class.java)
39+
// PerfMonitor declares Analytics as a dependency in the initializer graph,
40+
// so AppInitializer will invoke Analytics.create() first automatically —
41+
// the explicit sequence here is just for clarity.
42+
appInit.initializeComponent(PerfMonitorInitializer::class.java)
43+
}
44+
45+
// Lazy SDKs (FeatureFlags, RemoteConfig): deferred by 500 ms so they never
46+
// compete with Compose's first layout-and-draw pass. Both SDKs return safe
47+
// defaults until their background coroutines complete, so the UI is never gated
48+
// on their initialization.
49+
applicationScope.launch(Dispatchers.IO) {
50+
delay(500L)
51+
appInit.initializeComponent(FeatureFlagsInitializer::class.java)
52+
appInit.initializeComponent(RemoteConfigInitializer::class.java)
53+
}
54+
55+
Log.d(TAG, "Application.onCreate() returned in ${System.currentTimeMillis() - t0} ms " +
56+
"— all SDKs initializing in background")
57+
}
3658

37-
Log.d("AppStartup", "Application.onCreate() complete — total blocked=${System.currentTimeMillis() - t0}ms")
59+
companion object {
60+
private const val TAG = "AppStartup"
3861
}
3962
}

app/src/main/java/com/aquib/androidperflab/sdk/FakeCrashReportingSdk.kt

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,32 @@ import java.io.File
66

77
object FakeCrashReportingSdk {
88

9-
// Simulates: scanning the cache directory for pending crash dumps left by a previous
10-
// session, installing an UncaughtExceptionHandler, and uploading any found dumps
11-
// synchronously before the app is considered "ready".
12-
fun init(context: Context) {
13-
// Simulate: scanning cache dir for pending crash dump files
9+
// ── Fast path — called synchronously from CrashReportingInitializer ──────────
10+
//
11+
// Only registers the UncaughtExceptionHandler. Safe to call from any thread.
12+
// Separated from uploadPendingReports() so the handler is installed before any
13+
// other SDK work begins — matching the original "must be first" requirement —
14+
// without blocking the main thread for the full 120 ms upload simulation.
15+
16+
fun registerHandler(context: Context) {
17+
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
18+
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
19+
Log.e("FakeCrashReportingSdk", "Uncaught exception on ${thread.name}", throwable)
20+
previousHandler?.uncaughtException(thread, throwable)
21+
}
22+
Log.d("FakeCrashReportingSdk", "registerHandler complete")
23+
}
24+
25+
// ── Slow path — called from Dispatchers.IO via CrashReportingInitializer ─────
26+
//
27+
// Simulates: scanning the cache directory for pending crash dumps, parsing them,
28+
// writing a session sentinel, and uploading reports to the backend. All I/O and
29+
// the blocking upload sleep are safe on a background thread.
30+
31+
fun uploadPendingReports(context: Context) {
1432
val cacheDir = context.cacheDir
33+
34+
// Simulate: scanning cache dir for pending crash dump files
1535
val crashDumps = cacheDir.listFiles { f -> f.name.startsWith("crash_") }
1636
?.toList()
1737
?: emptyList()
@@ -25,21 +45,17 @@ object FakeCrashReportingSdk {
2545
}
2646
}
2747

28-
// Simulate: writing a session sentinel file so the next launch can detect
29-
// whether this session ended cleanly
48+
// Simulate: writing a session sentinel so the next launch can detect clean exits
3049
val sentinel = File(cacheDir, "crash_sentinel_${System.currentTimeMillis()}.tmp")
3150
sentinel.createNewFile()
3251

33-
// Simulate: registering the uncaught exception handler (involves thread locking)
34-
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
35-
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
36-
Log.e("FakeCrashReportingSdk", "Uncaught exception on ${thread.name}", throwable)
37-
previousHandler?.uncaughtException(thread, throwable)
38-
}
39-
40-
// Simulate: blocking upload of any pending crash reports to the backend
52+
// Simulate: blocking upload of pending reports to the backend
4153
Thread.sleep(120L)
4254

43-
Log.d("FakeCrashReportingSdk", "init complete — found ${crashDumps.size} pending dumps, parsed ${parsedReports.size} reports")
55+
Log.d(
56+
"FakeCrashReportingSdk",
57+
"uploadPendingReports complete — found ${crashDumps.size} dumps, " +
58+
"parsed ${parsedReports.size} reports",
59+
)
4460
}
4561
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.aquib.androidperflab.startup
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.startup.Initializer
6+
import com.aquib.androidperflab.AndroidPerfLabApplication
7+
import com.aquib.androidperflab.sdk.FakeAnalyticsSdk
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.launch
10+
11+
/**
12+
* NOT registered in AndroidManifest.xml — triggered manually from Application.onCreate()
13+
* via AppInitializer.initializeComponent() on Dispatchers.IO.
14+
*
15+
* create() returns immediately after launching the background coroutine; the ~180 ms
16+
* SDK init never touches the main thread. CrashReportingInitializer is declared as a
17+
* dependency so the exception handler is guaranteed to be registered before any SDK
18+
* code runs (AppInitializer resolves the dependency graph automatically).
19+
*/
20+
class AnalyticsInitializer : Initializer<Unit> {
21+
override fun create(context: Context) {
22+
val app = context.applicationContext as AndroidPerfLabApplication
23+
app.applicationScope.launch(Dispatchers.IO) {
24+
FakeAnalyticsSdk.init(context.applicationContext)
25+
Log.d("AppStartup", "Analytics ready")
26+
}
27+
Log.d("AppStartup", "AnalyticsInitializer.create() returned")
28+
}
29+
30+
override fun dependencies(): List<Class<out Initializer<*>>> =
31+
listOf(CrashReportingInitializer::class.java)
32+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.aquib.androidperflab.startup
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.startup.Initializer
6+
import com.aquib.androidperflab.AndroidPerfLabApplication
7+
import com.aquib.androidperflab.sdk.FakeCrashReportingSdk
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.launch
10+
11+
/**
12+
* Runs via InitializationProvider BEFORE Application.onCreate().
13+
*
14+
* Critical path (main thread, synchronous):
15+
* FakeCrashReportingSdk.registerHandler() — installs the UncaughtExceptionHandler.
16+
* Cost: < 1 ms.
17+
*
18+
* Non-critical path (Dispatchers.IO, fire-and-forget):
19+
* FakeCrashReportingSdk.uploadPendingReports() — scans crash dumps and simulates upload.
20+
* Cost: ~120 ms off the main thread.
21+
*
22+
* applicationScope is initialized at property-declaration time in AndroidPerfLabApplication,
23+
* so it exists before this Initializer runs (the Application object is created before
24+
* ContentProviders are started).
25+
*/
26+
class CrashReportingInitializer : Initializer<Unit> {
27+
28+
override fun create(context: Context) {
29+
val app = context.applicationContext as AndroidPerfLabApplication
30+
31+
// Synchronous — registers the exception handler before any other SDK work.
32+
FakeCrashReportingSdk.registerHandler(context.applicationContext)
33+
34+
// Asynchronous — upload is I/O bound and does not need to complete before
35+
// the first frame; move it off the critical path entirely.
36+
app.applicationScope.launch(Dispatchers.IO) {
37+
FakeCrashReportingSdk.uploadPendingReports(context.applicationContext)
38+
Log.d("AppStartup", "CrashReporting upload complete")
39+
}
40+
41+
Log.d("AppStartup", "CrashReportingInitializer.create() returned")
42+
}
43+
44+
// No dependencies — CrashReporting must be the root of the initializer graph.
45+
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
46+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.aquib.androidperflab.startup
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.startup.Initializer
6+
import com.aquib.androidperflab.AndroidPerfLabApplication
7+
import com.aquib.androidperflab.sdk.FakeFeatureFlagsSdk
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.launch
10+
11+
/**
12+
* Lazy initializer for the Feature Flags SDK.
13+
*
14+
* NOT registered in AndroidManifest.xml. Application.onCreate() defers triggering
15+
* this initializer by 500 ms so it never competes with first-frame rendering or the
16+
* immediately-needed Analytics / PerfMonitor SDKs.
17+
*
18+
* FakeFeatureFlagsSdk.isEnabled() returns false (the default) for any flag until the
19+
* background coroutine completes — the UI is never gated on this SDK being ready.
20+
*/
21+
class FeatureFlagsInitializer : Initializer<Unit> {
22+
23+
override fun create(context: Context) {
24+
val app = context.applicationContext as AndroidPerfLabApplication
25+
app.applicationScope.launch(Dispatchers.IO) {
26+
FakeFeatureFlagsSdk.init(context.applicationContext)
27+
Log.d("AppStartup", "FeatureFlags ready")
28+
}
29+
Log.d("AppStartup", "FeatureFlagsInitializer.create() returned")
30+
}
31+
32+
override fun dependencies(): List<Class<out Initializer<*>>> =
33+
listOf(CrashReportingInitializer::class.java)
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.aquib.androidperflab.startup
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.startup.Initializer
6+
import com.aquib.androidperflab.AndroidPerfLabApplication
7+
import com.aquib.androidperflab.sdk.FakePerformanceMonitorSdk
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.launch
10+
11+
/**
12+
* NOT registered in AndroidManifest.xml — triggered from Application.onCreate() on
13+
* Dispatchers.IO, sequenced after AnalyticsInitializer.
14+
*
15+
* Declaring AnalyticsInitializer as a dependency preserves the original intent: the
16+
* performance monitor should start after the other SDKs have launched so it can
17+
* baseline their initialization overhead. Note that the dependency guarantees ordering
18+
* of create() calls, not completion of the background coroutines — PerfMonitor may
19+
* start its actual work while Analytics is still initializing.
20+
*/
21+
class PerfMonitorInitializer : Initializer<Unit> {
22+
23+
override fun create(context: Context) {
24+
val app = context.applicationContext as AndroidPerfLabApplication
25+
app.applicationScope.launch(Dispatchers.IO) {
26+
FakePerformanceMonitorSdk.init(context.applicationContext)
27+
Log.d("AppStartup", "PerfMonitor ready")
28+
}
29+
Log.d("AppStartup", "PerfMonitorInitializer.create() returned")
30+
}
31+
32+
override fun dependencies(): List<Class<out Initializer<*>>> =
33+
listOf(AnalyticsInitializer::class.java)
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.aquib.androidperflab.startup
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.startup.Initializer
6+
import com.aquib.androidperflab.AndroidPerfLabApplication
7+
import com.aquib.androidperflab.sdk.FakeRemoteConfigSdk
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.launch
10+
11+
/**
12+
* Lazy initializer for the Remote Config SDK.
13+
*
14+
* NOT registered in AndroidManifest.xml. Triggered from Application.onCreate() after
15+
* a 500 ms delay (alongside FeatureFlagsInitializer) so first-frame rendering is
16+
* never competed with by config disk / network I/O.
17+
*
18+
* FakeRemoteConfigSdk.getString() returns the previously cached blob from SharedPreferences
19+
* until the background coroutine completes — the app always has a usable config value.
20+
*/
21+
class RemoteConfigInitializer : Initializer<Unit> {
22+
23+
override fun create(context: Context) {
24+
val app = context.applicationContext as AndroidPerfLabApplication
25+
app.applicationScope.launch(Dispatchers.IO) {
26+
FakeRemoteConfigSdk.init(context.applicationContext)
27+
Log.d("AppStartup", "RemoteConfig ready")
28+
}
29+
Log.d("AppStartup", "RemoteConfigInitializer.create() returned")
30+
}
31+
32+
override fun dependencies(): List<Class<out Initializer<*>>> =
33+
listOf(CrashReportingInitializer::class.java)
34+
}

0 commit comments

Comments
 (0)