Delay's Blog is the blog of David Anson, a Microsoft developer who works with C#, XAML, HTML, and Azure.
A few months ago I began a similar post about LowProfileImageLoader/DeferredLoadListBox updates by saying:
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 LowProfileImageLoader and 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 DeferredLoadListBox because 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 TransformToVisual method 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.
I'd like to thank Tore Lervik, Baldelli Gabriele, and Holger Schmeken for reporting this problem.
Aside: Another time I had to remove TransformToVisual was for the Silverlight for Windows Phone Toolkit's ContextMenu control. (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 ActualHeight.
I'd like to thank Rich Griffin and Michael James for reporting this problem.
LowProfileImageLoader originally 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 LowProfileImageLoader and DeferredLoadListBox together. 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 Queue to 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 ImageLoading sample (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, LowProfileImageLoader now 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 LowProfileImageLoader and DeferredLoadListBox to be used together, there's no reason LowProfileImageLoader can'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 LowProfileImageLoader to 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 LowProfileImageLoader in 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 LowProfileImageLoader didn'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 WorkItemQuantum number 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 WorkItemQuantum is 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 PhonePerformance assembly 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.
[Click here to download the compiled PhonePerformance assembly, sample applications, and full source code for everything.]
[Click here to visit the NuGet gallery page for a package containing the PhonePerformance assembly.]
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! :)