"Your feedback is important to us; please stay on the line..." [Improving Windows Phone 7 application performance is even easier with these LowProfileImageLoader and DeferredLoadListBox updates]
Windows Phone 7 applications run on hardware that's considerably less powerful than what drives typical desktop and laptop machines. Therefore, tuning phone applications for optimum performance is an important task - and a challenging one! To help other developers, I previously coded and blogged about two classes: LowProfileImageLoader (pushes much of the cost of loading images off the UI thread) and DeferredLoadListBox (improves the scrolling experience for long lists). These two classes can be used individually or together and have become a regular part of the recommendations for developers experiencing performance issues.
In the time since, I've continued to hear from people who are benefitting from
DeferredLoadListBox - and the code has even been incorporated into the WP7Contrib project! Along the way, I've also collected some great feedback, so I recently dedicated time to make a few improvements:
The most significant change is that I've removed the use of the UIElement.TransformToVisual platform-level method from
DeferredLoadListBoxbecause it has proven to be unreliable on Windows Phone 7 by throwing exceptions unexpectedly. Because this is not the first time I've had to fix crashes due to random ArgumentExceptions ("The parameter is incorrect."), I recommend not using the
TransformToVisualmethod in Windows Phone 7 applications until/unless the underlying problem is fixed. In the meantime, it has been my experience that the LayoutInformation.GetLayoutSlot method can often be used as a substitute with just a little bit of extra effort.
Aside: Another time I had to remove
TransformToVisualwas for the Silverlight for Windows Phone Toolkit's
ContextMenucontrol. (This fix was part of the November 2010 release).
I've previously explained why DeferredLoadListBox requires every container to have a height (note: each height can be different!). However, there are some scenarios where the Windows Phone 7 platform will report ActualHeight to be 0 for a container even though its height has been explicitly and correctly set (ex: via ItemContainerStyle). (Note: This seems to occur most often during scrolling.) Fortunately, I found an easy workaround that appears to resolve this problem in cases where the platform is misbehaving: a call to the UpdateLayout API is sufficient to correct the value of
LowProfileImageLoaderoriginally used a Queue to implement "first in, first out" (FIFO) behavior of the image downloads it performs. This is a "fair" implementation and is ideal for slowly scrolling up/down a list that uses
DeferredLoadListBoxtogether. However, for the scenario of quickly scrolling such a list in a single direction, FIFO behavior means the images you see on the screen will be among the last to load. The "obvious" fix is to switch from a
Queueto a Stack which gives "last in, first out (LIFO) behavior instead. But while that's better for the second scenario, it's worse for the first one - and it leads to a weird visual effect in apps like my
ImageLoadingsample (part of the download) because the "wall" of images loads bottom-to-top instead of "top-to-bottom" as people expect.
Clearly, there's no perfect answer here, so the solution is to do well on average! The classic way of amortizing unpredictable cost is to introduce randomness (ex: the QuickSort algorithm) - so instead of processing FIFO or LIFO,
LowProfileImageLoadernow works through its queue of pending work in random order. As a result, both the fast and the slow scrolling scenarios show images quickly and the application appears more responsive overall!
Aside: The way I've implemented randomization is a slight variation of the solution to a classic programming puzzle: How do you sort a deck of N cards in linear time and constant space? If you haven't seen this one before, take a minute to think about it before following this link to a description of the Fisher-Yates/Knuth shuffle.
Though I initially meant for
DeferredLoadListBoxto be used together, there's no reason
LowProfileImageLoadercan't be used on its own. In fact, I previously ensured that it works fine when used with the default ListBox/VirtualizingStackPanel combination. However, when the user is scrolling such a list very quickly, the default container recycling behavior means there will be multiple data bindings applied to a particular container in rapid succession. Every one of these will enqueue a request for
LowProfileImageLoaderto download the corresponding image - but only the most recent one matters. Any previous requests are "stale" and although it's safe to satisfy them, it's also unnecessary. Therefore, I've made a change with this update to detect stale requests and discard them before making an expensive web request on their behalf. This difference doesn't matter in non-virtualizing scenarios, but for virtualizing scenarios the amount of unnecessary work it saves can quickly add up!
Another consequence of using
LowProfileImageLoaderin the presence of container recycling is that re-used Image elements kept their old content until new content had been downloaded. This could lead to temporarily misleading UI where images show up alongside content they aren't associated with. It happens because
LowProfileImageLoaderdidn't previously "null-out" the Source property when a new request was made. I've modified the code so it does now - and the virtualizing experience is nicer because of it.
When implementing the worker thread logic for
LowProfileImageLoader, I intended for it to process
WorkItemQuantumnumber of items each time through the loop until the queue of requests was exhausted. I wrote the following code:
for (var i = 0; (i < pendingRequests.Count) && (i < WorkItemQuantum); i++)
I'd like to thank Ashish Gupta for pointing out a bug here; what I meant was:
for (var i = 0; (0 < pendingRequests.Count) && (i < WorkItemQuantum); i++)
Coding errors in loops can cause serious problems if they result in an attempt to process too many or too few items. I got lucky here because there's no functional bug due to the original typo - the only downside is that performance might be a little worse because it takes a couple of extra passes through the loop to complete once the count drops below
WorkItemQuantum. Fortunately, the value of
WorkItemQuantumis only 5, so the real-world impact of this is minimal. However, the whole point of this code is to help improve performance, so I've fixed the oversight.
And finally, because I recently became a NuGet publishing "expert", I've created a package for the
PhonePerformanceassembly to make it easy to reference for all the NuGet fans out there. It contains the same binary you'd download below, but it contains only the assembly (and its XML IntelliSense file) - the three sample projects are available only with the ZIP download. This split seems like a reasonable compromise to me: reference from the NuGet gallery if you know what you're doing and just need to add the binary to your project - or - read the relevant blog posts and download the samples if you're getting started.
Windows Phone 7 developers must pay attention to performance because otherwise it's easy to end up with a slow, badly-behaved application. The
PhonePerformance assembly focuses on two common scenarios (image loading and list scrolling) and attempts to improve upon the default experience by making it easy to avoid known problem areas. As with any performance technique, results can vary greatly depending on the specifics of each scenario, so it's important to take measurements and test everything on real phone hardware.
Many developers have told me they had success with the
PhonePerformance assembly - I hope you do, too!
- There's no substitute for customer feedback! [Improving Windows Phone 7 application performance now a bit easier with LowProfileImageLoader and DeferredLoadListBox updates]
- Keep a low profile [LowProfileImageLoader helps the Windows Phone 7 UI thread stay responsive by loading images in the background]
- Never do today what you can put off till tomorrow [DeferredLoadListBox (and StackPanel) help Windows Phone 7 lists scroll smoothly and consistently]
- Candy-eating lemming [Delay.Web.Helpers (and its easy-to-use Amazon S3 blob/bucket API) is now available via NuGet!]
- Each one is the best - for different definitions of "best" [The BestFitPanel collection of layout containers provides flexible, easy-to-use options for Silverlight, WPF, and Windows Phone applications]