Skip to content

Commit 9246004

Browse files
authored
Merge pull request #11 from bookingcom/gtarasov/background_identifiers
Experiment to create view controller observers on the background
2 parents cb39caf + b7ad18a commit 9246004

16 files changed

+214
-27
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
destination: platform=iOS Simulator,name=iPhone 15 Pro,OS=17.5
6060
workspace: Project.xcworkspace
6161
run: |
62-
xcodebuild test -scheme "$scheme" -workspace "$workspace" -destination "$destination" -test-iterations 3 -run-tests-until-failure -enableCodeCoverage YES -derivedDataPath DerivedData
62+
xcodebuild test -scheme "$scheme" -workspace "$workspace" -destination "$destination" -derivedDataPath DerivedData
6363
6464
- name: Slather
6565
env:

PerformanceSuite/PerformanceApp/IssuesSimulator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
class IssuesSimulator {
1111
static func simulateNonFatalHang() {
1212
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
13-
Thread.sleep(forTimeInterval: 4.5)
13+
Thread.sleep(forTimeInterval: 6)
1414
}
1515
}
1616

PerformanceSuite/PerformanceApp/MetricsConsumer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ class MetricsConsumer: PerformanceSuiteMetricsReceiver {
7474
interop?.send(message: Message.hangStarted)
7575
}
7676

77+
var hangThreshold: TimeInterval {
78+
return 3
79+
}
80+
7781
private func log(_ message: String) {
7882
logger.info("\(message, privacy: .public)")
7983
}

PerformanceSuite/Sources/LoggingObserver.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,23 @@ final class LoggingObserver<V: ViewControllerLoggingReceiver>: ViewControllerObs
9191
// MARK: - Top screen detection
9292

9393
private func rememberOpenedScreenIfNeeded(_ viewController: UIViewController) {
94-
guard isTopScreen(viewController) else {
95-
return
94+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
95+
DispatchQueue.main.async {
96+
guard self.isTopScreen(viewController) else {
97+
return
98+
}
99+
PerformanceMonitoring.queue.async {
100+
let description = RootViewIntrospection.shared.description(viewController: viewController)
101+
AppInfoHolder.screenOpened(description)
102+
}
103+
}
104+
} else {
105+
guard isTopScreen(viewController) else {
106+
return
107+
}
108+
let description = RootViewIntrospection.shared.description(viewController: viewController)
109+
AppInfoHolder.screenOpened(description)
96110
}
97-
let description = RootViewIntrospection.shared.description(viewController: viewController)
98-
AppInfoHolder.screenOpened(description)
99111
}
100112

101113
private func isTopScreen(_ viewController: UIViewController) -> Bool {

PerformanceSuite/Sources/PerformanceMonitoring.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import UIKit
1010
protocol AppMetricsReporter: AnyObject {}
1111

1212
public struct Experiments {
13-
public init() { }
13+
public init(observersOnBackgroundQueue: Bool = false) {
14+
self.observersOnBackgroundQueue = observersOnBackgroundQueue
15+
}
16+
17+
18+
/// Experiment to try to create view controller observers on the PerformanceMonitoring.queue
19+
let observersOnBackgroundQueue: Bool
1420
}
1521

1622
public enum PerformanceMonitoring {

PerformanceSuite/Sources/ScreenMetricsReceiver.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ public protocol ScreenMetricsReceiver: AnyObject {
1414

1515
/// Method converts `UIViewController` instance to `ScreenIdentifier`. It can be enum or String, which identifies your screen.
1616
/// Return `nil` if we shouldn't track metrics for this `UIViewController`.
17+
/// This method should be as effective as possible. Slow implementation may harm app performance.
1718
///
1819
/// This method is called on the main thread only once, during `UIViewController` initialization.
20+
/// If experiment `observersOnBackgroundQueue` is turned on, this method is called on the background internal queue `PerformanceMonitoring.queue`.
21+
/// Slow implementation may harm overall performance and also can affect the precision of the measurements.
1922
///
20-
/// Default implementation will return nil for view controllers that are not from the main bundle and return UIViewController itself for others
23+
/// Default implementation will return nil for view controllers that are not from the main bundle and return `UIViewController` itself for others
2124
///
22-
/// - Parameter viewController: UIViewController which is being tracked
25+
/// - Parameter viewController: `UIViewController` which is being tracked
2326
func screenIdentifier(for viewController: UIViewController) -> ScreenIdentifier?
2427
}
2528

PerformanceSuite/Sources/ViewControllerObserver.swift

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ final class ViewControllerObserverFactory<T: ViewControllerObserver, S: ScreenMe
3434
private let observerMaker: (S.ScreenIdentifier) -> T
3535

3636
private func observer(for viewController: UIViewController) -> T? {
37-
precondition(Thread.isMainThread)
37+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
38+
dispatchPrecondition(condition: .onQueue(PerformanceMonitoring.queue))
39+
} else {
40+
precondition(Thread.isMainThread)
41+
}
3842

3943
if let observer = ViewControllerObserverFactoryHelper.existingObserver(for: viewController, identifier: T.identifier) as? T {
4044
return observer
@@ -51,27 +55,63 @@ final class ViewControllerObserverFactory<T: ViewControllerObserver, S: ScreenMe
5155
}
5256

5357
func beforeInit(viewController: UIViewController) {
54-
observer(for: viewController)?.beforeInit(viewController: viewController)
58+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
59+
PerformanceMonitoring.queue.async {
60+
self.observer(for: viewController)?.beforeInit(viewController: viewController)
61+
}
62+
} else {
63+
observer(for: viewController)?.beforeInit(viewController: viewController)
64+
}
5565
}
5666

5767
func beforeViewDidLoad(viewController: UIViewController) {
58-
observer(for: viewController)?.beforeViewDidLoad(viewController: viewController)
68+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
69+
PerformanceMonitoring.queue.async {
70+
self.observer(for: viewController)?.beforeViewDidLoad(viewController: viewController)
71+
}
72+
} else {
73+
observer(for: viewController)?.beforeViewDidLoad(viewController: viewController)
74+
}
5975
}
6076

6177
func afterViewDidAppear(viewController: UIViewController) {
62-
observer(for: viewController)?.afterViewDidAppear(viewController: viewController)
78+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
79+
PerformanceMonitoring.queue.async {
80+
self.observer(for: viewController)?.afterViewDidAppear(viewController: viewController)
81+
}
82+
} else {
83+
observer(for: viewController)?.afterViewDidAppear(viewController: viewController)
84+
}
6385
}
6486

6587
func beforeViewWillDisappear(viewController: UIViewController) {
66-
observer(for: viewController)?.beforeViewWillDisappear(viewController: viewController)
88+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
89+
PerformanceMonitoring.queue.async {
90+
self.observer(for: viewController)?.beforeViewWillDisappear(viewController: viewController)
91+
}
92+
} else {
93+
observer(for: viewController)?.beforeViewWillDisappear(viewController: viewController)
94+
}
6795
}
6896

6997
func afterViewWillAppear(viewController: UIViewController) {
70-
observer(for: viewController)?.afterViewWillAppear(viewController: viewController)
98+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
99+
PerformanceMonitoring.queue.async {
100+
self.observer(for: viewController)?.afterViewWillAppear(viewController: viewController)
101+
}
102+
} else {
103+
observer(for: viewController)?.afterViewWillAppear(viewController: viewController)
104+
}
71105
}
72106

73107
func beforeViewDidDisappear(viewController: UIViewController) {
74-
observer(for: viewController)?.beforeViewDidDisappear(viewController: viewController)
108+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
109+
PerformanceMonitoring.queue.async {
110+
self.observer(for: viewController)?.beforeViewDidDisappear(viewController: viewController)
111+
}
112+
} else {
113+
observer(for: viewController)?.beforeViewDidDisappear(viewController: viewController)
114+
}
75115
}
76116

77117
static var identifier: AnyObject {
@@ -134,13 +174,20 @@ class ViewControllerObserverCollection: ViewControllerObserver {
134174
/// Non-generic helper for generic `ViewControllerObserverFactory`. To put all the static methods and vars there.
135175
final class ViewControllerObserverFactoryHelper {
136176
static func existingObserver(for viewController: UIViewController, identifier: AnyObject) -> Any? {
137-
var vc: UIViewController? = viewController
138-
while let current = vc {
177+
if PerformanceMonitoring.experiments.observersOnBackgroundQueue {
139178
let tPointer = unsafeBitCast(identifier, to: UnsafeRawPointer.self)
140-
if let result = objc_getAssociatedObject(current, tPointer) {
179+
if let result = objc_getAssociatedObject(viewController, tPointer) {
141180
return result
142181
}
143-
vc = current.parent
182+
} else {
183+
var vc: UIViewController? = viewController
184+
while let current = vc {
185+
let tPointer = unsafeBitCast(identifier, to: UnsafeRawPointer.self)
186+
if let result = objc_getAssociatedObject(current, tPointer) {
187+
return result
188+
}
189+
vc = current.parent
190+
}
144191
}
145192

146193
return nil

PerformanceSuite/Tests/LoggingObserverTests.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ final class LoggingObserverTests: XCTestCase {
7171
MyViewController3(rootView: MyView3()), // take
7272

7373
]
74-
let observers = vcs.compactMap {
74+
_ = vcs.compactMap {
7575
if let screen = stub.screenIdentifier(for: $0) {
7676
let o = LoggingObserver(screen: screen, receiver: stub)
7777
o.afterViewDidAppear(viewController: $0)
@@ -82,6 +82,17 @@ final class LoggingObserverTests: XCTestCase {
8282

8383
}
8484

85+
let exp = expectation(description: "openedScreens")
86+
87+
DispatchQueue.global().async {
88+
while (AppInfoHolder.appRuntimeInfo.openedScreens.count < 3) {
89+
Thread.sleep(forTimeInterval: 0.001)
90+
}
91+
exp.fulfill()
92+
}
93+
94+
wait(for: [exp], timeout: 5)
95+
8596
XCTAssertEqual(AppInfoHolder.appRuntimeInfo.openedScreens, [
8697
"MyViewForLoggingObserverTests",
8798
"MyViewController1",

PerformanceSuite/Tests/PerformanceMonitoringTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ final class PerformanceMonitoringTests: XCTestCase {
1515
continueAfterFailure = false
1616
try PerformanceMonitoring.disable()
1717
StartupTimeReporter.forgetMainStartedForTests()
18+
AppInfoHolder.resetForTests()
1819
}
1920

2021
override func tearDown() {
@@ -31,6 +32,16 @@ final class PerformanceMonitoringTests: XCTestCase {
3132
let vc = UIViewController()
3233
wait(for: [exp], timeout: 20) // increase timeout as it is very slow on CI
3334
_ = vc
35+
36+
// simulate vc appearance to generate more performance events
37+
// checking there are no crashes
38+
_ = vc.view
39+
vc.beginAppearanceTransition(true, animated: false)
40+
vc.endAppearanceTransition()
41+
vc.beginAppearanceTransition(false, animated: false)
42+
vc.endAppearanceTransition()
43+
PerformanceMonitoring.queue.sync { }
44+
3445
try PerformanceMonitoring.disable()
3546

3647
let exp2 = expectation(description: "onInit2")
@@ -55,6 +66,16 @@ final class PerformanceMonitoringTests: XCTestCase {
5566
setenv("ActivePrewarm", "", 1)
5667
}
5768

69+
func testNoPrewarming() throws {
70+
setenv("ActivePrewarm", "", 1)
71+
PerformanceMonitoring.onMainStarted()
72+
try PerformanceMonitoring.enable(config: .all(receiver: self))
73+
74+
XCTAssertFalse(PerformanceMonitoring.appStartInfo.appStartedWithPrewarming)
75+
76+
try PerformanceMonitoring.disable()
77+
}
78+
5879
private var onInitExpectation: XCTestExpectation?
5980
}
6081

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"configurations" : [
3+
{
4+
"id" : "ABDBB9DB-889D-4707-B550-881A8D2FE47B",
5+
"name" : "Test Scheme Action",
6+
"options" : {
7+
8+
}
9+
}
10+
],
11+
"defaultOptions" : {
12+
"maximumTestRepetitions" : 3,
13+
"testRepetitionMode" : "untilFailure",
14+
"userAttachmentLifetime" : "keepAlways"
15+
},
16+
"testTargets" : [
17+
{
18+
"target" : {
19+
"containerPath" : "container:Pods\/Pods.xcodeproj",
20+
"identifier" : "E7B17CF293A0585BA47ADEED225E1659",
21+
"name" : "PerformanceSuite-Unit-Tests"
22+
}
23+
}
24+
],
25+
"version" : 1
26+
}

0 commit comments

Comments
 (0)