How do you measure launch time from inside the app, and what can make the number wrong?
Predict first
What does the process-start clock give at the first line of main?
import Foundation
func processStart() -> Date {
var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var info = kinfo_proc(); var size = MemoryLayout<kinfo_proc>.stride
sysctl(&mib, 4, &info, &size, nil, 0)
let t = info.kp_proc.p_un.__p_starttime
return Date(timeIntervalSince1970: TimeInterval(t.tv_sec) + TimeInterval(t.tv_usec) / 1e6)
}
let sinceStart = Date().timeIntervalSince(processStart())
print("process start to the first line of main is positive and under 2 s:", sinceStart >= 0 && sinceStart < 2)
Thread.sleep(forTimeInterval: 0.3)
let later = Date().timeIntervalSince(processStart())
print("the same clock keeps counting from process start:", later - sinceStart > 0.25 && later - sinceStart < 0.6)
- positive and under 2 s, and still counting 0.3 s later
- Zero, because main starts the process
- It cannot be read from inside the process
Mental model. Timing a restaurant from the moment the chef arrives. If the chef came early to prep, the diner’s wait is shorter than your stopwatch says.
The honest start of launch is the process start time, which the kernel records. You can read it with sysctl (KERN_PROC_PID, kp_proc.p_starttime) and subtract it from “now” at any later point: when main runs, when didFinishLaunching returns, when the first frame is presented. In the check, the value at the first line of main was positive and under two seconds, and the same clock kept counting 0.3 s later. Mark the phases with signposts (Q430) so Instruments’ App Launch template and your own logs share names, and report the end point as first frame drawn (or “content ready”), not as the end of didFinishLaunching, which is far too early.
Ways the number lies: on iOS 15 and later the system may prewarm an app (run part of the launch before the user taps), so the time since process start includes idle time that the user never waited; compare against the time of the actual activation (UIApplication becoming active or the scene connecting) or use MetricKit’s launch histogram (Q431), which is designed for this. Cold launches (the system has evicted your binary and caches) and warm launches differ a lot; label each. Simulators and Debug builds are not representative, and a development machine under indexing load adds noise (Q428). Not asserted: iOS process-start values, prewarming behaviour, and cold-launch figures, all of which need a real device that was not available here; the check ran as a macOS command-line program.
The kernel records the start time; subtract it from now at any phase boundary.
Common traps
- Stopping the clock at
didFinishLaunching. The user is waiting until the first frame. - Averaging cold and warm launches. They are different populations.
They might follow up with
- What is a good target? Apple’s guidance is a first frame within about 400 ms; treat it as a budget to track, and measure on your oldest supported device.
- How do I get the field data? MetricKit and the Organizer’s launch-time chart.
Related questions
- 21.11.1 When do Swift globals and static properties initialise, and how does that move work into or out of launch?
- 21.3.1 What does MetricKit give you in production, how do you subscribe, and what is the difference between metric…
- 21.1.2 How do you measure performance reliably: what makes numbers lie, and why do percentiles beat averages?
← When do Swift globals and static properties initialise, and how does…