| name | startup-profiling |
| description | Profile cold, warm, and hot start with Android Macrobenchmark, Xcode Instruments App Launch template, and Flutter DevTools. Use when you need to attribute startup time to specific phases. |
Startup Profiling
Instructions
Startup time without a trace is a guess. This skill shows how to capture reproducible startup traces on every platform and how to read them.
1. Android — Macrobenchmark + Perfetto
Macrobenchmark runs your app in release mode on a physical device and emits a startup-compilation-*.trace file plus metrics.
module-benchmark/build.gradle.kts:
plugins { id("androidx.benchmark") ; id("com.android.test") }
android {
defaultConfig { testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner" }
buildTypes {
create("benchmark") { initWith(getByName("release")); signingConfig = signingConfigs.getByName("debug") }
}
}
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.3.0")
}
Test:
@RunWith(AndroidJUnit4::class)
class ColdStartupBenchmark {
@get:Rule val rule = MacrobenchmarkRule()
@Test fun startup() = rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(
StartupTimingMetric(),
TraceSectionMetric("databaseInit"),
),
iterations = 10,
startupMode = StartupMode.COLD,
compilationMode = CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require),
) {
pressHome(); startActivityAndWait()
device.wait(Until.hasObject(By.res("feed")), 5_000)
device.findObject(By.res("feed")).click()
}
}
Reading the output: open the generated .perfetto-trace in ui.perfetto.dev. Look at:
MainActivity.onCreate slice width.
bindApplication (ART process bootstrap).
Choreographer#doFrame gaps — long gaps before first doFrame = main-thread block.
android.os.AsyncTask / your own trace sections for hot spots.
Add custom sections with androidx.tracing:
androidx.tracing.Trace.beginSection("databaseInit")
try { AppDatabase.getInstance(context) } finally { Trace.endSection() }
2. iOS — Instruments "App Launch" Template
- Xcode → Product → Profile → choose App Launch template.
- Run on a physical device with the Release config (Profile builds use Release by default).
- Instruments prints four phases in the track view:
- System Interface Initialization (dyld, ObjC runtime).
- Static Runtime Initialization (
+[load], static initializers).
- UIKit Initialization /
application(_:didFinishLaunchingWithOptions:).
- Initial Frame Render.
- Any slice > 200 ms on a modern device is suspicious. Drill in to see the heaviest symbols.
Signpost your own TTFD:
import os.signpost
let log = OSLog(subsystem: "app", category: .pointsOfInterest)
os_signpost(.event, log: log, name: "TTFD")
Dyld/linker cost: check Time Profiler's "System Trace" → "Main Thread" for dyld symbols. If high, reduce dynamic framework count (switch to static linking for first-party frameworks).
3. Flutter — flutter run --trace-startup
flutter run --profile --trace-startup --verbose
Produces build/start_up_info.json:
{
"engineEnterTimestampMicros": 1234,
"timeToFirstFrameMicros": 890000,
"timeToFrameworkInitMicros": 210000,
"timeAfterFrameworkInitMicros": 680000
}
Instrument TTFD:
void main() {
runApp(const MyApp());
WidgetsBinding.instance.addPostFrameCallback((_) {
developer.Timeline.instantSync('TTFD');
});
}
Open DevTools → Performance → Enhance Tracing → Trace shader compilation. Shader compilation jank on first frames is common on Android; use --cache-sksl + Impeller.
4. React Native — Systrace + Hermes
Capture a systrace that includes JS:
adb shell atrace --async_start -t 10 gfx view wm am sched hal
# launch app, wait for interactive
adb shell atrace --async_stop > startup.trace
Open in Perfetto. Sections to look for:
JSBundleLoader.loadScript
ReactContext.initialize
UIManagerModule.createView pile-up
- Slow native module startup — rename legacy modules to TurboModules.
Hermes sampling profiler for the JS thread:
import * as Hermes from 'react-native/Libraries/SamplingProfiler';
Hermes.startSamplingProfiler();
Hermes.stopSamplingProfiler('/sdcard/hermes-sample.cpuprofile');
Open the .cpuprofile in Chrome DevTools → Performance → Load Profile.
5. Treat Startup Traces as Regression Gates
Commit a baseline JSON to the repo and fail CI on > 5% regression. Record:
- device model,
- OS version,
- build variant,
- p50 and p95 of 10 runs.
Checklist