https://doordash.engineering/2023/01/31/how-we-reduced-our-ios-app-launch-time-by-60/ * Engineering Blog + Blog Home + Backend + Mobile + Web + Data + Machine Learning + Culture + General * Locations + San Francisco + Mountain View + New York + Seattle + Los Angeles + Pittsburgh + Toronto * Events * University Join Our Team How We Reduced Our iOS App Launch Time by 60% January 31, 2023 7 Minute Read Mobile 19 [Screen-Shot-] Filip Busic Filip Busic is a software engineer at DoorDash, since March 2020, where he now works on iOS performance, app binary size reduction, and other stability improvements. He is also an avid part of the developer community and enjoys setting up home labs in his free time. Recent Posts [jake-grell] How We Reduced Our iOS App Launch Time by 60% 7 Minute Read [AdobeStock] Examining Problematic Memory in C/C++ Applications with BPF, perf, and Memcheck 47 Minute Read [Screen-Shot-]Filip Busic App startup time is a critical metric for users, as it's their first interaction with the app, and even minor improvements can have significant benefits for the user experience. First impressions are a big driver in consumer conversion, and startup times often indicate the app's overall quality. Furthermore, other companies found that an increase in latency equals a decrease in sales. At DoorDash, we take app startup speed extremely seriously. We are obsessed with optimizing our customers' experience and making continuous improvements. In this article, we will explore three separate optimizations that reduced the time it took to launch our iOS consumer app by 60%. We identified these opportunities using proprietary performance tools, but Xcode instruments or DTrace could also be suitable alternatives. Changing String(describing:) to ObjectIdentifier() In early 2022, our app startup optimization journey began with visualizing top bottlenecks using Emerge Tools' Performance Analysis tool, as seen in Figure 1. Figure 1: Stack trace showing the three performance optimization opportunitiesFigure 1: Stack trace showing the three performance optimization opportunities This performance tool helped showcase unoptimized branches from both a birdseye and a detailed point-of-view. One of the biggest immediate standouts was the time we spent on Swift protocol conformance checks (checking if a type conforms to a protocol), but why? Architectural principles like the single responsibility principle, separation of concerns, and others, are key to how we write code at DoorDash. Services and dependencies are often injected and described by their type. The problem is that we used String(describing:) to identify services, which came with a runtime performance penalty of checking if the type conforms to various other protocols. The stack trace in Figure 2 is taken directly from our app launch to showcase this. Figure 2: Stack trace of what happens behind the scenes of the String (describing:) APIFigure 2: Stack trace of what happens behind the scenes of the String(describing:) API The first question we asked ourselves was: "Do we really need a string to identify a type?"Eliminating the string requirement and switching to identifying types using ObjectIdentifier instead, which is a mere pointer to the type, yielded 11% faster app startup times. We also applied this technique to other areas where a pointer sufficed instead of a raw string, which yielded an additional 11% improvement. [Screenshot] If it's possible to use a raw pointer to the type instead of using String(describing:) We recommend making the same change to save on the latency penalty. Stop converting unnecessary objects to AnyHashable At DoorDash, we encapsulate user actions, network requests, data mutations, and other computational workloads into (what we call) commands. For example, when we load a store menu, we submit that as a request to the command execution engine. The engine will then store the command in a processing array and perform the inbound commands sequentially. Structuring our operations this way is a key part of our new architecture, where we purposely isolate direct mutations and observe the results of expected actions instead. This optimization began with rethinking how we identify commands and generate their hash value. Our processing array, and other dependencies, rely on a unique hash value to identify and separate the respective commands. Historically, we circumvented the need of having to think about hashing by using AnyHashable. However, as noted in the Swift standard, doing so was dangerous because relying on hash values given by AnyHashable could change between releases. We could have chosen to optimize our hashing strategy in several ways, but we started by rethinking our original restrictions and bounds. Originally, the hash value of a command was a combination of its associated members. This decision had been made deliberately since we wanted to maintain a flexible and powerful abstraction of commands. But after an app-wide adoption of the new architecture, we noticed that the design choice was premature and overall left unused. Changing this requirement to identify commands by their type led to a 29% faster app launch, 55% faster command execution, and 20% faster registration of commands. [Screenshot] Subscribe for weekly updates [ ] [Subscribe] Auditing 3rd party framework initializers At DoorDash, we strive to be free of third-party dependencies wherever possible. Still, there are times when a consumer's experience could greatly benefit from a third-party integration. Regardless, we conduct several rigorous audits of how third-party dependencies impact our service and the quality we uphold. A recent audit found that a certain third-party framework caused our iOS application to launch roughly 200ms slower. This framework alone occupied roughly 40%(!) of our app launch time, as seen in Figure 3. Figure 3: A flame chart showing roughly 200ms of our app startup time was due to a third-party framework iterating over our NSBundle.Figure 3: A flame chart showing roughly 200ms of our app startup time was due to a third-party framework iterating over our NSBundle. To make matters trickier, the framework in question was a key part of ensuring a positive consumer experience. So what can we do? How do we balance one aspect of the customer experience with fast app launch times? Typically, a good approach is to start by moving any computationally expensive startup functions to a later part of the launch process and reevaluate from there. In our case, we only called or referenced classes in the framework much later in the process, but the framework was still blocking our launch time; why? When an application starts up and loads into memory, the dynamic linker (dyld) is responsible for getting it ready. One of the steps of dyld is scanning through dynamically linked frameworks and calling any module initialization functions that it may have. dyld does this by looking for section types marked with 0x9 ( S_MOD_INIT_FUNC_POINTERS), typically located in the "__DATA" segment. [Screenshot] Once found, dyld sets a Boolean variable to true and calls the initializers in another phase shortly thereafter. [Screenshot] The third-party framework in question had a total of nine module initializers that all, because of dyld, were granted to run before our application ran main(). Those nine initializers attributed to the total cost that delayed our app launch. So how do we fix it? There are several ways we could have gone about fixing the delay. A popular option is to use dlopen and write a wrapper interface for functions that have yet to be resolved. This method meant losing compiler safety, though, since the compiler could no longer reassure that a certain function would exist in the framework at compile time. This option has other cons, but compile safety meant the most to us. We also contacted the third-party developers and asked them to convert the module initializer to a simple function we could instead call at our leisure. They, unfortunately, have yet to reply to us. Instead, we went for a slightly different approach than publicly known methods. The idea was to trick dyld into thinking it is looking at a regular section and therefore skip calling the module initializers. Then, later at runtime, we would grab the base address of the framework with dladdr, and call the initializers at a known static offset. We would enforce this offset by validating the hash of the framework at compile time, verifying sections at runtime, and checking that the section flag had indeed been replaced. With these safety guards and an overall plan in mind, we successfully rolled out this optimization and yielded an additional 36% faster app startup. Conclusion Accurately identifying performance bottlenecks and opportunities is often the most challenging part of any optimization. Notoriously, a common mistake is to measure A, optimize B, and conclude C. That is where good performance tools help highlight bottlenecks and surface them. Xcode instruments, a part of Xcode, come with several templates to help pinpoint various potential issues in a macOS/iOS application. But for additional granularity and ease of use, Emerge Tools provides a simplified view of app performance with their performance tools. Comments Leave a Reply Cancel reply Your email address will not be published. Required fields are marked * [ ] [ ] [ ] [ ] [ ] [ ] [ ] Comment * [ ] Name * [ ] Email * [ ] [Post Comment] [ ] [ ] [ ] [ ] [ ] [ ] [ ] D[ ] Share on: Popular Posts [eleventh-w] Your Deep Links Might Be Broken: Web Intents and Android 12 10 Minute Read [jesse-coll] Migrating From Python to Kotlin for Our Backend Services 14 Minute Read [tian-kuan-] Eliminating Task Processing Outages by Replacing RabbitMQ with Apache Kafka Without Downtime 17 Minute Read Subscribe to stay up to date with the lates engineering news and trends! [ ] [Subscribe] You May Also Like [gif] Backend Culture Data General Machine Learning Mobile Web 2020 Hindsight: Building Reliability and Innovating at DoorDash DoorDash recaps a number of its engineering highlights from 2020, including its microservices architecture, data platform, and new frontend development. [gif]WayneCunningham 8 Minute Read [gif] Mobile Why Apple's New M1 Chips Are Essential for Rapid iOS Development When looking for ways to help our iOS developers become more productive the first step was buying them the new M1 Max Macbook Pros. [gif] Michael Thole 6 Minute Read [gif] Backend Mobile Improving Development Velocity with Generic, Server-Driven UI Components Learn how Generic server driven UI enabled faster iterations and more experimentation on the DoorDash platform [gif] Ashwin Kachhara [gif] Alice Hyun [gif] Afshin Dehghani 27 Minute Read [gif] Mobile How to detect iOS memory leaks and retain cycles using Xcode's memory graph debugger At DoorDash we are consistently making an effort to increase our user experience by increasing our app's stability. A major part of this effort is to prevent, fix and remove any retain cycles and memory leaks in our large codebase. In order to detect and fix these issues, we have found the Memory Graph Debugger ... [gif] Vince Chau 10 Minute Read [gif] Mobile Using Automated Testing to Scale Android Development with a Growing Team At DoorDash, mobile is an integral part of our end user experience. Consumers, Dashers, and merchants rely on our mobile apps every day for delivery. And our Android team moves fast to ship impactful features that improve the user experience and the efficiency of our platform. In the past, we verified the functionality of our ... [gif] Rohan Shanbhag [gif] Thomas Belluscio 5 Minute Read [gif] Mobile Launching Dark Mode While Building a Scalable Design System A large number of our DoorDash deliveries happen during the evening and in late night hours. Dashers, our delivery partners, were finding it really hard to use the Dasher app because the app's bright screens did not adapt to the lower lighting conditions. The abundance of white in the design meant that critical information, such ... [gif] Pradeep Devarabetta Mallikarjun [Screen-Shot-] Laura Rodriguez [Screen-Shot-] Gerardo Diaz 32 Minute Read [gif] Mobile Contract Testing with Pact Introduction As we migrate our systems to a microservices-oriented architecture at DoorDash, we have taken care to balance the excitement around anticipated benefits (development velocity, deployment independence, etc.) with a more measured assessment of our preparedness to work with the complexities introduced by more distributed systems. One of the most common pain points with microservices -- or, ... [Erich-Dougla] Erich Douglass [Wei-Lin-1] Wei Lin [Steven-Li] Steven Liu [Yen-Pai_5] Yen Pai 10 Minute Read [gif] Mobile Group Ordering Just Became Easier with New iMessage Integration Earlier this year, at Apple's Worldwide Developer's Conference, we demoed a way for DoorDash customers to place group orders directly from the new iMessage app. After polishing up the feature for the past few months and seeing the successful roll out of the new iMessage for iOS10, we're excited that DoorDash for iMessage is now available with ... [Andy-Ma] Andy Mai 3 Minute Read [gif] Mobile Programmatic Scrolling with SwiftUI ScrollView Learn about DoorDash's journey exploring programmatic scrolling support in swiftUI. Check out the open sourced solution in this guide [gif] Zoltan Lippai 15 Minute Read Get To Know Us About us Careers Blog Linkedin Glassdoor Accessibility Let Us Help You Account details Order History Buy Gift Card Help Doing Business Become a Dasher Be a Partner Restaurant Get Dashers for Deliveries Privacy PolicyTerms and Conditions [ ] * Home * Engineering Blog + Blog Home + Backend + Mobile + Web + Data + Machine Learning + Culture + General * Locations + San Francisco + Mountain View + New York + Seattle + Los Angeles + Pittsburgh + Toronto * Events * University Subscribe to our Eng blog for updates [ ][Subscribe] Thank you for subscribing! Sign up for updates [ ][Subscribe] Want More Engineering Updates? Susbscribe to the DoorDash engineering blog [ ] [Subscribe for Updates] Loading Comments... Write a Comment... [ ] Email (Required) [ ] Name (Required) [ ] Website [ ] [Post Comment]