Skip to content

Commit 6d69a93

Browse files
fix: Optimize class chain lookup
1 parent e6ce542 commit 6d69a93

1 file changed

Lines changed: 56 additions & 39 deletions

File tree

WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@
1111
#import "FBClassChainQueryParser.h"
1212
#import "FBXCodeCompatibility.h"
1313
#import "FBExceptions.h"
14+
#import "XCUIElement+FBUtilities.h"
1415

1516
@implementation XCUIElement (FBClassChain)
1617

18+
// Walks a single upfront snapshot of `self` in memory to resolve every
19+
// non-indexed chain segment, instead of resolving an intermediate live
20+
// XCUIElement (and paying an accessibility round trip) just to obtain a
21+
// query root for the next segment. A live element is only ever resolved
22+
// once, for the final match(es), via a uid-predicate lookup.
1723
- (NSArray<XCUIElement *> *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
1824
{
1925
NSError *error;
@@ -23,74 +29,85 @@ @implementation XCUIElement (FBClassChain)
2329
return nil;
2430
}
2531
NSMutableArray<FBClassChainItem *> *lookupChain = parsedChain.elements.mutableCopy;
32+
NSArray<id<FBXCElementSnapshot>> *currentRoots = @[[self fb_customSnapshot]];
2633
FBClassChainItem *chainItem = lookupChain.firstObject;
27-
XCUIElement *currentRoot = self;
28-
XCUIElementQuery *query = [currentRoot fb_queryWithChainItem:chainItem query:nil];
34+
NSArray<id<FBXCElementSnapshot>> *candidates = [self.class fb_snapshotsMatchingItem:chainItem inRoots:currentRoots];
2935
[lookupChain removeObjectAtIndex:0];
3036
while (lookupChain.count > 0) {
31-
BOOL isRootChanged = NO;
3237
if (nil != chainItem.position) {
33-
// It is necessary to resolve the query if intermediate element index is not zero or one,
38+
// It is necessary to resolve the position if intermediate element index is not zero or one,
3439
// because predicates don't support search by indexes
35-
NSArray<XCUIElement *> *currentRootMatch = [self.class fb_matchingElementsWithItem:chainItem
36-
query:query
37-
shouldReturnAfterFirstMatch:nil];
40+
NSArray<id<FBXCElementSnapshot>> *currentRootMatch = [self.class fb_matchingSnapshotsWithItem:chainItem
41+
candidates:candidates
42+
shouldReturnAfterFirstMatch:nil];
3843
if (0 == currentRootMatch.count) {
3944
return @[];
4045
}
41-
currentRoot = currentRootMatch.firstObject;
42-
isRootChanged = YES;
46+
currentRoots = @[currentRootMatch.firstObject];
47+
} else {
48+
currentRoots = candidates;
4349
}
44-
chainItem = [lookupChain firstObject];
45-
query = [currentRoot fb_queryWithChainItem:chainItem query:isRootChanged ? nil : query];
50+
chainItem = lookupChain.firstObject;
51+
candidates = [self.class fb_snapshotsMatchingItem:chainItem inRoots:currentRoots];
4652
[lookupChain removeObjectAtIndex:0];
4753
}
48-
return [self.class fb_matchingElementsWithItem:chainItem
49-
query:query
50-
shouldReturnAfterFirstMatch:@(shouldReturnAfterFirstMatch)];
54+
NSArray<id<FBXCElementSnapshot>> *matchedSnapshots = [self.class fb_matchingSnapshotsWithItem:chainItem
55+
candidates:candidates
56+
shouldReturnAfterFirstMatch:@(shouldReturnAfterFirstMatch)];
57+
return [self fb_filterDescendantsWithSnapshots:matchedSnapshots onlyChildren:NO];
5158
}
5259

53-
- (XCUIElementQuery *)fb_queryWithChainItem:(FBClassChainItem *)item query:(nullable XCUIElementQuery *)query
60+
+ (NSArray<id<FBXCElementSnapshot>> *)fb_snapshotsMatchingItem:(FBClassChainItem *)item inRoots:(NSArray<id<FBXCElementSnapshot>> *)roots
5461
{
55-
if (item.isDescendant) {
56-
if (query) {
57-
query = [query descendantsMatchingType:item.type];
58-
} else {
59-
query = [self.fb_query descendantsMatchingType:item.type];
60-
}
61-
} else {
62-
if (query) {
63-
query = [query childrenMatchingType:item.type];
62+
NSMutableArray<id<FBXCElementSnapshot>> *typeMatches = [NSMutableArray array];
63+
for (id<FBXCElementSnapshot> root in roots) {
64+
if (item.isDescendant) {
65+
// descendantsByFilteringWithBlock: includes the receiver itself if it
66+
// matches the filter, unlike XCUIElementQuery's descendantsMatchingType:,
67+
// so the root has to be excluded explicitly here.
68+
[typeMatches addObjectsFromArray:[root descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot) {
69+
return snapshot != root && (item.type == XCUIElementTypeAny || snapshot.elementType == item.type);
70+
}]];
6471
} else {
65-
query = [self.fb_query childrenMatchingType:item.type];
72+
for (id<FBXCElementSnapshot> child in root.children) {
73+
if (item.type == XCUIElementTypeAny || child.elementType == item.type) {
74+
[typeMatches addObject:child];
75+
}
76+
}
6677
}
6778
}
68-
if (item.predicates) {
69-
for (FBAbstractPredicateItem *predicate in item.predicates) {
70-
if ([predicate isKindOfClass:FBSelfPredicateItem.class]) {
71-
query = [query matchingPredicate:predicate.value];
72-
} else if ([predicate isKindOfClass:FBDescendantPredicateItem.class]) {
73-
query = [query containingPredicate:predicate.value];
79+
for (FBAbstractPredicateItem *predicateItem in item.predicates) {
80+
if ([predicateItem isKindOfClass:FBSelfPredicateItem.class]) {
81+
typeMatches = [[typeMatches filteredArrayUsingPredicate:predicateItem.value] mutableCopy];
82+
} else if ([predicateItem isKindOfClass:FBDescendantPredicateItem.class]) {
83+
NSMutableArray<id<FBXCElementSnapshot>> *containingMatches = [NSMutableArray array];
84+
for (id<FBXCElementSnapshot> candidate in typeMatches) {
85+
NSArray<id<FBXCElementSnapshot>> *matchingDescendants = [candidate descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> descendant) {
86+
return descendant != candidate && [predicateItem.value evaluateWithObject:descendant];
87+
}];
88+
if (matchingDescendants.count > 0) {
89+
[containingMatches addObject:candidate];
90+
}
7491
}
92+
typeMatches = containingMatches;
7593
}
7694
}
77-
return query;
95+
return typeMatches.copy;
7896
}
7997

80-
+ (NSArray<XCUIElement *> *)fb_matchingElementsWithItem:(FBClassChainItem *)item query:(XCUIElementQuery *)query shouldReturnAfterFirstMatch:(nullable NSNumber *)shouldReturnAfterFirstMatch
98+
+ (NSArray<id<FBXCElementSnapshot>> *)fb_matchingSnapshotsWithItem:(FBClassChainItem *)item candidates:(NSArray<id<FBXCElementSnapshot>> *)candidates shouldReturnAfterFirstMatch:(nullable NSNumber *)shouldReturnAfterFirstMatch
8199
{
82100
if (1 == item.position.integerValue || (0 == item.position.integerValue && shouldReturnAfterFirstMatch.boolValue)) {
83-
XCUIElement *result = query.fb_firstMatch;
101+
id<FBXCElementSnapshot> result = candidates.firstObject;
84102
return result ? @[result] : @[];
85103
}
86-
NSArray<XCUIElement *> *allMatches = query.fb_allMatches;
87104
if (0 == item.position.integerValue) {
88-
return allMatches;
105+
return candidates;
89106
}
90-
if (allMatches.count >= (NSUInteger)ABS(item.position.integerValue)) {
107+
if (candidates.count >= (NSUInteger)ABS(item.position.integerValue)) {
91108
return item.position.integerValue > 0
92-
? @[[allMatches objectAtIndex:item.position.integerValue - 1]]
93-
: @[[allMatches objectAtIndex:allMatches.count + item.position.integerValue]];
109+
? @[[candidates objectAtIndex:item.position.integerValue - 1]]
110+
: @[[candidates objectAtIndex:candidates.count + item.position.integerValue]];
94111
}
95112
return @[];
96113
}

0 commit comments

Comments
 (0)