Introduction

Before an app even runs main and +applicationDidFinishLaunching, a considerable amount of work is done, including setting up dylibs for use, running +load methods, and more. This can take 500ms or more. Blog posts like this one show you how to measure it with the debugger, using DYLD_PRINT_STATISTICS, but it’s hard to find any help for measurement in the wild. Note the special handling for iOS 15’s pre-warming feature.

How To Do It

You can get the process start time with the following code:

#import <sys/sysctl.h>
#import <QuartzCore/QuartzCore.h>

static CFTimeInterval processStartTime() {
    size_t len = 4;
    int mib[len];
    struct kinfo_proc kp;

    sysctlnametomib("kern.proc.pid", mib, &len);
    mib[3] = getpid();
    len = sizeof(kp);
    sysctl(mib, 4, &kp, &len, NULL, 0);

    struct timeval startTime = kp.kp_proc.p_un.__p_starttime;
    return startTime.tv_sec + startTime.tv_usec / 1e6;
}

static CFTimeInterval sTimeToInitializer = 0;

// With iOS 15 pre-warming, initializers and other pre-main steps are run preemptively, potentially hours before the
// app is started and main() is run. So, take the difference between process start time in pre-main initializer
// (which we control with this "constructor" attribute) and add that later with post-main time.
// NOTE: any initializers run after this will not have their startup time impact included, and the placement of this
// constructor function amongst the others is undefined
__used __attribute__((constructor)) static void recordProcessStartTime() {
    sTimeToInitializer = [NSDate date].timeIntervalSince1970 - processStartTime();
}

static CFTimeInterval sMainStartTime;

int main(int argc, char * argv[], char **envp) {
    // We're now in main, so pre-warming is done, begin recording time again. Any time before this line in main will not be included.
    sMainStartTime = CACurrentMediaTime();

    if ([[NSProcessInfo processInfo].environment[@"ActivePrewarm"] isEqual:@"1"]) {
        // The ActivePrewarm variable indicates whether the app was launched via pre-warming.
        // Based on this, for example, you can choose whether or not to include the pre-main time in your calculation based on that.
    }
    // ...
}

// Call this function when startup is done to get your final measurement
CFTimeInterval timeFromStartToNow() {
    return CACurrentMediaTime() - sMainStartTime + sTimeToInitializer;
}

Caveat: absolute timing

The process start time that we get from processStartTime is given in seconds since 1970. Unfortunately, this is not so great for timing because it can be changed, e.g. if the phone synchronizes its time with a server in between when the process starts and when we run [NSDate date].timeIntervalSince1970 in main(). This would be a very rare case, but it should be kept in mind (you may want to remove negative and ridiculously large values from your results).

Main thread CPU Time

You can also get the amount of CPU time the main thread has spent by running this code on the main thread:

struct timespec tp;
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tp);
CFTimeInterval time = tp.tv_sec + tp.tv_nsec / 1e9;

This number represents the amount of time the main thread has spent doing work, but does not include the amount of time it was waiting, e.g. due to being preempted by another process or thread. In practice, it will be about the same as the time taken from the method in the gist (I measured it being consistently 7ms less). This number may be preferable if you don’t want to time anything that’s out of your control.

Conclusion

The pre-main() time provides an important piece of information in understanding your overall launch time. By measuring from the very start of the process to when the app is actually interactive, you can gain insight into the actual experience for your users.

Optimizing App Startup Time (WWDC)

App Startup Time: Past, Present, and Future (WWDC)