Using a Custom AutomationPeer in Cases of UIA Related Performance Degradation

Summary

The Microsoft User Interface Automation (UIA) framework is great for helping automate testing and provide accessibility services for applications generally, but there are some cases where it may be disadvantageous for a client with very deeply nested UI components, since UIA requests can become taxing under those circumstances, degrading performance particularly.  This may even inadvertently happen to an application, which has no intentional awareness of UIA features, but which simply derives from the default features exposed within another framework, for example, the Windows Presentation Framework (WPF), which incorporates UIA features into every control by default.  In this case, an application with deeply nested UI components (such as a large TreeView, or DataGrid) might expose thousands of elements to UIA, without even being aware.  If such an application is run in an environment where another application is making frequent or numerous UIA requests, then those requests, which might normally be very quickly processed, can become burdensome.  Examples of such scenarios could be remote desktop sharing applications, narration or accessibility applications, testing frameworks, or any other application that might make UIA requests, ultimately answered from the process space of the unwary WPF developer, who may only recognize that when running in a particular scenario, his application performance begins to suffer significant degradation.

Potential alternate causes for similar but unrelated behavior

It should be noted that there are several known issues in .NET 3.5 and 4.0, wherein WPF’s implementation walks the visual tree unnecessarily in some cases, causing further performance hits.  The fix for .NET 4.0 was included with the .NET 4.5 distribution, and when 4.5 is installed on a client, the 4.0 binaries are also affected (being one in the same for both versions of the framework).  For prior releases, the fixes are available through the following KB articles:

2591373:  FIX: A .NET Framework 3-based WPF application stops responding if you right-click a control to open a pop-up control
2484841:  FIX: A .NET Framework 4.0 WPF application becomes unresponsive if used with MSAA or UIA client applications

For these scenarios, one may directly distinguish these performance issues from the ones addressed by the workaround suggested in this sample, through observing the following call stacks when inspecting the process in a debugger during its poorly performing state (or by debugging hang dumps captured of the process memory in that state).  The issues addressed in the above fixes will exhibit the following characteristic call stacks, of deeply nested calls to InvalidateAutomationAncestors():

PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf
PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf
PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf
PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf
PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf
PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf
PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf
PresentationCore_ni!MS.Internal.UIElementHelper.InvalidateAutomationAncestors(System.Windows.DependencyObject)+0xcf

A call stack more characteristic to the issue described and addressed in this post might show a deeply nested series of calls into UpdateLayout() or UpdateSubtree() functions instead, and are not due to any outstanding known issues within the WPF framework itself, but are sometimes an inevitable consequence of implementing a broad UI accessibility framework, and then running UIA clients, some of which may expose the framework to heavy load under some circumstances.  Assuming the above known issues are not involved then, the application developer caught in such a circumstance – wherein some external UIA client is unnecessarily generating excessive hits against the automation tree – may implement an easy workaround to avoid the issue. 

The workaround is that one may simply override the default AutomationPeer exposed by some top level control, under which were many other descendants generating heavy UIA activity by some UIA client or another, and for which he does not actually require exposure to that UIA client.  Instead of returning all of the UI children of the control from its AutomationPeer (as WPF does by default), one may return a null collection, and in doing so, short circuit UIA from that portion of the tree.  If automation must continue to work for some few particular node(s) from the underlying controls, we can even return just a specific node, or any other combination of nodes, with any degree of sophistication required to meet automation requirements. 

WARNING:

The developer employing this approach must be aware that he is breaking the default automation tree intentionally, an act which eliminates the performance overhead of traversing a large and complex control tree, but which also destroys the ability of automation to reach those exposed controls, unless they are explicitly exposed through his custom implementation.  

For example, narration would not work for any controls that were eliminated from the tree, nor would they be accessible to automation tests themselves, without being included.  To witness this limitation, just run the sample, and then use the Narrator in Windows, clicking on a few of the nodes in the tree to experiment.  You will find that the nodes we did not expose through our custom AutomationPeer are not narrated, while the one we did expose is still narrated.  But the top level item using the default AutomationPeer narrates any node of the tree selected from its descendants.  In many cases, this constraint is perfectly acceptable, but a developer using the approach should be aware of it before he considers a change that could potentially break his code for some users or scenarios.

With all of that explanation and forewarning out of the way, on to the fun part!

How to Implement the Workaround

We create a TreeViewItemWithCustomAutomationPeer just as we do a normal TreeViewItem (where local: corresponds to the local namespace of our project as provided in an xmlns declaration at the top of the MainWindow.xaml):

<local:TreeViewItemWithCustomAutomationPeer Header="CustomAutomationPeerItem"> ... <TreeViewItem Name="SpecialItemToBeExposed" ... ></TreeViewItem> </local:TreeViewItemWithCustomAutomationPeer Header="CustomAutomationPeerItem">

Code taken from MainWindow.xaml.

The code works by exposing a custom version of the control we want to use, but for which we will trim its children out of the UIA tree.  In my example, I use a TreeViewItem control, since this kind of control is easy to show deep nesting, but it could be anything at all:

// Our custom TreeViewItem, which implements its own custom AutomationPeer public class TreeViewItemWithCustomAutomationPeer : TreeViewItem {     // The only change we make, to return our custom AutomationPeer,      // instead of the default one that would have been returned.      protected override AutomationPeer OnCreateAutomationPeer()     {         return new CustomAutomationPeerForTreeViewItemWithCustomAutomationPeer(         // The item itself for the default TreeViewItemAutomationPeer constructor         this,         // And get some child designated as necessary for UIA, for whatever reason,          // just used as an example, so we have something to expose custom...         TreeHelper.FindChildren<TreeViewItem>(this).Where(             i => i.Name == "SpecialItemToBeExposed").First()          );     } }

Code taken from TreeViewItemWithCustomAutomationPeer.cs.

We must also implement a custom TreeViewItemAutomationPeer, which I named in the sample with the silly but verbosely accurate choice CustomAutomationPeerForTreeViewItemWithCustomAutomationPeer.
Here is that implementation:

// Our custom TreeViewItemAutomationPeer, deriving from the default one...
public class CustomAutomationPeerForTreeViewItemWithCustomAutomationPeer :
    TreeViewItemAutomationPeer
{
    public TreeViewItem SomeItemWeWantToExpose = null;
    public CustomAutomationPeerForTreeViewItemWithCustomAutomationPeer(
        TreeViewItem currentItem, TreeViewItem someItemWeWantToExpose)
        : base(currentItem)
    {
        SomeItemWeWantToExpose = someItemWeWantToExpose;
    }
    // HERE'S THE ONLY THING THAT REALLY MATTERS IN THIS SIMPLE IMPLEMENTATION!!!
    // You could just return NULL for no children, trim the whole tree out. 
    // But let's say you DO need to expose some element way down in the tree,
    // for whatever reason. You can do that too, however you want to!
    // That is what is happening with the arbitrary element I chose to include
    // for the purposes of this sample. But you could also implement
    // the same thing with multiple nodes, or even with other children,
    // having their own custom AutomationPeer implementations too. Whatever
    // your actual UIA requirements, you could still manage to achieve them, and
    // unless they actually do consist of the entirety of the app's control tree,
    // you could still improve performance by eliminating any nodes that are not
    // actually required...
    protected override List<AutomationPeer> GetChildrenCore()
    {
        List<AutomationPeer> children = new List<AutomationPeer>();
        // Some child we want to return... We just passed it into our custom class
        // during its construction and kept track of it to return as the only child.
        children.Add(
            TreeViewItemAutomationPeer.CreatePeerForElement(
                SomeItemWeWantToExpose as UIElement));
        return children;
    }
Code taken from TreeViewItemWithCustomAutomationPeer.cs.

In the sample, this implementation provides a short-circuit of an otherwise arbitrarily loaded TreeView, to demonstrate how many fewer nodes are represented, while still exposing one deeply nested node that we designated here as being relevant to UI Automation.  The sample window, when fully expanded to show all the nodes, looks like this:

Although the print is small due to the size of the window to show the deep nesting, you can see both of the top level nodes in the TreeView hold deeply nested structures of child nodes and siblings without any children of their own.

The bottommost node in the second structure is highlighted.  Its parent is the one I chose to expose arbitrarily in the custom AutomationPeer’s children.  As such, when UIA navigates this tree, it will traverse 25 nodes in the top portion of the tree, which are skipped in the lower tree of the same layout, because we have short circuited it to return only one particular node there, with no children even of its own except for its contents.  The impact can be observed using Inspect.exe, from the Windows SDK, to witness the UIA tree returned from both nodes in the TreeView now:

Note the sample does add a reference to UIAutomationProvider in its references, from the default WPF project references added by Visual Studio.  Sample code attached.

CustomAutomationPeerSample.zip