Diagnosing Memory Leaks in Managed Windows Store Apps
Diagnosing Memory Leaks in Managed Windows Store Apps
Join the DZone community and get the full member experience.Join For Free
There is so much material on the web (and even on this blog) about memory leak diagnostics in managed code, and a considerable number of tools that make diagnostics increasingly easier. Modern memory profilers can open application dumps, attach to live processes, display live memory graphs, compare snapshots, identify problematic retention patterns, and so much more.
Unfortunately, these tools presently don’t work with Windows Store apps. Moreover, the UI model of Windows Store apps poses a significant challenge in diagnosing many UI-related memory leaks, such as composite UI controls retaining objects embedded in them, or UI elements retaining objects registered to their events.
Namely, the Windows Store app UI framework is implemented in unmanaged code and exposed to C# apps through fairly standard COM interop (with some minor tweaks). Because the garbage collector has no insight into or control over unmanaged COM objects in the Windows Runtime, tracing a memory leak to its true source becomes exceptionally difficult. Needless to say, the lack of profiler support does not help – you need to pull the trusty WinDbg and SOS combination for even the simplest of problems.
To begin with, you can’t reliably keep WinDbg attached to a Windows Store app. It seems that Windows detects that the app is not running, and eventually terminates it under the nose of the debugger. This isn’t that big a deal – you have to resort to dump files instead. (On Windows 8, Task Manager can capture dumps for you; just make sure you capture dumps of 32-bit apps using the 32-bit Task Manager, and vice versa.)
Consider an app that consumes a large amount of memory because it has a ListView with many large objects retained by it. The standard leak diagnostic commands produce the following:
0:006> !dumpheap -stat
…edited for brevity…
3077ce04 14 224 Windows.UI.Xaml.Controls.ListBoxItem
3077ee2c 15 240 Windows.UI.Xaml.Data.Binding
005e5f00 14 392 MemoryLeakingStoreApp.BigObject
5b6d7940 7 420 System.Reflection.RuntimeMethodInfo
5b6d7180 5 420 System.RuntimeType+RuntimeTypeCache
5b6a1e64 18 432 System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeMarshal+NativeOrStaticEventRegistrationImpl+EventRegistrationTokenListWithCount
309d0af4 14 448 Windows.UI.Xaml.Controls.SelectionChangedEventHandler
5ab845ec 17 476 System.UriParser+BuiltInUriParser
5b6a1094 5 600 System.Runtime.CompilerServices.ConditionalWeakTable`2+Entry[[System.Object, mscorlib],[System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeMarshal+NativeOrStaticEventRegistrationImpl+EventRegistrationTokenListWithCount, mscorlib]]
5b6dd690 2 616 System.Globalization.CultureData
5ab845a4 2 952 System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[System.UriParser, System]]
5b6dc738 15 988 System.Int32
5b6a0d40 94 1504 System.Runtime.InteropServices.WindowsRuntime.ICustomPropertyProviderProxy`2[[System.Object, mscorlib],[System.Object, mscorlib]]
5b6dc168 55 1540 System.RuntimeType
5b6dafb0 205 9996 System.String
5b68ae88 30 18076 System.Object
008a0dd8 125 32450 Free
5b6dd244 29 21000348 System.Byte
OK, so at this point we know that we have a small number of byte arrays that consume a bunch of memory. A typical root chain for the larger ones looks like this:
0:006> !gcroot 04393620
00541478 (ref counted handle)
-> 025471e8 System.Runtime.InteropServices.WindowsRuntime.ICustomPropertyProviderProxy`2[[System.Object, mscorlib],[System.Object, mscorlib]]
-> 02546bf8 Windows.UI.Xaml.Controls.ItemCollection
-> 0254b734 MemoryLeakingStoreApp.BigObject (dependent handle)
-> 04393620 System.Byte
That is to say, the byte arrays are retained by BigObject instances, which are held by a XAML ItemsCollection. But which objects holds this collection? It remains a mystery behind the ICustomPropertyProviderProxy generic interop interface… Now surely, ItemsCollection points in the general direction of the user interface, and hints pretty strongly that some kind of items control is involved. But there is no way to determine which – the mysterious answer is behind the Great Wall of Interop.
Similarly, suppose you have a bunch of objects registered for an event of a UI control, such as ListView.SelectionChanged. Here’s what the reference chain will look like in the debugger:
0:006> !gcroot 0c0935d0
005418f8 (ref counted handle)
-> 0254d22c Windows.UI.Xaml.Controls.SelectionChangedEventHandler
-> 0254d220 MemoryLeakingStoreApp.MyHandler
-> 0c0935d0 System.Byte
As before, the useful part terminates at the SelectionChangedEventHandler object. It is retained by a CCW (COM-callable wrapper), and you don’t know how to proceed behind the Great Wall. Yes, you have the hint of “SelectionChangedEventHandler”, but if you have many UI controls on the page, or pages that your app may have retained from past navigations, this still doesn’t help much.
Interestingly, you can always discern between an event handler (or any CCW reference in fact) retained by the XAML framework (“Jupiter”) as opposed to a standard CCW reference from COM or a non-UI Windows Runtime component. To do so, find the CCW address for your managed object and run !dumpccw. For example:
0:000> !do -nofields 02028c28
Size: 28(0x1c) bytes
0:000> !dumpccw 0065f240
Managed object: 02028c28
Outer IUnknown: 00000000
Ref count: 0
Jupiter ref count: 8, Pegged by Jupiter & CLR
RefCounted Handle: 002d17f8 (STRONG)
COM interface pointers:
IP MT Type
To conclude, memory leak diagnostics have just become much more interesting (read: more difficult) with Windows Store apps, especially when any Windows Runtime objects are involved. Because your C# code is now playing in a largely-unmanaged field, with COM objects retaining .NET objects, which in turn retain other COM objects, there’s plenty of new types of leaks and nasty interop scenarios to be worried about.
There hasn’t ever been a better time to take an “Introduction to COM” course since the late ‘90s :-)
Published at DZone with permission of Sasha Goldshtein , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.