Keep a low profile [LowProfileImageLoader helps the Windows Phone 7 UI thread stay responsive by loading images in the background]
When writing applications for small, resource-constrained scenarios, it's not always easy to balance the demands of an attractive UI with the requirements of fast, functional design. Ideally, coding things the natural way will "just work" (the so-called pit of success) - and it usually does. But for those times when things need some extra "oomph", it's nice to have some options. That's what my latest project, PhonePerformance
is all about: giving developers performance-focused alternatives for common Windows Phone 7 scenarios.
Note: Everything in this post pertains to the latest (internal) Windows Phone 7 builds, not the public Beta bits. Although I'd expect most everything to work on the Beta build, I haven't tested it. While that might seem a bit premature, the team has committed to releasing the final development tools on September 16th, so I'm only leading the curve a little bit here.:)
In this first post, I'll be demonstrating the use of the LowProfileImageLoader
class. (A future post will explore the DeferredLoadListBox
class which is also part of the sample download - but for now I don't want to get into what that's all about.) LowProfileImageLoader
is meant to address a very specific scenario: loading lots of images from the web at the same time. This turns out to be more common than you might expect - just think about all the social media scenarios where every user has a little picture alongside their content. To help show what I'm talking about, I created the "Image Loading" sample application shown below - just run the sample, wait a little while for it to load the user list from the web (and enable the two buttons), then choose the "default" (left) or "performance" (right) scenario and watch the behavior of the little blue dots moving across the screen:
Aside: My discussion assumes all testing is done on actual phone hardware. Although running the emulator on the desktop is a fantastic development experience, performance of applications in the emulator can vary significantly. The emulator is great for writing new code and fixing bugs, but performance work should be done on a real phone if at all possible.
The ProgressBar element just below the title text has its IsIndeterminate property set to true
, so it's always animating. And as others have explained, ProgressBar animates entirely on the UI thread - so while it might not be a good choice for real applications, it makes a great test tool! In this case, I'm using it to provide an indication of how overwhelmed the UI thread is - and therefore how responsive the application will be to user input or updates. If the dots are flowing smoothly, then life is good; if the dots get jumpy or stop animating completely, then the application is unresponsive.
As you can see by trying the "default" side of the sample, populating an ItemsControl having a WrapPanel (actually, it's my BalancedWrapPanel!) ItemsPanel with a couple hundred small (48x48) images from the web (the followers of my Twitter account in case you're curious) hangs the application for a noticeable amount of time. The first part of the delay is simply the cost of applying the list to the container and the second part of the delay is the cost of downloading and processing those images (mostly on the UI thread).
In an attempt to avoid this extended period of unresponsiveness, the "performance" side of the sample uses LowProfileImageLoader
. Although the initial cost of binding the list is the same as before, using LowProfileImageLoader
allows the UI thread to become responsive much sooner. The obvious side effect is that the images load more slowly - but the fact that those ProgressBar
dots stay active the entire time demonstrates that the UI thread isn't hung like it is for the default scenario. Think about it like this: LowProfileImageLoader
trades off image load speed in favor of application responsiveness.
The way LowProfileImageLoader
works is straightforward: it creates a worker Thread and performs as much work there as possible. As soon as the SourceUri
attached property is set, that Uri is enqueued for the worker thread to process. Similarly, whenever an asynchronous response comes in, that's also queued for the worker thread to process. Meanwhile, the worker thread is looking for work on any of its (three) queues and processing it in batches for efficiency. At the same time, it's making regular calls to Thread.Sleep(1) which signals to the system that other threads with work to do should get priority. (True thread priorities would be ideal, but Thread.Priority doesn't exist in Silverlight.) Finally, when it has done as much as it can off the UI thread, the worker thread calls Dispatcher.BeginInvoke to perform the final BitmapImage operations on the UI thread where they need to take place (in batches, of course). The net effect of all of this is that all the image loading LowProfileImageLoader
performs occurs on a single worker thread which does its best to be efficient and stay out of the UI thread's way as much as possible - which results in a happier UI thread and a less frustrated user.
Of course, every application - and every scenario - is different, so there's no guarantee LowProfileImageLoader
will help all the time (or even most of the time!). But what's nice is that it's really easy to hook up, so there's nothing to lose by trying it if your scenario seems relevant!
To make things a tad more concrete, here's what a typical scenario looks like:
<Image Source="{Binding ProfileImageUrl}" Width="24" Height="24"/>
And here's all it takes to convert it to use LowProfileImageLoader
:
<Image delay:LowProfileImageLoader.UriSource="{Binding ProfileImageUrl}" Width="24" Height="24"/>
(Remember to add the appropriate XMLNS to the top of the XAML file:)
xmlns:delay="clr-namespace:Delay;assembly=PhonePerformance"
PS - As an added bonus, the first 500 callers will receive a free (single-purpose, super bare-bones) Twitter API! (NIH disclaimer: I know there are Twitter libraries out there, but I generally avoid third party code because I don't want to deal with licensing issues. Yeah, it's a little paranoid, but it helps me sleep better at night - and besides, this was really simple to dash off.) Operators are standing by...