Zygote
Hands-on labsintermediate

Lab 11 — App startup

Measure a cold start three ways — logcat, am start -W, and a trace — and learn why the number you get depends on the method. Then break the start into its phases and find the expensive one.

11 minAPI 1–37 · 17interactiveverified 2026-10-27

Startup is the performance metric users feel most and measure worst. The platform reports several different numbers, they measure different things, and comparing them without knowing the difference is how teams optimise the wrong phase.

TL;DR#

  • Time to initial display (TTID) is the first frame. Time to full display (TTFD) is when the app says it is usable.
  • am start -W prints TotalTime and WaitTime; they are not the same measurement.
  • logcat's Displayed line is the system's own TTID record.
  • A trace is the only way to attribute the time to a phase.
  • Cold, warm and hot starts differ by what has to be created, and only cold start is comparable across runs.

Mental model#

Think of a restaurant's "how long did the food take?".

The time you sat down is the launch intent. The first plate on the table is TTID: something arrived. The moment you could start eating properly, with the sides and the drink, is TTFD.

A stopwatch started at different moments gives different answers, and all of them are honest. The argument is about which moment matters.

Where the analogy breaks#

A restaurant's timeline is simple: one table, one order. An app start has a process creation, an Application object, an activity, a window, a first frame and asynchronous content, and each of those can be cached or not. Two "cold" starts can differ because the process was or was not still alive.

Deep dive#

The three numbers from am start#

adb shell am start -W -S -n com.android.settings/.Settings

The flags matter: -W waits for the launch, -S force-stops the app first so it is a cold start.

The output has two times:

FieldWhat it measures
TotalTimeThe launch time reported by the activity manager, including process start if there was one
WaitTimeThe wall-clock time the am command waited, measured around the launch call

WaitTime is measured by the shell command itself, so it includes the command's own overhead; TotalTime is the number the activity manager reports. The two are usually close, and the gap is mostly the shell round trip. Compare TotalTime across runs, and only when the start state is the same. With -S you get a cold start; without it, the second run is warm or hot.

Modern output also includes a LaunchState line that says whether the launch was COLD, WARM or HOT, which is the platform telling you which comparison you are making.

logcat's display record#

adb logcat | grep -i "Displayed"

The ActivityManager logs a line when the first frame is displayed, with the package and the duration. That is the system's own TTID record, and it is the same number the platform reports to its vitals pipeline. It is a useful cross-check on am start, and it appears without -W.

For TTFD, the app must call reportFullyDrawn. The log line then says Fully drawn, and the duration is measured from the launch intent.1

The phases in a trace#

The trace's value is attribution. A cold start has a shape:

Loading diagram…
Text description of this diagram

Eight ordered phases of a cold start. In the system lane: the launch request crosses Binder to ActivityTaskManager, the system shows a starting window, and Zygote forks the app process. In the app lane: bindApplication creates the Application object and runs Application.onCreate; activity creation runs Activity.onCreate and builds the UI; the first frame is drawn and presented; asynchronous content arrives; and the app calls reportFullyDrawn. Positions are ordinal, not measured milliseconds.

Map the phases onto the trace:

  • Process fork shows as the app process appearing in the trace, with its threads starting.
  • bindApplication is a slice on the main thread, and it contains Application.onCreate.
  • Activity creation is the next large slice, containing inflate or first composition.
  • First draw is the main thread's draw plus the RenderThread's work.
  • Presentation is when the frame reaches the display.

The gap between the fork and the first app slice is the process setup cost. The gap between onCreate and the first draw is the app's own work. The gap between the draw and the presentation is the render and composition cost.

Cold, warm and hot#

The platform distinguishes three start states, and only one of them is a fair comparison:

  • Cold: the process does not exist. The system creates it, the app initialises, and the first activity is created.
  • Warm: the process exists but the activity must be recreated, often with saved state.
  • Hot: the process and the activity are alive; the system brings it to the foreground.

A cold start is the most expensive and the one to optimise, because improving it tends to improve the others. Measuring a hot start and calling it "startup" is a common way to get a number that does not reflect what users experience after a reboot.

Reading the four gaps#

The startup-phases diagram makes the structure explicit. Each phase has a boundary, and the boundary is where the time hides:

  • Launch to fork. The system resolving the intent and deciding to create a process. Mostly not under your control, but a heavy launcher or a busy system makes it longer.
  • Fork to bindApplication. Process setup: the fork itself, the runtime initialisation, the first Binder call into the new process.
  • bindApplication to first draw. Your code. This is the part you can change, and the part that Application.onCreate and activity creation both occupy.
  • First draw to presentation. Render and composition. Usually short, unless the first frame is expensive to draw.

If the third gap dominates, the fix is in your startup code. If the second does, the process is paying for something the platform controls. If the fourth does, the first frame is too expensive to render, which is a different problem from being too slow to create.

What to do with the number#

Three levers, in order of typical payoff:

  1. Reduce work in Application.onCreate. It runs before any activity and delays everything.
  2. Defer non-critical initialisation until after the first frame, and make sure it is actually deferred rather than moved to a background thread that still competes for CPU.
  3. Use baseline profiles so the hot code paths are compiled ahead of time rather than interpreted on first run.2

Macrobenchmark is the supported way to measure this repeatably on a device, with a real launch and frame timing rather than a synthetic loop.

Walkthrough#

  1. Measure a cold start

    adb shell am start -W -S -n com.android.settings/.Settings

    You should seeA LaunchState line, then TotalTime and WaitTime.
  2. Repeat and compare

    Run the same command three times.

    You should seeThe second run is usually faster; the third is stable.
  3. Find the display record

    adb logcat -c then start the app, then adb logcat -d | grep -i Displayed

    You should seeA Displayed line with a duration.
  4. Record a trace of the launch

    Record with the config from lab 10, start the app during the recording, then pull the trace.

    You should seeA trace containing the app process and its threads.
  5. Find bindApplication

    Search the trace for bindApplication in the UI.

    You should seeA slice on the app's main thread.
Measure a launch
adb shell am start -W -S -n com.android.settings/.Settings
adb logcat -c
adb shell am start -W -n com.android.settings/.Settings
adb logcat -d | grep -i Displayed
Example output
Illustrative output — durations differ per device and run.

Challenges#

Challenge 1 — Compare a cold start with a warm start#

Measure both and explain the difference in TotalTime.

Reveal the answer
adb shell am start -W -S -n com.android.settings/.Settings
adb shell am start -W -n com.android.settings/.Settings

The cold start includes process creation, so TotalTime is larger. The second command, run immediately, benefits from a live process and a warm file cache. Check the LaunchState line to confirm which case you measured; if it says COLD both times, the process was killed between runs.

Challenge 2 — Find the most expensive phase#

Using a trace, decide whether the cold start is dominated by process setup, Application.onCreate, activity creation, or rendering.

Reveal the answer

Find the app process's first slice, then measure the gaps: fork to bindApplication, the bindApplication slice itself, activityCreate and its children, and the RenderThread's first frame. The largest of those four is the phase to attack. In SQL, the same question is a query over slices filtered by process and thread, ordered by duration.

Challenge 3 — Make TTID and TTFD different#

Find an app whose first frame appears before its content, and explain the gap.

Reveal the answer

Any app that loads data asynchronously has this gap: TTID is the first frame with a loading state, TTFD is after reportFullyDrawn. In logcat, the Displayed line and the Fully drawn line have different durations, and the difference is the async load. If the app never calls reportFullyDrawn, there is no TTFD record at all, which is itself a finding.

What you learned#

You can measure a launch three ways, explain the difference between the numbers, and attribute the time to a phase in a trace. The next lab applies the same method to frames.

Next#

Lab 12 — Jank asks why a frame was late, not why a launch was slow.

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsApp startup timedeveloper.android.comaccessed 2026-10-27↩
  2. docsMacrobenchmark overviewdeveloper.android.comaccessed 2026-10-27↩
  3. docsOverview of system tracingdeveloper.android.comaccessed 2026-10-27↩
  4. docsAndroid tracing with Perfettoperfetto.devaccessed 2026-10-27↩
  5. docsPerfetto documentationperfetto.devaccessed 2026-10-27↩
  6. docsAndroid Debug Bridge (adb)developer.android.comaccessed 2026-10-27↩