Profiling Android app cold start with Perfetto traces
SEO Meta Description: Use Perfetto to diagnose Android cold start regressions — ContentProvider chains, Binder IPC latency, and DI allocations that silently steal 400ms or more.
Tags: android kotlin mobile architecture cleanarchitecture
TL;DR
Cold start times above 1 second on mid-range devices almost always trace back to three culprits: an uncontrolled ContentProvider initialization chain, blocking Binder IPC calls during Application.onCreate(), and synchronous allocations in dependency injection. Perfetto gives you the precision to find all three. This post walks through exactly how to read those traces and act on them.
Why cold start still matters in 2026
A 1-second cold start on a Pixel 8 becomes a 2.4-second cold start on a mid-range device with constrained memory bandwidth. Google’s data shows apps exceeding 5 seconds to first frame lose a significant share of new users before they ever interact — Play Console’s Android Vitals corroborates this, and it’s come up repeatedly at Google I/O quality sessions. Yet most teams treat startup regression as a secondary concern until it shows up in retention dashboards. By then, the damage is done.
Setting up a Perfetto cold start trace
You need clean captures. Use the record_android_trace script with a config targeting sched, binder, and atrace categories:
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace.pb \
<<EOF
buffers { size_kb: 32768 }
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "binder/binder_transaction"
atrace_categories: "am"
atrace_categories: "view"
atrace_apps: "com.yourapp"
}
}
}
EOF
Force a true cold start by killing the process and dropping file caches before each capture. Profile on the lowest-spec device in your supported range — flagship numbers lie.
Reading trace slices: the ContentProvider chain
Most teams assume only their ContentProviders run at init time. They’re wrong. Every library that registers a ContentProvider in its manifest — WorkManager, Firebase, Lifecycle, LeakCanary in debug builds — chains into your Application startup before a single line of your code executes.
In Perfetto’s timeline, look for the bindApplication slice under your main thread. Nested beneath it, ActivityThread.installContentProviders shows individual provider init slices. In production traces I’ve reviewed, finding 8–14 providers initializing in sequence is normal, with the chain consuming anywhere from 180ms to 600ms cold.
| Provider (example category) | Typical init cost (mid-range) |
|---|---|
| Firebase Performance | 80–140ms |
| WorkManager (auto-init) | 60–90ms |
| Lifecycle ProcessObserver | 20–40ms |
| Custom app providers (2–3) | 30–80ms |
| Total chain | 190–350ms+ |
The fix is the App Startup library — replace individual ContentProvider registrations with a single InitializationProvider and control initialization order explicitly. Deferred, non-blocking initializers should move out of the critical path entirely.
Binder IPC: the hidden blocking call
After the provider chain, the next hotspot is Binder transactions. In the Perfetto trace, filter for binder_transaction events on the main thread. Any synchronous Binder call during cold start that blocks for more than 10ms is a regression candidate.
The most common offenders, in my experience:
PackageManager.getInstalledPackages()called during feature-flag initializationAccountManager.getAccounts()triggered by authentication library setupSettings.Secure.getString()inside analytics SDK init
Each crosses process boundaries via Binder. On a loaded mid-range device with contention on the system server, a single call can block 40–120ms. The trace will show your main thread in a binder reply wait state — unmistakable once you know what to look for.
Move all Binder calls off the main thread. If a value is needed synchronously, cache it at install time or first-run, not at every cold start.
Application.onCreate() allocations
The third category is object allocation pressure during Application.onCreate(). In the Perfetto timeline, look for GC events (HeapTaskDaemon slices) occurring within the first 500ms. Premature GC during startup indicates large allocations — often from Dagger/Hilt component initialization building the full dependency graph eagerly.
The fix is dagger.Lazy<T>. Rather than letting Hilt construct every @Singleton at component creation time, declare non-critical dependencies as Lazy<T> so their providers are only invoked on first access:
@HiltAndroidApp
class App : Application() {
// Injected but NOT constructed until first .get() call
@Inject lateinit var analytics: dagger.Lazy<AnalyticsManager>
@Inject lateinit var featureFlags: dagger.Lazy<FeatureFlagClient>
override fun onCreate() {
super.onCreate()
// Critical path only — analytics and featureFlags
// are not allocated here; their graphs stay dormant
initCriticalPath()
}
}
// Elsewhere, on demand:
class HomeFragment : Fragment() {
@Inject lateinit var analytics: dagger.Lazy<AnalyticsManager>
override fun onResume() {
super.onResume()
analytics.get().track("home_viewed") // allocated here, not at startup
}
}
Splitting the dependency graph into critical and deferred subgraphs cut onCreate() time by 200–400ms in our benchmarks on apps with large DI graphs — without touching the architecture.
Takeaways
-
Capture on your p50 device, not your desk machine. A Perfetto trace on a Pixel flagship hides the majority of real-world startup regressions. Always profile on a mid-range device with constrained RAM.
-
Audit your ContentProvider chain immediately. Run
adb shell dumpsys package com.yourapp | grep providerand trace every registered provider back to a library dependency. Migrate to the App Startup library and move non-critical initializers toWorkManagerwith anInitialDelay. -
Add startup regression CI gates. Integrate Macrobenchmark’s
measureRepeatedwithStartupMode.COLDinto your CI pipeline and fail builds that exceed a defined p95 threshold. Catching a 50ms regression at PR time costs nothing; catching it post-release costs users.
Perfetto makes the problem concrete — you’re not guessing anymore, you have stack frames. Getting the team to treat startup as a first-class metric before Play Console starts making the argument for you is the harder part.