How to improve performance in your Metro style app
Nobody likes slow or unresponsive apps. Users expect that apps respond immediately to touch, taps, clicks, gestures and key-presses. Users expect that animations are smooth, that they can play, pause and restart their music and videos quickly, and that they never have to wait for the app to catch up with them. This is the first in a series of posts on how to make your apps “fast and fluid.”
We invested a lot of time in the engineering teams thinking about how we can ensure the performance of Metro style apps. We have learned what we can do in the platform to deliver on fast and fluid performance and have also learned what works and what does not work in building apps that deliver great experiences. In this blog I share with you some of the hard lessons from our own experiences so that you can build the best possible experiences for your customers.
The psychology of performance
Performance is more than just a stopwatch and efficient algorithms. When I think of performance, I like to take a holistic view and consider how users experience their time using apps. What does it mean for an app to be fast and fluid? One way to think about it is to separate a user’s experiences into three categories: perception, tolerance, and responsiveness.
Perception: This is taking forever!
Perception contributes to the “fast” in fast and fluid. We define a user’s perception of performance as how favorably they recall the time it took to perform their tasks within your app. In a perfect world a user’s perceptions would match reality. But often this is not the case, and their perception of time matters more than the reality. Have you ever left an installation to finish on its own, and come back to realize it’s paused part way through, waiting for you to answer one more question?
The more steps you can remember about a process, the slower it seems.
- Reduce the amount of time between activities that the user needs to perform to accomplish their task
- Make sure that anytime you need to ask the user questions or have them provide info that you are asking all of the questions up front.
- Separate out user activities into multiple periods with some time in between.
Tolerance: Time flies when you’re having fun
Tolerance contributes to both the fast and fluid of fast and fluid. If perception is a measure of a user’s recollection of how much time passed, then tolerance is a measure of how favorable the passing of that time is.
When a user doesn’t know how long an action will take, the wait is painful. Imagine you’re using an app to do some photo editing work. When you click to apply a filter, the app becomes unresponsive. The time it spends frozen quickly becomes intolerable, even if it is just for a few seconds.
Photo apps have solved this problem by adding a progress bar or small animation to indicate that it is working on applying the filter. The amount of time users are willing to wait for the work to complete is far higher because there is no uncertainty. The app is both fast and fluid, and the difference is profound.
- Take a moment to identify the areas of your app that may require a substantial (>= 1s) loading time.
- Take steps to limit or eliminate user uncertainty during these scenarios.
- Give users a visual indication of where they are in the process and how long it will take.
- Use async APIs to avoid blocking the UI thread and making the app appear frozen.
- Undergo long running actions without providing user feedback.
Responsiveness: Reflexes < reactions < acknowledgements
Responsiveness contributes to the “fluid” in fast and fluid. If tolerance is a measure of the expectations and favorability of time, then responsiveness is the concept that the expectations of time are relative to the activity being performed. To measure and rate the performance of an activity, there must be a time interval to compare it against. I’ll refer to these time intervals as interaction classes. Internally we use these interaction classes to define responsiveness goals for core scenarios throughout Windows and track failures to meet these goals as bugs in the product.
|Interaction class||Target||Upper bound||Human perception||Typical scenario|
|Instant||<= 50 ms||100 ms||No noticeable delay.||Input response – mouse click, button tap, etc.|
|Fast||50 – 100 ms||200 ms||Minimally noticeable delay. No feedback necessary.||Pan/Scroll|
|Typical||100 – 300 ms||500 ms||Quick, but too slow to be described as fast. No feedback necessary.||Open an in-page dialog (i.e. an info tip, pop up, fly out, toast, etc.)|
|Responsive||300 – 500 ms||1 sec||Not fast, but still feels responsive. No feedback necessary.||Navigate to a new page, zooming, display processed or ready data|
|Continuous||>500 ms||10 sec||Medium wait, no longer feels responsive. Not long enough to do something else. May need feedback.||Launch the app, Snap the app, error messages, time-outs, update progress indication|
|Extended||>5 sec||>1 minute||Long enough to do something else while waiting. May need feedback.||Sync or index a device/library|
- Assign the important scenarios of your app to an interaction class representing the desired experience.
- Identify if your scenarios are not meeting these goals and optimize them.
- Identify if a scenario may require user feedback and provide it.
Key experiences to optimize
Performance problems show up in various ways. They can reduce battery life, cause panning and scrolling to lag behind the user’s finger, or even make the app appear frozen for an extended period of time. I know that you don’t have infinite time to work on improving performance, so this section will go over some tips on how to identify areas where optimization will have the most impact, and some guidelines that can help you make those optimizations.
One way you can think about making optimizations, is that a process needs to be 20% faster or 20% slower before a user perceives a difference.
You may find “low-hanging fruit” in your app that can greatly increase performance for your key scenarios. But when you fix these issues, it becomes increasingly unlikely to stumble across a single performance issue which will immediately reduce the duration of the process by 20%. More often it is an accumulation of smaller problems along the same path which all contribute to the same user experience issue, and in the next section I discuss a great tool for discovering these paths.
One of the simplest and most readily available techniques for determining where optimizations have the greatest effect is to perform app profiling. Profiling provides data on how much time your app is spending in its various functions, so you can see the hot spots where the app is doing most of its work. Thankfully there is a great profiling tool available in Visual Studio 11 that you can access on the Windows 8 Consumer Preview.
But before you begin, remember that Windows 8 will be run on a wide variety of devices and taking performance measurements on powerful hardware may not truly show performance characteristics on other form factors. For many of my own features, when performing measurements I use a lower-end laptop. For more info see the blog on Running the Consumer Preview: system recommendations.
Here’s how to prep a machine for measurements:
- Make sure the machine is plugged in and not running on battery – many systems operate differently on battery to conserve power.
- Don’t use remote desktop to perform measurements because it can disable hardware acceleration and skew results.
- Ensure that the total memory utilization on the system is less than 50%. If it’s higher, close apps until you reach 50% to make sure you are measuring the true impact of your app and not other processes.
To begin profiling your app:
- Launch your app in Visual Studio.
- On the Debug menu pick one of the two Performance Analysis options:
- Start Performance Analysis: Immediately start recording usage info and launch the app.
- Start Performance Analysis Paused: Launch the app and later you can resume recording of info. This is used to get the app into a particular state before recording info (such as to test a specific user scenario).
Two of the most useful views in the performance report are the Call Tree view and the Function Details view. In the Call Tree view, you can see all the functions your app called during execution, how many times they were invoked, and how much time they took to execute. There is also an Expand Hot Path button, which shows you which functions took the longest time to execute. These are the first areas for you to concentrate your optimizations, because they will likely have the largest effect.
The report shows both the inclusive and exclusive times for each function. Exclusive time measures the percentage of time spent executing code in that function. Inclusive time is the percentage of time between when the function was called and when it returned. In other words, it is not only the time spent executing code in the function, but also the time spent executing code in any functions that it calls.
For any of the functions in your app, you can open the Function Details view to get detailed info about what the function did, including the specific code that it executed and what functions it called and how long these functions took. In the example shown in the figure, Array.concat function is the major contributor to the hot path, taking up 29.7% of app execution time. The Function Details view shows us that Array.concat function is actually only taking up about 12.6% of the time itself (shown in orange), and that the majority of the time is actually taken up in get_MainResourceMap function, which the concatenation function calls as part of its execution (shown in purple). You can then click on any of these functions to get more details.
A few weeks before the release of the Windows 8 Consumer Preview, I was using one of the apps and noticed that my laptop was getting hot and the fans started to turn on. Working with the owner of the app, we used profiling to identify some code paths that were causing large amounts of unnecessary CPU usage, which were then fixed. The difference was immediately noticeable, and other app scenarios (such as snapping) were also improved by the changes.
Suspend is your friend
App launch occurs only when your app is not suspended – otherwise your app pops back into view nearly instantly as it is resumed. Keeping your app suspended is a technique for managing perception, tolerance, and responsiveness. Users enjoy your app more quickly, and never experience the uncertainty of waiting for your app to load each time they use it. In short, keeping your app from being terminated is a performance grand slam. The easiest way to do that is to keep your app’s memory usage low when suspended.
Before your app gets suspended, it has a few seconds to do some work. During this time, free any large objects that can be easily re-acquired on resume. This keeps your app’s memory footprint low, and greatly reduces the likelihood that the system terminates your app to make room for something else. Here’s how you can see if your app correctly suspends:
- Launch the app and then go back to the desktop.
- Launch the Task Manager by pressing Ctrl+Shift+Esc, and click “More details” to see all available options.
- Click View > Status values > Show suspended status.
After a few seconds, the word “Suspended” appears next to your app’s name. While suspended, make sure your app’s private working set memory usage is significantly lower than when it is running. I recommend that you target these memory metrics for your app while it is suspended, based on its complexity and general size:
|App complexity (approx.)||Private set while suspended (max)|
|Minimal app (ex. Hello World)||40-60 MB|
|Medium app (ex. Weather)||60-80 MB|
|Large app (ex. Windows Live Photos)||120-150 MB|
Here’s how you can see how much memory your app is using while suspended:
- Make sure the app is suspended using the steps we saw earlier.
- Make sure that the “Memory (private working set)” column is present. If not, right-click any column, go to Select columns, and select Memory (private working set).
- The value listed under this column is the private working set of your app.
Being a good citizen in the app ecosystem
As disappointing as it may sound, your app is not the only one that will be used . Therefore, be a good citizen when running on the user’s system so that they don’t begin to attribute any perceived latencies in the system to your app and start to mentally relate it to bad performance or battery life on their system. Here are a couple tips to help your app play well with others.
Don’t spike memory usage
The system’s job is to accommodate the resource needs of all Metro style apps by automatically terminating suspended apps to make room for new ones, freeing the user from having to manage resources. A side effect of this is that if an app requests a large amount of memory, then other apps might be terminated– even if the app frees that memory soon after requesting it. To avoid this issue when tackling heavyweight operations, it is best to approach them in small chunks.
Regardless of language, I recommend that you target these runtime memory metrics for your app, based on its complexity and general size:
|App complexity (approx.)||Total working set (max)|
|Minimal app (ex. Hello World)||50-70 MB|
|Medium app (ex. Weather)||80-100 MB|
|Large app (ex. Photos)||120-150 MB|
Here’s how you can see how much memory your app is using while running:
- Launch the Task Manager by pressing Ctrl+Shift+Esc, and click More details to see all available options.
- Click the Options menu item, and make sure that Always on top is checked.
- Launch your app. When the app appears in Task Manager, right-click it and click Go to details.
- Make sure that the Working set (memory) column is present. If not, right-click any column, and go to Select columns. Check the Working set (memory)column.
- The value listed under this column is the total working set of your app.
Minimize use of system resources while idle or snapped
One of the biggest concerns for users is the battery life of their devices. Therefore, I recommend that your Metro style apps help conserve energy by freeing their use of system resources when the user is not actively interacting with them. Here are just a few tips to reduce resource usage:
Don’t run animations, audio, or background videos while snapped.
- Your app is no longer the center of attention. Make your app feel colorful and usable, but don’t take away from the resources available to the primary app or use the battery.
- This assumes your app isn’t a dedicated music/video playback client.
Don’t run animations, audio or background videos in an infinite loop.
- If the user hasn’t interacted with the app over some small duration, pause these activities. You can safely resume them when the user next interacts with your app.
- I’ve seen apps go from 40% CPU usage while idle down to 0% simply because they paused their background activities.
Don’t perform file caching, device syncing, or any other disk-intensive operation while snapped or idle.
- Using the disk unnecessarily increases seek times for other operations and uses a lot of power.
In this post I described how you can think about performance, methods for setting goals for the key experiences in your app, and talked about some tools to help identify areas that you might need to optimize. My next post will cover how to go about making these optimizations for the biggest problem areas, and how to avoid some common pitfalls. I hope these tips will prove useful to you!
Until next time,
– David Tepper, Program Manager, Windows
Leave a Reply
You must be logged in to post a comment.