[ Music ]
[ Applause ]
Hello everyone, my name is Spencer Lewson, and I'm an engineer on the Performance Team here at Apple.
Today I'm very excited to tell you about how you can optimize your app's launch.
We'll be covering these four main topics today.
First, what is launch?
What are the different types of launches and how do we break them down into their different subphases?
Next, we'll be talking about how to properly measure your app's launch.
Out in the field, iOS devices can be in a variety of different states and conditions, and these states and conditions can produce inconsistent launch results.
So, it's important to understand these states and how to reduce their impact when you're taking measurements.
Once you've done that, you can take a look at how to use Instruments to profile and understand your app to find opportunities to improve it.
And finally, we'll leave you with some tips and some tricks on how to monitor your app's launch, both over time and in the field, to ensure that you consistently deliver a delightful experience to all of your users.
So, what is that app launch I was talking about?
Well, app launch is a user experience interruption.
What do I mean by this?
Let's take a look.
Okay, ready, set, go.
Wow, on the iPhone 6S Plus, launch took nearly 2.5 seconds, and this wasn't as delightful as our users expect it to be.
You know, it's really important for launch to be delightful, because it happens a lot.
In fact, across all iOS devices, it happens billions of times a day.
So, we did some number crunching, and we figured out that with we save only one millisecond on each of those launches, we would save an astounding 162 days of launch time, yes, in other words, [applause] thank you, in other words, that's the amount of time it takes to send a rocket to Mars.
But it's also important for a number of other reasons.
First of all, your app's launch is your user, first experience with your app, and as such, it should be delightful.
Now it's important to remember that as developers, we tend to gravitate towards newer devices.
So, it's important to ensure that the experience that you see in your hand is the same experience that the customers, that your users, have in their hands on different iOS devices and under different conditions.
Furthermore, launch covers a huge part of your code base, from primer coating, to initialization, to view creation, and more.
And as such, if you're seen that your launch isn't as delightful as your users expected to be, this might be indicative that there's other parts of your code base that are delightful, as well.
Finally, launch is a very intense time for the phone.
Involves a lot of CPU work and a lot of memory work.
So, you should try to reduce this as it impacts the system performance, and of course, your user's battery life.
So, let's take a look at those launches I talked about before, there's a cold launch, a warm launch, and something is often referred to as launch, but isn't quite a launch, a resume.
Cold launches occur after reboot, or when your app has not been launched for very long time.
In order to launcher app, we need to bring it from disk into memory, startup system-side services that support your app, and then spawn your process.
As you'd expect, this can take a little time, but fortunately, once it's happened once, you'll experience a warm launch.
In this case, your app still needs to be spawned, but we've already brought your app into memory and started up some of those system-side services.
So, this will be a little bit faster and a little bit more consistent.
Finally, there's that resume.
This occurs when a user reenters your app from either the home screen or the app switcher.
As you know, the app is already launched at this point, so it's going to be very fast.
What you need to remember from this is not to confuse resumes with launches when you're taking measurements.
So, given this information, wouldn't it be great if launches were as quick and as delightful as resumes?
How can we achieve that?
Well, we need to hit the goal of rendering our first frame within 400 milliseconds.
That's so that we have pixels displayed to the user during the launch animation, and by the time that launch animation is complete, your app is interactive and responsive.
The first step to doing that is understanding what is happening during launch.
So, let's launch Maps.
As you know, launch generally starts when the user taps your icon on your home screen.
Then over the next 100 or so milliseconds, iOS will do the necessary system-side work in order to initialize your app.
That leaves you as developers about 300 milliseconds to create your views, load your content, and generate your first frame.
Now this frame doesn't necessarily need to be fully complete.
It can have some placeholders for asynchronously loading data, but at this point, your app should be interactive and responsive.
So, in the case of Maps, all the tiles have not yet loaded.
You can still initiate your search and browse your favorites.
Then over the next couple hundred milliseconds, you can display that asynchronously loaded data and generate your final frame for your user.
Let's take a closer look at these phases.
These six phases cover everything from system initialization to the app initialization to view creation and layout, and then depending on your app, potentially a asynchronous loading phase for your data, the extended phase.
The first half of system interface is dyld.
For those of you unfamiliar, a dynamic linker loads your shared libraries and frameworks.
Now in 2017, we introduced dyld 3, which added exciting optimizations to the system.
Well, we're happy to announce that in iOS 13, we're bringing these optimizations to your apps.
That means we are now caching your runtime dependencies, or warm launches, which should give them a significant speed improvement [applause].
Now with a new linker, comes some new recommendations.
To take full advantage of these new improvements, we recommend that you avoid linking unused frameworks, as this can have hidden costs, which we'll show you later.
We also recommend that you avoid dynamic library loading, such as DLOpen or NSbundleLoad, because this forfeits those wins you would have gotten by having those in your cache.
Finally, that means that you should be hard linking all of your dependencies, as it's now even faster than it was before.
The second half of system interface is libSystemInit.
This is when we initialize the low-level system components within your application.
Now this is mostly system-side work with a fixed cost.
So, use developers don't need to focus on the section.
Now we have static runtime initialization.
This is when the system initializes your Objective-C and Swift run times.
Now in general, your app shouldn't be doing any work here unless you have static initializer methods, which are possibly present in your code, or more likely, a surprise from the frameworks that you link.
In general, we don't recommend static initialization.
So, let's take a moment to talk about how to reduce its impact.
If you own a framework which uses static initialization, consider exposing API to initialize your stack early.
But if you must use static initialization, consider moving code out of class load which is invoked every time during launch to class initialize, which is lazily invoked the first time you use a method within your class.
Next up is UIKit Initialization.
This is when the system instantiates your UIApplication and your UIApplicationDelegate.
For the most part, this is system-side work, setting up event processing and integration with the system.
But you can still effect this phase if you subclass UIApplication or you do any work in UIApplicationDelegate initializers.
Now we have application initialization.
This is where the good stuff is.
This is where you as developers can likely have the biggest impact on your app's launch.
For those of you who have not yet adopted the new UIC in APIs or are targeting iOS 12 or earlier, Application Init works, again, with these delegate call-back methods.
application: willFinishLaunchingwithOptions, and application: didFinishLaunchingwithOptions.
As your app is displayed to the user, the further methods, applicationDidBecomeActive: will be invoked.
Now it's important to know that if you have not UIScenes, you should be creating your view controllers and didFinishLaunchingwithOptions.
That's because with UIScene, ApplicationInit works a little bit differently.
Now you will still get willFinishLaunching and didFinishLaunchingwithOptions, but as your app is displayed to the user, you will get the UISceneDelegate lifecycle callbacks.
Those are, of course scene: willConnectwithSession with options.
ScenewillEnterForeground, and sceneDidBecomeActive.
You should be creating your view controllers, and scene: willConnecttoSessionwithOptions.
It's important to note that you should be only creating your view controllers, and scene: willConnectToSessionwithOptions, and that also, and didFinishLaunchingwithOptions.
That the common pitfall, which, of course, results in performance losses and, likely, unpredictable bugs in your code base.
Regardless of whether or not you've adopted the new UIScene APIs, our advice for this phase is generally the same.
You should be deferring any unrelated work but it's not necessary to commit your first frame, by either pushing it to the background queues or just doing it later entirely.
If you did adopt UIScenes, you can do one more thing, and that's make sure that you're sharing your resources between your Scenes.
This is, of course, to reduce the overhead of doing any work unnecessarily multiple times.
To learn more about UIScenes, please take a look at these two talks from earlier this week.
Next is the first frame render phase.
Now, this is relatively straightforward.
This is when we create your views, perform layout, and then draw them.
We then take that information, and we commit and render your first frame into nice, shiny pixels.
You can affect this phase by reducing the number of views in your hierarchy.
And you can do that by flattening your views to use less or by lazily loading views that are not shown during launch.
You should also take a look at your auto layout and see if you can reduce the number of constraints you're using.
Finally, we have the extended phase.
This is the app-specific period from your first commit until when you show your final frame to your user.
This is when you load that asynchronous data we talked about.
Now not every app has this phase.
But if you do have this phase, your app should be interactive and responsive.
If you do have this phase, we only have general advice on how you should approach it, and that is to understand what is happening, and you can do that by leveraging OS signpost APIs to mark out and measure the work that occurs in between these two time periods.
Now that we talked about what launch is, let's talk about how to get usable measurements.
At any given time, an iOS device is under a variety of different states and conditions, and this can introduce substantial variance into launch.
So, when we're analyzing and comparing our launch results, it's critical to ensure that we're making apples-to-apples comparisons, because how do you know if you're making any progress if before you make any changes, your launch results are completely unpredictable?
The first step to making them predictable is removing those sources of variance, such as networking interference and interference in background processes.
Now we realize that this sounds counterintuitive, as this may result in a launch that's not entirely representative of regular usage, but we wanted to let you know that that's okay.
It's more important to have consistent results with which you can evaluate progress.
At Apple, we've been using this technique to successfully detect regressions during development and drive down launch times.
We then validate these performance improvements by using telemetry collected from the field.
Fortunately, we have some tips on setting up that clean and consistent environment.
First, reboot your device.
This will clear out any unnecessary state, and then let it settle down over the next few minutes to clear up any boot time work.
You could also reduce your dependence on the network by either turning on airplane mode or marking out your network dependencies in code.
Networking can introduce a fair amount of variance.
Next is iCloud.
ICloud is a great feature which works in the background to deliver a seamless experience to our users, but that work in the background can interfere with app launch.
So, during your measurements, using unchanging iCloud account with unchanging data, or log out of iCloud entirely.
Next be sure to use the release build of your application when you're making measurements.
This is, of course, to reduce the overhead of unnecessary debugging code during your measurements and to take advantage of the compile time optimizations.
Finally, you should be measuring with warm launches, which as mentioned before, are more consistent, because some of your app may already be in memory, and some of those system-side services may already be running.
Now we can set up some data to test with.
It's important to create a mock data set which is consistent, and you might need a couple data sets for different types of users, such as users with small amounts of data and users with large amounts of data, though, in the ideal situation, your app should be able to scale to any amount of data.
That's why loading only the data that is necessary to show your first frame.
Now we're ready to pick out some devices.
You should pick out a variety of devices that are important to your users and then stick to them force consistency.
Be sure to include your oldest devices for your oldest-supported releases.
This is because performance characteristics look different between older devices and newer devices, which have different amounts of RAM and CPU cores.
This will ensure that your launch is delightful for all of your users on all of their devices.
Now we're ready to take some measurements.
We can leverage the new XCTest for app launce performance in Xcode 11.
With just a few lines of code, Xcode will launch your app repeatedly and then provide statistical results about how it performs.
We'll talk about this more later.
So, now we've talked about what launch is and how to measure it, let's talk a little bit about how to improve it.
When you're reviewing your app's launch both in code and in instruments, you should keep these three tips and tricks in mind.
That is to first minimize your work, then prioritize your work, and finally, optimize your work.
When minimizing work, you should be deferring anything unrelated to generating the first frame.
That means deferring things like undisplayed views or pre-warming features that are not yet used.
You should also avoid blocking the main thread, either with network I/O, file I/O, or more, as this will affect launch.
Move it to a background thread.
Finally, you should take care to reduce your memory usage.
Allocating and manipulating memory can take time.
Next, prioritize your work.
This is when you should make sure that work is scheduled at the right quality of service.
Now in iOS 13, we've made some exciting optimizations to the Scheduler to make your apps launch even faster.
But that means it's more critical than ever to preserve priority issue propagate work across threads.
You should take a look at Modernizing Grand Central Dispatch Usage from WW 2017, which goes into depth about how to handle concurrency correctly.
Finally, we have optimizing work.
Anything that's remaining after you've minimized it and prioritized it should be optimized.
That is to say it should be simplified and limited.
For example, limit the amount of data that you fetch only what you need during launch, or lazily compute any variables and results that you need.
When you're doing this, take a look at your methods and algorithms and see if you can optimize them, as you could get significant improvements by calculating a result differently or using a different data structure.
And finally, you should be caching your resources and your complications.
This is, of course, to reduce the CPU and memory overhead by doing work multiple times unnecessarily.
So, I'd love to hand the stage over to Dan, who is going to give you a great demo on how to use the new App Launch Template in Xcode Instruments to understand and improve our app's launch.
[ Applause ]
Thank you, Spencer.
Hi, everyone, my name is Dan Sawada, and I'm also one of the performance engineers here at Apple.
Today I will be going over a typical workflow of understanding your app's launch and looking for opportunities to minimize, prioritize, and optimize the work, so that you can actually deliver a delightful first user experience.
So, let's get started.
The app that I'm going to be demonstrating today is called Star Searcher.
It's an example app that we specifically written for this session.
As you can see, it's a very typical UI table view that lists all of my imaginary stars.
If you click on the cell, or a star, it shows you a little description blurb, in addition to a picture.
However, we have one problem, let's go ahead and launch it.
So, that took an astounding 2.5 seconds to launch, not sure if I could call that delightful.
So, let's use Xcode and Instruments to see if there's anything we can do about this.
So, here we have our Xcode project for Star Searcher.
Now one important thing that we should do before we do any performance-related analysis is selecting the profile scheme in Xcode.
This will ensure Xcode to recompile your app in release mode, so that you can take the advantages of compiler time optimizations.
Once Xcode recompiles your app, it will install it on your device and launch Instruments.
Now we are happy to announce that as of iOS 13, or Xcode 11, we now have the AppLaunchTemplate, which we can use specifically for triage purposes like this, figuring out what's wrong with AppLaunch.
So, let's go ahead and double-click on AppLaunch.
Now the first thing we want to do here is hit the record button.
At this point, Instruments will automatically launch Star Searcher, our app, gather all of the metrics, telemetry data, analyze them, and create visualizations for all of the app launch phases.
So, with take a look.
The first few phases marked in purple are the phases that occur before your main function is invoked within your app.
Onto the green phases, these phases of the early phases that occur at the very first of your main function, as your app finishes its launch and draws its first frame in UI.
Let's go ahead and expand the lanes.
As we expand the lanes, you can see the detailed states of all of the threads that respond within your app's process.
Obviously, the most important one would be the main thread, or also known as the UI thread, which is responsible for handing user input and drawing your UI.
Let's go ahead and pin down the lanes that are relevant for our purpose, starting with the app launch phases, our main thread, and there's one more worker thread that's doing a substantial amount of work during launch.
So, let's go ahead and pin this down, too.
Speaking of thread states oops.
Speaking of thread states, gray means it's blocked, meaning that the thread isn't doing any work.
Red means it's runnable, meaning that there's work scheduled to be done, but lacking CPU resources.
Orange means it's preempted, meaning that it was doing work but got interrupted in favor of other competing work that has a higher priority.
And last but not least, blue means it's running, meaning that it's actually doing work on the CPU core.
So, with that information, let's take a look phase by phase starting with the system interface initialization.
As we triple-click on a phase, we can highlight the phase and get detailed information towards the bottom half of the screen.
To your left, you can see the detailed stack trace of all the work that's being done during this time period.
To your right, you can see a aggregated stack trace, which lists all of the symbols ordered by the number of CPU sample size.
Now notice that this initial phase only took 6 milliseconds as it sets up its system interfaces.
This is primarily due to the benefits of dyld3 introduction and third-party apps, in addition to other system layer enhancements.
So, as developers, we can take advantage of all of those enhancements without writing a single line of code.
Let's move on, but before we do so, there's one other thing I should point out here.
Notice that while this phase only spent 6 milliseconds on the CPU clock for Star Searcher, it spent 149 milliseconds on the wall clock.
This discrepancy comes from the overhead of the profiling mechanism itself, which does give us a lot of information and insight, but has a cost of its own.
So, this is why it's very important to distinguish profiling with measurements, which I will explain more later on.
On to the next phase, which is static runtime initialization.
Now notice this phase took an astonishing 375 milliseconds.
Now that's a little bit too long.
So, let's take a look.
Looking at the detailed stack trace, we see a highlighted symbol with a blue icon marking 370 milliseconds' worth of work on the CPU.
Now all of these highlighted symbols indicate code that's declared within our sources.
Let's click on it.
Now by expanding the stack trace, it points us to the SLSuperfastLogger.
Now, if a library is calling itself superfast, that implies some fishiness, but let's take a look.
So, SLSuperfastLogger is a external framework that we've imported specifically into Star Searcher to take the benefits of powerful and convenient logging.
However, the only place we invoke this framework is within the table view controller.
Specifically, within the didSelectRowAt callback.
Now this callback is completely out of the launch path, because it's only invoked when the user taps on a cell.
So, why is it doing over 300 milliseconds' worth of work during early launch and even before our main function is invoked?
Well, let's investigate.
By searching the symbol, it points us to a plus-load method declared within the SL SuperfastLogger class.
Now, this is a static initializer, meaning that all of this work would be done at very early in launch before a main function is invoked, given the fact that we link against it.
Now, the take away here is that it's very important to understand the impact of your dependencies in the frameworks that you use.
External libraries and frameworks may be convenient and may be powerful, but it may come with a heavy cost.
So, if those costs justifies the benefits, great.
But for our case, 300 milliseconds during launch is a little bit too much for what it's worth.
So, let's go ahead and pursue alternatives.
In our case let's use os.log, which is a very lightweight and efficient logging mechanism that comes right with iOS as well as other Apple platforms.
Now once we remove the dependency, there's one additional thing that we absolutely need to remember to do, which is to remove the actual linkage.
Now because the cost here is with a static initializer, we need to make sure to remove the linkage in order for it not to impact us.
So, with that, let's go back to our trace.
The next phase is UIKit initialization, which took 28 milliseconds on the wall clock.
Now this is pretty much a fixed cost for all applications.
So, unless you subclass UI application or do a custom initialization work in UIApplicationDelegate, it's pretty much something that we can disregard for now.
So, let's move on.
The next chunk of work is your applications initialization, which is pretty much what you control.
Now notice there is a big amount of work being done with didFinishLaunchingWithOptions callback, which took 791 milliseconds on the wall clock.
Now that's very long.
Let's take a look.
So, this phase immediately points us to heavy amounts of work in the StarDataProvider class.
It says, "loading stars."
Okay, now, notice that there's a huge blockage in the main thread, which essentially is a delay in our launch.
Our main thread was blocked for 754 milliseconds.
Now that's not nice.
Let's take a look.
So, in order to inspect the detailed states, we should look at the event list.
By looking at the event list, we notice that it was blocked for 754 milliseconds, and afterwards, it was unblocked, or made runnable, by thread 0x12253.
Now this corresponds to this worker thread that was doing a lot of work.
So, there's some relationship here.
Now going back to the main thread, notice that it's scheduled to do work at priority 47.
Forty-seven is equivalent to the user interactive QoS.
Now look at all this red meeting there's a lot of work to do, but it's lacking CPU resources.
Well, let's figure out why.
As we click on the worker thread, we notice that there's a lot of work scheduled to do work at priority 4.
This is equivalent to the background QoS.
What we're actually seeing here is a symptom known as priority inversion, where a given thread is being blocked by a separate thread that has a lower QoS, or priority, than itself.
Obviously, this isn't ideal, because it's still aimed to launch more than it should.
So, let's go ahead and try to fix that.
Looking back at the StarDataProvider, which is at the core of this issue, is a very simple class that's responsible for fetching data for our stars from SQLite database, has a dedicated dispatch queue with a background QoS, and note that this is to ensure that data fetching doesn't compete with the UI.
And there's two API being exposed.
One for loading data asynchronously using this GrandCentralDispatch's async primitive and another synchronous API that loads the data in a synchronous fashion.
Now looking at the actual call sites within the didFinishLaunchingwithOptions, we are leveraging the asynchronous API, but also leveraging the dispatch semaphore to ensure that we wait for all of the data to be fetched before we proceed on to drawing the actual first frame of our table view.
Now if we're going to be doing this, we should use the correct concurrency primitive, which is the sync primitive in GCD.
Now using the correct concurrency primitives, GrandCentralDispatch will temporarily propagate the priority of the main thread to the worker thread and boost it up to user inactive so that it matches.
So, at this point, I think we have the potential to resolve the priority inversion, but there's one more issue that I notice here.
LoadStarDataSync API accepts a range of rows to load the data for.
In our case, we're loading from row 0 to the very last row, which is essentially everything.
Now when you think about it, the first frame can only fit just a limited number of cells that may be on the screen size.
In the case of Star Searcher, perhaps around 10 to 15, depending on the device.
So, let's go ahead and optimize that, and instead of loading everything, let's just load the first 20 rows, just enough to draw the first frame of the table view in a synchronous fashion.
Afterwards, we should load all of the rest lazily in the background and only update the table view when finished after launch.
Let's move on.
Back to the trace, last but not least.
The last phase is our first frame rendering.
Notice that this phase took 951 milliseconds, which is very long, considering that this is only responsible for doing the layout work and the rendering of our first frame.
Now let's taking a deeper dive, it points us to the StarTableviewController, and looking at the detailed stack trace, we see a lot of work and a CellForRowAt callback, which is responsible for doing the layout work of the cells.
let's go ahead and expand that.
As we expand the stack trace, it points us to a lot of initialization work for the StarDetailView controller which took 882 milliseconds on the CPU.
So, at this point, we've identified this is pretty much the bottleneck here.
Let's take a look at our code.
Now looking at the table view controller within the CellforRowAt callback, we create the cells using our custom cell, and at the same time, we put in a speculative optimization which is to pre-warm and cache the DetailViewControllers of the DetailVew, as we do the layout work.
This is with the hopes to streamline the transition from a table view to a detail view.
But as we saw in the trace, this doesn't create a high cost.
Now stepping back a little bit, when you think about it, the detailed view doesn't really make sense for our first frame.
It only makes sense when the user taps on a cell.
So, let's go ahead and defer that work.
Where should we defer it to?
Perhaps the didSelectRowAt callback, which is invoked when the user taps on a cell.
So, at this point, we've made several enhancements, or optimizations, to Star Searcher.
So, let's go ahead and re-profile it.
Now one thing to note here is that as you make incremental changes, you should consistently remeasure and re-profile as you make progress.
That way, you can actually understand the exact impact of your incremental change set.
But for the sake of his demo, we've actually aggregated all the changes into one for the sake of time and boom.
There's a little UI glitch, but we can immediately see that our launch is under 500 milliseconds.
Now, as I said earlier, the profiling mechanism does come with a cost of its own.
So, to get a better understanding of what our users would experience, let's go ahead and leverage the new XCTest APIs to measure our launch performance within our test.
With just a few lines of code, we can actually integrate launch performance tests, or any performance tests, with an XCTest.
Let's go ahead and kick this off.
Now at this point, XCTest will do one throwaway launch attempt, which cancels out the variance that would come about by cold launches.
Afterwards, it will do the specified number of iterations or by default five iterations of launches and measure the time it took.
Afterwards, it will produce a nice statistics of that data.
It's going to take a few minutes for the test to complete, and now we've taken the launch of Star Searcher from 2.5 seconds to just over 300 milliseconds.
[ Applause ]
And to wrap up the demo, I'd like to show you what this actually looks like on the UI.
So, let's make sure we kill Star Searcher.
That was quick.
[ Applause ]
Back to you, Spencer.
[ Applause ]
Thanks, Dan, for that awesome demo on how to use Xcode, Instruments, AppLaunchTemplate to improve our app launch experience.
So, we realize that in your code bases, you're not going to find just a few couple places in your code that you can fix with just a few lines and get such substantial improvements.
It's more likely that you're going to have to find a bunch of 5 to 10 milliseconds wins and then stack all those together.
We want to let you know that we've got your back.
We've been making a ton of iOS optimizations to improve your app's launch and help you reach your goal with very little to no adoption from your side.
I want to call on a few in particular.
As mentioned before, dyld3 brings caching of your runtime dependencies to your apps, which you saw in the demo, that provided a huge improvement.
The Scheduler has also been optimized to help prioritize the work that happens during launch.
We also put Auto Layout and Objective-C under the microscope and made a bunch of optimizations there.
And then finally, we have exciting changes to app packaging coming later this year.
We think that altogether these changes should result in a huge improvement your apps with very little to no adoption.
So, let's wrap things up with some tips and tricks on how to make sure your app stays delightful once you've done all this work.
First of all, don't let performance be an afterthought.
You should start working on it and thinking about it at the beginning of every bug fix, at the beginning of every re-factor, and the beginning of every feature.
This is because it's incredibly easy to introduce regression, especially a little one like 2 milliseconds.
The problem is these little ones add up to a big problem, and if you don't address them immediately, it becomes very hard to find them all.
In order to do that, to detect those regressions, you should be plotting your app's launch over time and running tests regularly.
This will ensure that you're meeting your target and that you immediately know if you've regressed from that target.
You should also take a look at the new Xcode organizer, which lets you know how your app performs in the field.
In iOS 13, for users that have opted in, power and performance metrics will be gathered about your app.
They will then be aggregated over 24-hour periods and sent back to your organizer where you can view them in the form of histograms by software version and device version.
However, if you desire a little bit more control over that data, you can adopt MetricKit.
MetricKit allows you to specify custom power and performance metrics.
Now like the organizer, this data will be gathered up and aggregated over 24-hour periods of time and then delivered back to you through a delegate method in your app.
From there, you're free to handle the data as you see fit.
To learn more about this, we recommend you check out Improving Battery Life and Performance from WW 2019.
So, in summary, we'd love for you today to start understanding your app's launch with the new AppLauchTemplate in Xcode Instruments.
See if you can find opportunities to minimize, prioritize, and optimize your work.
Next, although well intended, not all optimizations work out, such as the pre-warming DetailView controllers that Dan addressed in his demo.
So, be sure to measure, not estimate, performance whenever you're making changes.
Again, it's very easy to introduce regressions unintentionally.
Finally, you should be tracking your performance in all phases of development.
This means utilizing the new XCTest app launch measurements on a variety of devices and possibly integrating this with continuous integration.
This will ensure that you're consistently delivering a delightful app launch to all of your users on all of their devices.
For more information, please view the talks that we referenced today, and have a great rest of your Friday afternoon.
[ Applause ]