AutomationElement.FindAll() 性能问题
AutomationElement.FindAll() performance issue
我们一直在使用 MS UIA 框架并注意到在 Windows 10 / .NET 4.6 中查找对象集合时速度似乎显着下降。
在 Windows 10 / .NET 4.6 盒子上测试 AutomationElement.FindAll() 时,与查找完全相同的元素相比,我们查找元素集合的平均时间大约长 3 到 5 倍在 Windows 8.1 / .NET 4.5.1 盒子上。
我的测试是针对启用了虚拟化(使用回收)并获取 DataGrid 每一行内的所有单元格的 WPF 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()' 调用是我正在测试的。
Win 8.1 和 Win10 机器规格:
- 至强 W3670 3.20GHz cpu
- 12GB 内存
- 64 位 OS
我们已经尝试通过 com 包装使用非托管 MS uia API,但在 Win10 上没有发现明显的性能改进。
如有任何建议,我们将不胜感激。
一般应避免使用除直系子项以外的 TreeScope。它不仅会降低性能,而且可能 永远不会 结束(取决于下面的内容...)。
我建议您使用其他判别式(ControlType、名称等)来优化搜索,或者仅在您 100% 确定子树确实很小或有界时才使用它。
顺便说一句,这通常是大多数自动记录器工具无法实现 UI 自动化的原因,它们不够聪明,无法在他们想要的上下文中确定只有人类才能看到的良好 AutomationProperty 标准自动化。
我建议尝试使用与 FindAll() 不同的方法,例如使用 TreeWalker。 UIAutomation 实现往往会在不同的 OS 版本之间发生变化(至少我是这么想的)(如果您想检查版本,它们会被编译为 UIAutomationCore.dll)。请注意,您可以检查最新的 .NET 框架的托管代码。 TreeWalker 示例 link: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);
}
这可能与您的具体情况无关,但有时托管 .NET UIA API 和本机 Windows [之间的性能特征可能存在差异=29=]一个API。因此,如果可行,您可能需要考虑通过使用 Windows UIA API 来查看您正在与之交互的 UI 是否会获得更好的性能结果。
作为测试,我刚刚创建了一个应用程序,其中显示了一个包含 25 行且每行 10 个单元格的 DataGrid。然后我在下面编写了 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...
}
}
}
}
}
}
}
}
这似乎已在最新一轮 Windows 10 次更新 (kb/3093266) 中得到修复。据我采访过的 MS 支持代表说:
"UIA was frequently calling the NtQuerySystemInformation, the performance of calling that API frequently is not satisfactory. They made changes to that particular code path and do not call that API anymore and that improved the overall performance."
不幸的是,这就是他们所掌握的所有信息,所以我无法确定到底是那个电话导致了问题。
更新和测试后,两台机器的性能相同。
我们一直在使用 MS UIA 框架并注意到在 Windows 10 / .NET 4.6 中查找对象集合时速度似乎显着下降。
在 Windows 10 / .NET 4.6 盒子上测试 AutomationElement.FindAll() 时,与查找完全相同的元素相比,我们查找元素集合的平均时间大约长 3 到 5 倍在 Windows 8.1 / .NET 4.5.1 盒子上。 我的测试是针对启用了虚拟化(使用回收)并获取 DataGrid 每一行内的所有单元格的 WPF 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()' 调用是我正在测试的。
Win 8.1 和 Win10 机器规格:
- 至强 W3670 3.20GHz cpu
- 12GB 内存
- 64 位 OS
我们已经尝试通过 com 包装使用非托管 MS uia API,但在 Win10 上没有发现明显的性能改进。
如有任何建议,我们将不胜感激。
一般应避免使用除直系子项以外的 TreeScope。它不仅会降低性能,而且可能 永远不会 结束(取决于下面的内容...)。
我建议您使用其他判别式(ControlType、名称等)来优化搜索,或者仅在您 100% 确定子树确实很小或有界时才使用它。
顺便说一句,这通常是大多数自动记录器工具无法实现 UI 自动化的原因,它们不够聪明,无法在他们想要的上下文中确定只有人类才能看到的良好 AutomationProperty 标准自动化。
我建议尝试使用与 FindAll() 不同的方法,例如使用 TreeWalker。 UIAutomation 实现往往会在不同的 OS 版本之间发生变化(至少我是这么想的)(如果您想检查版本,它们会被编译为 UIAutomationCore.dll)。请注意,您可以检查最新的 .NET 框架的托管代码。 TreeWalker 示例 link: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);
}
这可能与您的具体情况无关,但有时托管 .NET UIA API 和本机 Windows [之间的性能特征可能存在差异=29=]一个API。因此,如果可行,您可能需要考虑通过使用 Windows UIA API 来查看您正在与之交互的 UI 是否会获得更好的性能结果。
作为测试,我刚刚创建了一个应用程序,其中显示了一个包含 25 行且每行 10 个单元格的 DataGrid。然后我在下面编写了 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...
}
}
}
}
}
}
}
}
这似乎已在最新一轮 Windows 10 次更新 (kb/3093266) 中得到修复。据我采访过的 MS 支持代表说:
"UIA was frequently calling the NtQuerySystemInformation, the performance of calling that API frequently is not satisfactory. They made changes to that particular code path and do not call that API anymore and that improved the overall performance."
不幸的是,这就是他们所掌握的所有信息,所以我无法确定到底是那个电话导致了问题。
更新和测试后,两台机器的性能相同。