Skip to content

Commit 20b6092

Browse files
committed
Fix that searching for tabs in Xcode cause Xcode to have performance issue
1 parent 8abc25e commit 20b6092

2 files changed

Lines changed: 118 additions & 6 deletions

File tree

Tool/Sources/AXExtension/AXUIElement.swift

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public extension AXUIElement {
2121
var value: String {
2222
(try? copyValue(key: kAXValueAttribute)) ?? ""
2323
}
24-
24+
2525
var intValue: Int? {
2626
(try? copyValue(key: kAXValueAttribute))
2727
}
@@ -134,7 +134,7 @@ public extension AXUIElement {
134134
var isFullScreen: Bool {
135135
(try? copyValue(key: "AXFullScreen")) ?? false
136136
}
137-
137+
138138
var isFrontmost: Bool {
139139
(try? copyValue(key: kAXFrontmostAttribute)) ?? false
140140
}
@@ -191,6 +191,16 @@ public extension AXUIElement {
191191
return nil
192192
}
193193

194+
/// Get children that match the requirement
195+
///
196+
/// - important: If the element has a lot of descendant nodes, it will heavily affect the
197+
/// **performance of Xcode**. Please make use ``AXUIElement\traverse(_:)`` instead.
198+
@available(
199+
*,
200+
deprecated,
201+
renamed: "traverse(_:)",
202+
message: "Please make use ``AXUIElement\traverse(_:)`` instead."
203+
)
194204
func children(where match: (AXUIElement) -> Bool) -> [AXUIElement] {
195205
var all = [AXUIElement]()
196206
for child in children {
@@ -233,6 +243,52 @@ public extension AXUIElement {
233243
}
234244
}
235245

246+
public extension AXUIElement {
247+
enum SearchNextStep {
248+
case skipDescendants
249+
case skipSiblings
250+
case skipDescendantsAndSiblings
251+
case continueSearching
252+
case stopSearching
253+
}
254+
255+
/// Traversing the element tree.
256+
///
257+
/// - important: Traversing the element tree is resource consuming and will affect the
258+
/// **performance of Xcode**. Please make sure to skip as much as possible.
259+
///
260+
/// - todo: Make it not recursive.
261+
func traverse(_ handle: (_ element: AXUIElement, _ level: Int) -> SearchNextStep) {
262+
func _traverse(
263+
element: AXUIElement,
264+
level: Int,
265+
handle: (AXUIElement, Int) -> SearchNextStep
266+
) -> SearchNextStep {
267+
let nextStep = handle(element, level)
268+
switch nextStep {
269+
case .stopSearching: return .stopSearching
270+
case .skipDescendants: return .continueSearching
271+
case .skipDescendantsAndSiblings: return .skipSiblings
272+
case .continueSearching, .skipSiblings:
273+
for child in element.children {
274+
switch _traverse(element: child, level: level + 1, handle: handle) {
275+
case .skipSiblings, .skipDescendantsAndSiblings:
276+
break
277+
case .stopSearching:
278+
return .stopSearching
279+
case .continueSearching, .skipDescendants:
280+
continue
281+
}
282+
}
283+
284+
return nextStep
285+
}
286+
}
287+
288+
_ = _traverse(element: self, level: 0, handle: handle)
289+
}
290+
}
291+
236292
// MARK: - Helper
237293

238294
public extension AXUIElement {

Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,16 +353,21 @@ extension XcodeAppInstanceInspector {
353353

354354
for window in windows {
355355
let workspaceIdentifier = workspaceIdentifier(window)
356+
var traverseCount = 0
356357

357358
let tabs = {
358359
guard let editArea = window.firstChild(where: { $0.description == "editor area" })
359360
else { return Set<String>() }
360361
var allTabs = Set<String>()
361-
let tabBars = editArea.children { $0.description == "tab bar" }
362+
let tabBars = editArea.tabBars
362363
for tabBar in tabBars {
363-
let tabs = tabBar.children { $0.roleDescription == "tab" }
364-
for tab in tabs {
365-
allTabs.insert(tab.title)
364+
tabBar.traverse { element, _ in
365+
traverseCount += 1
366+
if element.roleDescription == "tab" {
367+
allTabs.insert(element.title)
368+
return .skipDescendants
369+
}
370+
return .continueSearching
366371
}
367372
}
368373
return allTabs
@@ -416,3 +421,54 @@ private func isCompletionPanel(_ element: AXUIElement) -> Bool {
416421
return matchXcode16CompletionPanel
417422
}
418423

424+
public extension AXUIElement {
425+
var tabBars: [AXUIElement] {
426+
// Searching by traversing with AXUIElement is (Xcode) resource consuming, we should skip
427+
// as much as possible!
428+
429+
guard let editArea: AXUIElement = {
430+
if description == "editor area" { return self }
431+
return firstChild(where: { $0.description == "editor area" })
432+
}() else { return [] }
433+
434+
var tabBars = [AXUIElement]()
435+
editArea.traverse { element, _ in
436+
let description = element.description
437+
if description == "Tab Bar" {
438+
element.traverse { element, _ in
439+
if element.description == "tab bar" {
440+
tabBars.append(element)
441+
return .stopSearching
442+
}
443+
return .continueSearching
444+
}
445+
446+
return .skipDescendantsAndSiblings
447+
}
448+
449+
if element.identifier == "editor context" {
450+
return .skipDescendantsAndSiblings
451+
}
452+
453+
if element.isSourceEditor {
454+
return .skipDescendantsAndSiblings
455+
}
456+
457+
if description == "Code Coverage Ribbon" {
458+
return .skipDescendants
459+
}
460+
461+
if description == "Debug Area" {
462+
return .skipDescendants
463+
}
464+
465+
if description == "debug bar" {
466+
return .skipDescendants
467+
}
468+
469+
return .continueSearching
470+
}
471+
472+
return tabBars
473+
}
474+
}

0 commit comments

Comments
 (0)