Quantcast
Channel: Windows Desktop Development for Accessibility and Automation forum
Viewing all articles
Browse latest Browse all 585

[Advanced Topic] Understanding UI Automation Navigation and Performance

$
0
0
While there are several interesting threads out there on the various problem's people have had in developing automation, there aren't many good resources for understanding how to make UI Automation fast.

UI Automation should never take longer to perform any UI interaction then a human user.  If this is ever the case, it is more then likely a case of your implementation and this thread should help.

Performance with navigation is particularly painful with .Net controls over their win32 equivalents due to the inherent weight of .Net which makes doing things efficiently incredibly important.

What follows are some lessons we learned developing our internal UI Automation testing framework based on .Net 3.0 over the last year and a half.  I invite anyone with other perspectives to form a discussion in this thread and Microsoft to correct us all if we misrepresent something.

-----------------------------------------------------------

You can't build fast automation without optimizing the most fundamental aspect of UI Automation - navigation.  Navigation is the straight forward need to "find" something based on criteria and a starting point.

Microsoft provides a basic find tool in the form of FindFirst off of the base AutomationElement class.  It allows the user to specify the search criteria and the scope of the search.  Scope allows automation to determine where to look when trying to find your element and the most common ones needed are Element, Children, Descendants, and SubTree.  Element is just your current element, Children are just your immediate child elements, Descendants are any nodes which are below the current element in the UI tree, and SubTree is the current element plus all of the descendants.

The number one inefficiency I have seen people do in writing UI Automation code is the abuse of the scope field.  To understand why, you first need to consider what Microsoft is doing when you specify a scope.

Try a simple experiment on a .Net form.  Using a known element in the middle of a complex tree (lots of elements above and below), perform a search using FindFirst of an immediate child using the scopes of Children and Descendants.  The result is that the search with a scope of Children isn't just faster, it is ridiculously faster depending on how many descendants the node you are testing on has.

The reason is not that Microsoft wrote a terrible search tool, it is that Microsoft is assuming you know what you are talking about when you specify scope.  Microsoft generates a cache of the entire scope of the search before applying the criteria.   The reason for the cache is that the round trip to Win32 is expensive and the search is expensive, so by doing everything upfront they are optimizing the marginal per node cost in the search domain.  This becomes particularly important because Microsoft can't assume that the element you are looking for exists and thus has to optimize both cases in a balanced way.

So what does all that mean?  Well, use the smallest scope you possibly can.  If you need to drill down multiple levels using Microsoft FindFirst, it is faster to find each level and perform the search piecemeal.

-----------------------------------------------------------

In addition to FindFirst, Microsoft also provides a TreeWalker class for basic navigation the UI tree.  TreeWalker essentially allows you to navigate from any node to its immediate parent, child, or siblings.  By leveraging this tool, it is possible to build a vastly improved Find library, including support for Regular Expressions against AutomationIds, Names, etc.

The simple use of recursion and a copy of the Depth First Search algorithm would get you a basic Find function, but unless you consider the performance reasons why Microsoft's FindFirst wasn't fast enough, the new tool will be at least as inefficient since you don't have access to the optimizations under the hood Microsoft does when they perform their searching.

The golden rule when using TreeWalker is to minimize the number of times it is used to achieve a desired result.  Work smarter, not harder.

FindFirst is a great starting point for developing automation, but to truly write fast automation you will require something more.  The problem isn't with the way FindFirst was written but with the assumptions it makes aren't necessarily the assumption you want your Find Algorithm to make.

The alternative I suggest using is one which takes advantage of more contextual information when searching then simple scope.

1) As the author of Automation which needs to find a control, you are in the perfect position to assert whether the control you are looking for should exist or not.  In most cases, if you are looking for something you are probably doing so because you want to interact with it.  If you can assume that the common case is that your element should exist and you develop your find tool to use short circuit logic to return as soon as you find your element, then you are already theoretically twice as fast as Microsoft since you will only need to access statistically 1/2 of the nodes in the scope instead of all of them.  (In practice you'll see a performance improvement but only to a lesser degree because the cost of you searching every node using TreeWalker is not equivalent to the cost of Microsoft searching every node due to under the hood optimizations.)

2) Windows forms are collections of organization and interactive controls.  Organization controls are found in the form of Windows and Panes while interactive controls are the Edit Controls, Combo Boxes, Buttons, etc.  95% of the controls you will ever need find are on a window and the immediate child of either that window or a descendant pane.  A simple optimization then would to narrow the scope to just those elements which are the immediate descendants of either windows or panes.  This eliminates the need to uselessly walk into list item groups, grids, tree views, etc which while interesting - should be approached as a two step find for the sake of performance.  Find the control of interest, then manipulate that control or its children as appropriate.

3) When automating a page, reconsider not just how to Find a single element but how to perform the collective Find actions required on a specific window.  The common case is not that only one control is desired.  Consider the case you have a data form with 20 fields and need to set the values for all 20.

The least efficient way to approach this problem is iteratively by "finding a field, setting a field, and repeated."  The reason this approach is bad is that you are asking the Framework to repeatedly load the same controls as the find algorithm explores the tree.

A slightly better approach, but also inefficient would be to perform a FindAll of the scope (such as Descendants) to return a collection of all AutomationElements which would then be searched for the desired elements.  The benefit of the approach is the simplicity of reducing the number of loads required but it still requires us to navigate more of the tree then absolutely necessary.

The optimum approach would be to accept a list of criteria, and as you navigate the tree you set aside those objects so that once you've found all of the elements on the list you can return - eliminating the need to visit all of the nodes in the scope.  It is important that when you build your list of automation elements in the recursive structure that you don't constantly create new list objects and combine them.  By simply passing the list by reference between levels of recursion, you eliminate a great deal of extra work for .Net.

An everyday example would be to ask is it faster to go to the grocery store every time you need something, go to the store and bring the entire contents of the store home, or build a list ahead of time and just bring those items you think you'll need.  If you're wrong, you can always go back - but you'll have saved yourself all those trips you were able to consolidate.  (If you really get into the act you can accept the new values for the fields to go along with those criteria so automation can set all of the fields as it walks the tree without ever needing to even return AutomationElements to the caller.)

4) When you build your own Find function you have the luxury of determining how criteria are specified.  If you want to use the Microsoft Condition objects, simply doing myElement.FindFirst(Element, myCondition) would be sufficient for comparing if the condition matches the current element.  This is the point were you can decide that regular expressions are important when searching for an element by name of automation id and add the support to do so.  (The lack of Regex support can be really annoying for dynamic automation ids).

One approach could be to have a function which takes an Interface requiring the function IsMatch, and then any class which implements that interface would be compatible with your Find algorithm.  There are a lot of different choices.

-----------------------------------------------------------

Those are the lessons we learned when working to optimize navigation code in UI Automation.  I'm not adding code because I don't want people to skim, copy, and paste and hope magically that performance will be improved.  True performance comes from a greater understanding of how things work and how to leverage tools for the specific projects you are faced with.

I'm also curious how everyone is working to address the performance issues of UI Automation - and if there is the interest out there we can definitely create additional threads to talk about how to optimize other processes.

Viewing all articles
Browse latest Browse all 585

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>