Windows Phone: Background Agents Pitfalls (2 of n)
Published on Thursday, February 23, 2012 11:21:00 PM UTC in Programming
In the first part of this series, I mainly talked about issues with software design when you work with background agents. We saw that the partly really strict API limitations can have a pretty severe effect on how you need to structure your code to follow all requirements of the validation and certification process. This time, I want to get a bit more technical, when we learn about some other limitations of agents.
Part 2: Timeout and Memory Constraints
Once again I'm stunned about how little this particular aspect of agents is covered in most articles and tutorials, if at all. Fact is that you're facing a pretty hefty usage cap regarding memory when your code executes in the background, and timeout limits apply as well. The details of this constraint can be found in the MSDN documents here. It says:
"Periodic agents typically run for 25 seconds."
and
"Periodic agents and resource-intensive agents can use no more than 6 MB of memory at any time. [...] If a Scheduled Task exceeds this memory cap, it is terminated immediately."
Note: I'm focusing on periodic agents here, not on the special case of resource-intensive or audio agents. Everything that is said however also applies to these cases, accordingly.
So we have a maximum of 6 megabytes at our disposal. That's the equivalent of approximately 0.02 seconds of uncompressed full HD 1080p50 video :). Nah, seriously: we all know that programmers have achieved amazing things with extremely little memory consumption in the past. However, by today's standards, and using a rich-featured environment like Silverlight and the .NET Framework 6 megabytes really don't seem like a lot. This is put into perspective a bit when you think about the fact that your agent will not have any UI elements and visible content. Traditionally, pixels take quite a share of an applications memory, and since we don't have images, animations and controls in our background processing, this does not apply.
One of the fundamental problems however is that a lot of developers do not have a good sense for the amount of memory they will need for a certain operation or scenario. That's not really their fault. Ten years of managed code and the comfort of automated memory management, combined with the fact that we usually have gigabytes of memory at our hands at any time, have blunted these senses. Some of the younger devs even might never have done manual memory management in their whole lives, at all. Fortunately, we have some possibilities to help us keep track of our memory usage and execution time on the phone.
The Debug Dilemma
Before we start taking a closer look at the way to analyze the situation, I want to briefly talk about what I call the debug dilemma. The problem with debugging is that things like timeouts and other constraints get in your way when you least want it. When you step through your code to analyze it or look for issues, you don't want to be interrupted by timeout exceptions or trigger memory constraints. Of course the team at Microsoft has recognized this problem too, hence the following can be found in the above docs:
"When running under the debugger, memory and timeout restrictions are suspended."
This makes it very convenient to thoroughly test your code; the dilemma is that as soon as you are debugging, you don't get the same behavior as in the real world, and if you never test your app without a debugger attached, everything might look perfectly fine at your end. Until your app fails certification that is, or even worse, until after your users download the app and run into crashes and non-working features. So, to test things like timeouts, we need to put some more effort into our code and testing.
Tracking Memory Usage
To get information about both your application's memory usage as well as the system's limits the class DeviceStatus has been added in Windows Phone 7.1. This class has three particularly interesting properties for our purposes:
- ApplicationMemoryUsageLimit: Returns the maximum amount of memory your application process can allocate, in bytes. The actual value of this property depends on the current context it is called from (application or background agent) as well as other factors, like the amount available on the device. For background agents, this typically returns 6291456 bytes.
- ApplicationCurrentMemoryUsage: Returns the number of currently allocated bytes by your application. This value combined with the previously described limit can be used to determine what amount of memory is left for your process to allocate.
- ApplicationPeakMemoryUsage: Returns the peak amount of memory your application process has used, in bytes. This is interesting during testing and debugging, to see whether your memory consumption stayed within reasonable/allowed bounds during the whole lifetime of the process.
To simplify my testing, I'm using a class that derives from ScheduledTaskAgent and implements some convenient methods. My actual agent then in turn derives from my helper class, so I get to use these methods easily. By using the Conditional attribute, I don't have to worry too much about the methods; once compiled using the release configuration (which is required for submission), the method calls are stripped from my application by the compiler:
[Conditional("DEBUG")]
protected void DebugOutputMemoryUsage(string label = null)
{
var limit = DeviceStatus.ApplicationMemoryUsageLimit;
var current = DeviceStatus.ApplicationCurrentMemoryUsage;
var remaining = limit - current;
var peak = DeviceStatus.ApplicationPeakMemoryUsage;
var safetyMargin = limit - peak;
if (label != null)
{
Debug.WriteLine(label);
}
Debug.WriteLine("Memory limit (bytes): " + limit);
Debug.WriteLine("Current memory usage: {0} bytes ({1} bytes remaining)", current, remaining);
Debug.WriteLine("Peak memory usage: {0} bytes ({1} bytes safety margin)", peak, safetyMargin);
}
This method can now be used in my agent anywhere I want to get a snapshot of the current memory usage, for example as the first thing to do in the OnInvoke override, and again as the last thing to do just before I call the NotifyComplete base method.
protected override void OnInvoke(ScheduledTask task)
{
DebugOutputMemoryUsage("Initial Memory Snapshot:");
// perform actual agent logic
DebugOutputMemoryUsage("Final Memory Snapshot:");
NotifyComplete();
}
Now if you expect that the first snapshot will return something close to the 6 megabytes you are granted, you're wrong. For my sample application, this is the output of the first call:
Initial Memory Snapshot:
Memory limit (bytes): 6291456
Current memory usage: 3764224 bytes (2527232 bytes remaining)
Peak memory usage: 3764224 bytes (2527232 bytes safety margin)
As you can see, right at the entry point into my custom code, before I even have executed the first line of my own logic, the agent process already is consuming more than half of the 6 megabytes! The overhead of running my code, including the referenced assemblies that are already loaded into memory, ate up more than 3.5 megabytes of memory.
Then, after loading some local data from isolated storage and performing some service calls, the final snapshot looks something like this:
Final Memory Snapshot:
Memory limit (bytes): 6291456
Current memory usage: 6422528 bytes (-131072 bytes remaining)
Peak memory usage: 6422528 bytes (-131072 bytes safety margin)
Doh! I've already exceeded the allowed quota by 128 kilobytes, and there's a chance that my agent will be terminated when it's running without the debugger attached. Now you might think that I made up this sample and deliberately tried to exceed the limit. Not at all! I encourage you to run these tests on your own agents, and you will see that really not much is required to get into that kind of trouble.
Note: please do not forget to test this on a real device. In my tests, the values on real hardware always were even slightly worse than in the emulator. Testing in the emulator only is NOT sufficient.
What can you do to solve these problems? Well, the usual things when it comes to saving memory:
- Avoid loading large amounts of data from isolated storage or remote services. Try to break down the data into small chunks and only load into memory what you really need.
- Try to optimize your data format. For example, instead of loading an Xml file, a binary format might work as well. But be careful not to worsen the situation. Loading a compressed version of the file just to expand it back to its original content in memory, is not exactly helping :).
- Strip down dependencies as much as possible. We've seen in the first part of the series that often you have to restructure your code base for agents anyway. Refactor it into separate assemblies so your agent does not have to load any code or libraries into memory that it doesn't use or need.
- Watch out for typical mistakes people make in .NET/Silverlight, like not using StringBuilders when concatenating strings and similar things. Make sure the garbage collector can do its work; choose variable scopes wisely, remove references when you don't need them anymore, and mind types that implement IDisposable.
- In critical situations, you may once again have to change your software design. If you cannot reduce memory consumption to reasonable levels no matter what you try, then maybe it's time to consider removing a fancy third party component and look for something simpler, or create an own, more optimized implementation.
One important last thing is that you should always try to test your agent for the worst case (the situation where it consumes most memory), and make sure that you still have a bit of a safety margin left in these scenarios. Remember: if your agent is terminated two consecutive times, it will be unscheduled automatically. So you want to avoid running into this limit at all costs.
Keeping Track of the Time
Timeouts usually are the less critical factor for agents. You can really do a lot in 25 seconds. Still, you should keep an eye on execution time too, to be on the safe side. In terms of the base class I talked about above, this translates to some code like:
#if DEBUG
private const long MaximumMilliseconds = 25000L;
private readonly Stopwatch _stopwatch = new Stopwatch();
#endif
[Conditional("DEBUG")]
protected void DebugStartStopwatch()
{
_stopwatch.Start();
}
[Conditional("DEBUG")]
protected void DebugOutputElapsedTime(string label = null)
{
var milliSeconds = _stopwatch.ElapsedMilliseconds;
var remaining = MaximumMilliseconds - milliSeconds;
if (label != null)
{
Debug.WriteLine(label);
}
Debug.WriteLine("Running time: {0} milliseconds", milliSeconds);
Debug.WriteLine("Remaining time (max): {0} milliseconds", remaining);
}
This again allows me to take snapshots of the running time at any point in my agent, to get an idea what potentially critical places in my code are. One important place of course is the time just before you call NotifyComplete().
// output memory usage and performance
DebugOutputElapsedTime("Final Time Snapshot:");
NotifyComplete();
This may result in a sample output like:
Final Time Snapshot:
Running time: 563 milliseconds
Remaining time (max): 24437 milliseconds
There's not many recommendations to make here. The biggest dangers are:
- Features that depend on external resources, like making network calls. Once the network connection is slow or a remote resource is not available, your calls may not return for a long period of time or timeout altogether, which may become a problem.
- Excessive use of isolated storage. Especially writing to the storage is much slower than what you are used to from desktop computer hard disks or even SSDs. If you generate a lot of data that you want to write to isolated storage, then an agent probably is not the best place to do this.
The first problem is not straight-forward to tackle on the phone, because the involved networking classes do not expose simple-to-use properties to e.g. set shorter timeouts. There are some solutions available using wait events or timers to establish custom timeouts when you use classes like the WebClient; this however exceeds the scope of this post.
Conclusion
When you're working with background agents, especially the memory constraints can quickly turn into a problem that's not very easy to solve. Furthermore, due to the lifted restrictions in your development environment, it can be a challenge to even recognize that a problem exists with your implementation at all. The ways shown here allow you to keep an eye on your agents execution time and memory consumption all the time, so you can take countermeasures early and identify problems before your users (painfully) will.
Tags: Background Agents · Windows Phone 7