Skip to content

Commit e19dfc1

Browse files
committed
Improve performance of children traversing
1 parent 81b2474 commit e19dfc1

File tree

3 files changed

+104
-36
lines changed

3 files changed

+104
-36
lines changed

OverlayWindow/Sources/OverlayWindow/OverlayPanel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ final class OverlayPanel: NSPanel {
4949
)
5050

5151
isReleasedWhenClosed = false
52+
menu = nil
5253
isOpaque = false
5354
backgroundColor = .clear
5455
hasShadow = true

Tool/Sources/AXExtension/AXUIElement.swift

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ public extension AXUIElement {
8484
var isHidden: Bool {
8585
(try? copyValue(key: kAXHiddenAttribute)) ?? false
8686
}
87-
87+
8888
var debugDescription: String {
8989
"<\(title)> <\(description)> <\(label)> (\(role):\(roleDescription)) [\(identifier)] \(rect ?? .zero) \(children.count) children"
9090
}
91-
91+
9292
var debugEnumerateChildren: String {
9393
var result = "> " + debugDescription + "\n"
9494
result += children.map {
@@ -98,7 +98,7 @@ public extension AXUIElement {
9898
}.joined(separator: "\n")
9999
return result
100100
}
101-
101+
102102
var debugEnumerateParents: String {
103103
var chain: [String] = []
104104
chain.append("* " + debugDescription)
@@ -166,7 +166,7 @@ public extension AXUIElement {
166166
var isFullScreen: Bool {
167167
(try? copyValue(key: "AXFullScreen")) ?? false
168168
}
169-
169+
170170
var windowID: CGWindowID? {
171171
var identifier: CGWindowID = 0
172172
let error = AXUIElementGetWindow(self, &identifier)
@@ -265,7 +265,7 @@ public extension AXUIElement {
265265
fatalError("AXUIElement.children: Exceeding recommended depth.")
266266
}
267267
#endif
268-
268+
269269
var all = [AXUIElement]()
270270
for child in children {
271271
if match(child) { all.append(child) }
@@ -282,11 +282,19 @@ public extension AXUIElement {
282282
return parent.firstParent(where: match)
283283
}
284284

285-
func firstChild(depth: Int = 0, where match: (AXUIElement) -> Bool) -> AXUIElement? {
285+
func firstChild(
286+
depth: Int = 0,
287+
maxDepth: Int = 50,
288+
where match: (AXUIElement) -> Bool
289+
) -> AXUIElement? {
286290
#if DEBUG
287-
if depth >= 50 {
291+
if depth > maxDepth {
288292
fatalError("AXUIElement.firstChild: Exceeding recommended depth.")
289293
}
294+
#else
295+
if depth > maxDepth {
296+
return nil
297+
}
290298
#endif
291299
for child in children {
292300
if match(child) { return child }
@@ -313,11 +321,11 @@ public extension AXUIElement {
313321
}
314322

315323
public extension AXUIElement {
316-
enum SearchNextStep {
324+
enum SearchNextStep<Info> {
317325
case skipDescendants
318-
case skipSiblings
326+
case skipSiblings(Info)
319327
case skipDescendantsAndSiblings
320-
case continueSearching
328+
case continueSearching(Info)
321329
case stopSearching
322330
}
323331

@@ -327,34 +335,79 @@ public extension AXUIElement {
327335
/// **performance of Xcode**. Please make sure to skip as much as possible.
328336
///
329337
/// - todo: Make it not recursive.
330-
func traverse(_ handle: (_ element: AXUIElement, _ level: Int) -> SearchNextStep) {
338+
func traverse<Info>(
339+
access: (AXUIElement) -> [AXUIElement] = { $0.children },
340+
info: Info,
341+
file: StaticString = #file,
342+
line: UInt = #line,
343+
function: StaticString = #function,
344+
_ handle: (_ element: AXUIElement, _ level: Int, _ info: Info) -> SearchNextStep<Info>
345+
) {
346+
#if DEBUG
347+
var count = 0
348+
let startDate = Date()
349+
#endif
331350
func _traverse(
332351
element: AXUIElement,
333352
level: Int,
334-
handle: (AXUIElement, Int) -> SearchNextStep
335-
) -> SearchNextStep {
336-
let nextStep = handle(element, level)
353+
info: Info,
354+
handle: (AXUIElement, Int, Info) -> SearchNextStep<Info>
355+
) -> SearchNextStep<Info> {
356+
#if DEBUG
357+
count += 1
358+
#endif
359+
let nextStep = handle(element, level, info)
337360
switch nextStep {
338361
case .stopSearching: return .stopSearching
339-
case .skipDescendants: return .continueSearching
340-
case .skipDescendantsAndSiblings: return .skipSiblings
341-
case .continueSearching, .skipSiblings:
342-
for child in element.children {
343-
switch _traverse(element: child, level: level + 1, handle: handle) {
362+
case .skipDescendants: return .continueSearching(info)
363+
case .skipDescendantsAndSiblings: return .skipSiblings(info)
364+
case let .continueSearching(info), let .skipSiblings(info):
365+
loop: for child in access(element) {
366+
switch _traverse(element: child, level: level + 1, info: info, handle: handle) {
344367
case .skipSiblings, .skipDescendantsAndSiblings:
345-
break
368+
break loop
346369
case .stopSearching:
347370
return .stopSearching
348371
case .continueSearching, .skipDescendants:
349-
continue
372+
continue loop
350373
}
351374
}
352375

353376
return nextStep
354377
}
355378
}
356379

357-
_ = _traverse(element: self, level: 0, handle: handle)
380+
_ = _traverse(element: self, level: 0, info: info, handle: handle)
381+
382+
#if DEBUG
383+
let duration = Date().timeIntervalSince(startDate)
384+
.formatted(.number.precision(.fractionLength(0...4)))
385+
Logger.service.debug(
386+
"AXUIElement.traverse count: \(count), took \(duration) seconds",
387+
file: file,
388+
line: line,
389+
function: function
390+
)
391+
#endif
392+
}
393+
394+
/// Traversing the element tree.
395+
///
396+
/// - important: Traversing the element tree is resource consuming and will affect the
397+
/// **performance of Xcode**. Please make sure to skip as much as possible.
398+
///
399+
/// - todo: Make it not recursive.
400+
func traverse(
401+
access: (AXUIElement) -> [AXUIElement] = { $0.children },
402+
file: StaticString = #file,
403+
line: UInt = #line,
404+
function: StaticString = #function,
405+
_ handle: (_ element: AXUIElement, _ level: Int) -> SearchNextStep<Void>
406+
) {
407+
traverse(access: access, info: (), file: file, line: line, function: function) {
408+
element, level, _ in
409+
handle(element, level)
410+
}
358411
}
359412
}
360413

Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ extension XcodeAppInstanceInspector {
425425
allTabs.insert(element.title)
426426
return .skipDescendants
427427
}
428-
return .continueSearching
428+
return .continueSearching(())
429429
}
430430
}
431431
return allTabs
@@ -480,22 +480,39 @@ private func isCompletionPanel(_ element: AXUIElement) -> Bool {
480480
}
481481

482482
public extension AXUIElement {
483+
var editorArea: AXUIElement? {
484+
if description == "editor area" { return self }
485+
var area: AXUIElement? = nil
486+
traverse { element, level in
487+
if level > 10 {
488+
return .skipDescendants
489+
}
490+
if element.description == "editor area" {
491+
area = element
492+
return .stopSearching
493+
}
494+
if element.description == "navigator" {
495+
return .skipDescendantsAndSiblings
496+
}
497+
498+
return .continueSearching(())
499+
}
500+
return area
501+
}
502+
483503
var tabBars: [AXUIElement] {
484-
guard let editArea: AXUIElement = {
485-
if description == "editor area" { return self }
486-
return firstChild(where: { $0.description == "editor area" })
487-
}() else { return [] }
504+
guard let editorArea else { return [] }
488505

489506
var tabBars = [AXUIElement]()
490-
editArea.traverse { element, _ in
507+
editorArea.traverse { element, _ in
491508
let description = element.description
492509
if description == "Tab Bar" {
493510
element.traverse { element, _ in
494511
if element.description == "tab bar" {
495512
tabBars.append(element)
496513
return .stopSearching
497514
}
498-
return .continueSearching
515+
return .continueSearching(())
499516
}
500517

501518
return .skipDescendantsAndSiblings
@@ -521,20 +538,17 @@ public extension AXUIElement {
521538
return .skipDescendants
522539
}
523540

524-
return .continueSearching
541+
return .continueSearching(())
525542
}
526543

527544
return tabBars
528545
}
529546

530547
var debugArea: AXUIElement? {
531-
guard let editArea: AXUIElement = {
532-
if description == "editor area" { return self }
533-
return firstChild(where: { $0.description == "editor area" })
534-
}() else { return nil }
548+
guard let editorArea else { return nil }
535549

536550
var debugArea: AXUIElement?
537-
editArea.traverse { element, _ in
551+
editorArea.traverse { element, _ in
538552
let description = element.description
539553
if description == "Tab Bar" {
540554
return .skipDescendants
@@ -561,7 +575,7 @@ public extension AXUIElement {
561575
return .skipDescendants
562576
}
563577

564-
return .continueSearching
578+
return .continueSearching(())
565579
}
566580

567581
return debugArea

0 commit comments

Comments
 (0)