AutomationElement.FindAll()性能问题

时间:2015-10-06 12:41:26

标签: c# ui-automation microsoft-ui-automation

我们一直在使用MS UIA框架,并注意到在Windows 10 / .NET 4.6中查找对象集合时出现的显着减速。

在Windows 10 / .NET 4.6上测试AutomationElement.FindAll()时,查找元素集合的时间平均大约是在Windows 8.1 / .NET 4.5上查找完全相同的元素时的3到5倍.1盒子。 我的测试是针对虚拟化的WPF DataGrid(使用Recycling)并获取DataGrid每行内的所有单元格。

在我们的Win10框中,每个FindAll调用以获取每行中的单元格大约需要30到50毫秒甚至更长时间。在Win8.1盒子上大约需要5到10毫秒。我无法弄清楚原因,但我不认为问题仅限于DataGrid,因为我们的FindAll()调用没什么特别的。

        //get grid
    AutomationElement gridElement = AutomationElement.RootElement.FindFirst(TreeScope.Descendants,
           new PropertyCondition(AutomationElement.AutomationIdProperty, "dataGridAutomationId"));

    //get all visible rows
    AutomationElementCollection dataItems = gridElement.FindAll(TreeScope.Descendants,
                       new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.DataItem));

 foreach (AutomationElement dataItem in dataItems)
 {
    if (!string.IsNullOrEmpty(dataItem.Current.Name))
    {
        //call under test
        AutomationElementCollection cells = dataItem.FindAll(TreeScope.Children,
             new PropertyCondition(AutomationElement.ClassNameProperty, "DataGridCell"));
    }
 }

AutomationElement.RootElement的使用仅用于测试。 'dataItem.FindAll()'调用是我正在测试的。

赢得8.1和Win10机器规格:

  • Xeon W3670 3.20GHz cpu
  • 12gb ram
  • 64bit OS

我们尝试通过com包装使用非托管MS uia API,并且在Win10上没有看到明显的性能改进。

非常感谢任何建议。

4 个答案:

答案 0 :(得分:2)

这似乎已在最新一轮的Windows 10更新(kb/3093266)中得到修复。据一位MS支持代表说:

“UIA经常调用NtQuerySystemInformation,调用该API的性能经常不令人满意。他们对该特定代码路径进行了更改,不再调用该API,从而提高了整体性能。”

不幸的是,这是他们拥有的所有信息,所以我无法确定该电话究竟是什么造成了这个问题。

更新和测试后,两台机器的性能相同。

答案 1 :(得分:0)

一般情况下应避免使用TreeScope而非直系儿童。它不仅可以扼杀性能,而且可能只是从不结束(取决于下面的内容......)。

我建议您使用其他判别式(ControlType,Name等)优化搜索,或仅在您100%确定子树​​非常小或有限时才使用它。

顺便说一句,这通常是大多数自动记录工具无法实现UI自动化的原因,他们并不聪明,无法确定只有人类可以看到的良好AutomationProperty标准,在他们想要的背景下自动化。

答案 2 :(得分:0)

我建议尝试使用与FindAll()不同的方法,例如使用TreeWalker。 UIAutomation实现往往会在不同的OS版本之间发生变化(至少就是我的感觉)(如果要检查版本,它们会被编译为UIAutomationCore.dll)。请注意,您可以检查最新.NET框架的托管代码。 TreeWalker的示例链接:http://referencesource.microsoft.com/#UIAutomationClient/System/Windows/Automation/TreeWalker.cs

例如,要使用TreeWalker检查直接孩子,您可以使用:

var walker = TreeWalker.RawViewWalker;
var current = walker.GetFirstChild(/* a parent automation element here*/);
while (current != null)
{
  // use current (an automationelement) here, than go to next:
  current = walker.GetNextSibling(current);
}

答案 3 :(得分:0)

这可能与您的特定情况无关,但有时在托管的.NET UIA API和本机Windows UIA API之间可能存在性能特征的差异。因此,如果可行,您可能需要考虑通过使用Windows UIA API来查看是否通过与之交互的UI获得更好的性能结果。

作为测试,我刚刚创建了一个应用程序,它提供了一个DataGrid,其中包含25行,每行有10个单元格。然后,我在下面编写了UIA客户端代码,以访问通过UIA公开的每个单元的名称。 (关于我如何使用C#代码调用本机Windows UIA API的一些注释位于http://blogs.msdn.com/b/winuiautomation/archive/2015/09/30/so-how-will-you-help-people-work-with-text-part-2-the-uia-client.aspx。)

我认为测试代码真正有趣的是,一旦我拥有了DataItems的父元素,我就可以通过单个跨进程调用访问我需要的所有数据。鉴于跨进程调用很慢,我想尽可能少地进行调用。

谢谢,

IUIAutomationElement rootElement = uiAutomation.GetRootElement();

// The first few steps below find a DataGridRowsPresenter for the 
// DataGrid we're interested in.
IUIAutomationElement dataGridRowsPresenter = null;

// We'll be setting up various UIA conditions and cache requests below.
int propertyIdControlType = 30003; // UIA_ControlTypePropertyId
int propertyIdName = 30005; // UIA_NamePropertyId
int propertyIdAutomationId = 30011; // UIA_AutomationIdPropertyId
int propertyIdClassName = 30012; // UIA_ClassNamePropertyId
int controlTypeIdDataItem = 50029; // UIA_DataItemControlTypeId

// Look for the test app presenting the DataGrid. For this test, assume there's
// only one such UIA element that'll be found, and the current language doesn't
// effect any of the searches below.
string testAppName = "Window1";

IUIAutomationCondition conditionTestAppName =
    uiAutomation.CreatePropertyCondition(
        propertyIdName, testAppName);

IUIAutomationElement testAppElement =
    rootElement.FindFirst(
        TreeScope.TreeScope_Children,
        conditionTestAppName);

// Did we find the test app?
if (testAppElement != null)
{
    // Next find the DataGrid. By looking at the UI with the Inspect SDK tool first,
    // we can know exactly how the UIA hierarchy and properties are being exposed.
    string dataGridAutomationId = "DataGrid_Standard";

    IUIAutomationCondition conditionDataGridClassName =
        uiAutomation.CreatePropertyCondition(
            propertyIdAutomationId, dataGridAutomationId);

    IUIAutomationElement dataGridElement =
        testAppElement.FindFirst(
            TreeScope.TreeScope_Children,
            conditionDataGridClassName);

    // Did we find the DataGrid?
    if (dataGridElement != null)
    {
        // We could simply look for all DataItems that are descendents of the DataGrid.
        // But we know exactly where the DataItems are, so get the element that's the 
        // parent of the DataItems. This means we can then get that element's children,
        // and not ask UIA to search the whole descendent tree.
        string dataGridRowsPresenterAutomationId = "PART_RowsPresenter";

        IUIAutomationCondition conditionDataGridRowsPresenter =
            uiAutomation.CreatePropertyCondition(
                propertyIdAutomationId, dataGridRowsPresenterAutomationId);

        dataGridRowsPresenter =
            dataGridElement.FindFirst(
                TreeScope.TreeScope_Children,
                conditionDataGridRowsPresenter);
    }
}

// Ok, did we find the element that's the parent of the DataItems?
if (dataGridRowsPresenter != null)
{
    // Making cross-proc calls is slow, so try to reduce the number of cross-proc calls we 
    // make. In this test, we can find all the data we need in a single cross-proc call below.

    // Create a condition to find elements whose control type is DataItem.
    IUIAutomationCondition conditionRowsControlType =
        uiAutomation.CreatePropertyCondition(
            propertyIdControlType, controlTypeIdDataItem);

    // Now say that all elements returned from the search should have their Names and
    // ClassNames cached with them. This means that when we access the Name and ClassName
    // properties later, we won't be making any cross-proc call at that time.
    IUIAutomationCacheRequest cacheRequestDataItemName = uiAutomation.CreateCacheRequest();
    cacheRequestDataItemName.AddProperty(propertyIdName);
    cacheRequestDataItemName.AddProperty(propertyIdClassName);

    // Say that we also want data from the children of the elements found to be cached 
    // beneath the call to find the DataItem elements. This means we can access the Names 
    // and ClassNames of all the DataItems' children, without making more cross-proc calls.
    cacheRequestDataItemName.TreeScope =
        TreeScope.TreeScope_Element | TreeScope.TreeScope_Children;

    // For this test, say that we don't need a live reference to the DataItems after we've 
    // done the search. This is ok here, because the cached data is all we need. It means
    // that we can't later get current data (ie not cached) from the DataItems returned.
    cacheRequestDataItemName.AutomationElementMode =
        AutomationElementMode.AutomationElementMode_None;

    // Now get all the data we need, in a single cross-proc call.
    IUIAutomationElementArray dataItems = dataGridRowsPresenter.FindAllBuildCache(
        TreeScope.TreeScope_Children,
        conditionRowsControlType,
        cacheRequestDataItemName);

    if (dataItems != null)
    {
        // For each DataItem found...
        for (int idxDataItem = 0; idxDataItem < dataItems.Length; idxDataItem++)
        {
            IUIAutomationElement dataItem = dataItems.GetElement(idxDataItem);

            // This test is only interested in DataItems with a Name.
            string dataItemName = dataItem.CachedName;
            if (!string.IsNullOrEmpty(dataItemName))
            {
                // Get all the direct children of the DataItem, that were cached 
                // during the search.
                IUIAutomationElementArray elementArrayChildren = 
                    dataItem.GetCachedChildren();
                if (elementArrayChildren != null)
                {
                    int cChildren = elementArrayChildren.Length;

                    // For each child of the DataItem...
                    for (int idxChild = 0; idxChild < cChildren; ++idxChild)
                    {
                        IUIAutomationElement elementChild =
                            elementArrayChildren.GetElement(idxChild);
                        if (elementChild != null)
                        {
                            // This test is only interested in the cells.
                            if (elementChild.CachedClassName == "DataGridCell")
                            {
                                string cellName = elementChild.CachedName;

                                // Do something useful with the cell name now...
                            }
                        }
                    }
                }
            }
        }
    }
}