Skip to content

Commit 2a36c3f

Browse files
authored
impr(profiling): always remove launch profile config after starting (#5318)
1 parent e0424b9 commit 2a36c3f

File tree

12 files changed

+414
-88
lines changed

12 files changed

+414
-88
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### Improvements
1212

1313
- Removed `APPLICATION_EXTENSION_API_ONLY` requirement (#5524)
14+
- Improve launch profile configuration management (#5318)
1415

1516
## 8.53.1
1617

Samples/iOS-Swift/iOS-Swift-UITests/BaseUITest.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class BaseUITest: XCTestCase {
77
//swiftlint:disable implicit_getter
88
var automaticallyLaunchAndTerminateApp: Bool { get { true } }
99
//swiftlint:enable implicit_getter
10-
10+
1111
override func setUp() {
1212
super.setUp()
1313
continueAfterFailure = false
@@ -37,7 +37,7 @@ extension BaseUITest {
3737
return app
3838
}
3939

40-
func launchApp(args: [String] = [], env: [String: String] = [:]) {
40+
func launchApp(args: [String] = [], env: [String: String] = [:], activateBeforeLaunch: Bool = true) {
4141
app.launchArguments.append(contentsOf: args)
4242
for (k, v) in env {
4343
app.launchEnvironment[k] = v
@@ -46,10 +46,17 @@ extension BaseUITest {
4646
// Calling activate() and then launch() effectively launches the app twice, interfering with
4747
// local debugging. Only call activate if there isn't a debugger attached, which is a decent
4848
// proxy for whether this is running in CI.
49-
if !isDebugging() {
49+
if !isDebugging() && activateBeforeLaunch {
50+
// activate() appears to drop launch args and environment variables, so save them beforehand and reset them before subsequent calls to launch()
51+
let launchArguments = app.launchArguments
52+
let launchEnvironment = app.launchEnvironment
53+
5054
// App prewarming can sometimes cause simulators to get stuck in UI tests, activating them
5155
// before launching clears any prewarming state.
5256
app.activate()
57+
58+
app.launchArguments = launchArguments
59+
app.launchEnvironment = launchEnvironment
5360
}
5461

5562
app.launch()

Samples/iOS-Swift/iOS-Swift-UITests/ProfilingUITests.swift

Lines changed: 79 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,41 @@
1+
@testable import Sentry
12
import XCTest
23

34
//swiftlint:disable function_body_length todo
45

56
class ProfilingUITests: BaseUITest {
67
override var automaticallyLaunchAndTerminateApp: Bool { false }
7-
8+
89
func testAppLaunchesWithTraceProfiler() throws {
910
guard #available(iOS 16, *) else {
1011
throw XCTSkip("Only run for latest iOS version we test; we've had issues with prior versions in SauceLabs")
1112
}
1213

13-
// by default, launch profiling is not enabled
14-
try launchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false)
15-
16-
// after configuring for launch profiling, check the marker file exists, and that the profile happens
17-
try launchAndConfigureSubsequentLaunches(terminatePriorSession: true, shouldProfileThisLaunch: true)
14+
try performTest(profileType: .trace)
1815
}
1916

2017
func testAppLaunchesWithContinuousProfilerV1() throws {
2118
guard #available(iOS 16, *) else {
2219
throw XCTSkip("Only run for latest iOS version we test; we've had issues with prior versions in SauceLabs")
2320
}
2421

25-
// by default, launch profiling is not enabled
26-
try launchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, continuousProfiling: true)
27-
28-
// after configuring for launch profiling, check the marker file exists, and that the profile happens
29-
try launchAndConfigureSubsequentLaunches(terminatePriorSession: true, shouldProfileThisLaunch: true, continuousProfiling: true)
22+
try performTest(profileType: .continuous)
3023
}
3124

3225
func testAppLaunchesWithContinuousProfilerV2TraceLifecycle() throws {
3326
guard #available(iOS 16, *) else {
3427
throw XCTSkip("Only run for latest iOS version we test; we've had issues with prior versions in SauceLabs")
3528
}
3629

37-
// by default, launch profiling is not enabled
38-
try launchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, continuousProfiling: true, v2TraceLifecycle: true)
39-
40-
// after configuring for launch profiling, check the marker file exists, and that the profile happens
41-
try launchAndConfigureSubsequentLaunches(terminatePriorSession: true, shouldProfileThisLaunch: true, continuousProfiling: true, v2TraceLifecycle: true)
30+
try performTest(profileType: .ui, lifecycle: .trace)
4231
}
4332

4433
func testAppLaunchesWithContinuousProfilerV2ManualLifeCycle() throws {
4534
guard #available(iOS 16, *) else {
4635
throw XCTSkip("Only run for latest iOS version we test; we've had issues with prior versions in SauceLabs")
4736
}
4837

49-
// by default, launch profiling is not enabled
50-
try launchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, continuousProfiling: true, v2ManualLifecycle: true)
51-
52-
// after configuring for launch profiling, check the marker file exists, and that the profile happens
53-
try launchAndConfigureSubsequentLaunches(terminatePriorSession: true, shouldProfileThisLaunch: true, continuousProfiling: true, v2ManualLifecycle: true)
38+
try performTest(profileType: .ui, lifecycle: .manual)
5439
}
5540

5641
/**
@@ -144,72 +129,104 @@ extension ProfilingUITests {
144129
func stopContinuousProfiler() {
145130
app.buttons["io.sentry.ios-swift.ui-test.button.stop-continuous-profiler"].afterWaitingForExistence("Couldn't find button to stop continuous profiler").tap()
146131
}
147-
132+
133+
enum ProfilingType {
134+
case trace
135+
case continuous // aka "continuous beta"
136+
case ui // aka "continuous v2"
137+
}
138+
139+
func performTest(profileType: ProfilingType, lifecycle: SentryProfileOptions.SentryProfileLifecycle? = nil) throws {
140+
try launchAndConfigureSubsequentLaunches(shouldProfileThisLaunch: false, shouldProfileNextLaunch: true, profileType: profileType, lifecycle: lifecycle)
141+
try launchAndConfigureSubsequentLaunches(terminatePriorSession: true, shouldProfileThisLaunch: true, shouldProfileNextLaunch: false, profileType: profileType, lifecycle: lifecycle)
142+
}
143+
144+
fileprivate func setAppLaunchParameters(_ profileType: ProfilingUITests.ProfilingType, _ lifecycle: SentryProfileOptions.SentryProfileLifecycle?, _ shouldProfileNextLaunch: Bool) {
145+
app.launchArguments.append(contentsOf: [
146+
// these help avoid other profiles that'd be taken automatically, that interfere with the checking we do for the assertions later in the tests
147+
"--disable-swizzling",
148+
"--disable-auto-performance-tracing",
149+
"--disable-uiviewcontroller-tracing",
150+
151+
// sets a marker function to run in a load command that the launch profile should detect
152+
"--io.sentry.slow-load-method",
153+
154+
// override full chunk completion before stoppage introduced in https://github.com/getsentry/sentry-cocoa/pull/4214
155+
"--io.sentry.continuous-profiler-immediate-stop"
156+
])
157+
158+
switch profileType {
159+
case .ui:
160+
app.launchEnvironment["--io.sentry.profile-session-sample-rate"] = "1"
161+
switch lifecycle {
162+
case .none:
163+
fatalError("Misconfigured test case. Must provide a lifecycle for UI profiling.")
164+
case .trace:
165+
break
166+
case .manual:
167+
app.launchArguments.append("--io.sentry.profile-lifecycle-manual")
168+
}
169+
case .continuous:
170+
app.launchArguments.append("--io.sentry.disable-ui-profiling")
171+
case .trace:
172+
app.launchEnvironment["--io.sentry.profilesSampleRate"] = "1"
173+
}
174+
175+
if !shouldProfileNextLaunch {
176+
app.launchArguments.append("--io.sentry.disable-app-start-profiling")
177+
}
178+
}
179+
148180
/**
149181
* Performs the various operations for the launch profiler test case:
150182
* - terminates an existing app session
151-
* - creates a new one
183+
* - starts a new app session
152184
* - sets launch args and env vars to set the appropriate `SentryOption` values for the desired behavior
153-
* - launches the new configured app session
185+
* - launches the new configured app session, which will optionally start a launch profiler and then call SentrySDK.startWithOptions configured based on the launch args and env vars
154186
* - asserts the expected outcomes of the config file and launch profiler
155187
*/
156188
func launchAndConfigureSubsequentLaunches(
157189
terminatePriorSession: Bool = false,
158190
shouldProfileThisLaunch: Bool,
159-
continuousProfiling: Bool = false,
160-
v2TraceLifecycle: Bool = false,
161-
v2ManualLifecycle: Bool = false
191+
shouldProfileNextLaunch: Bool,
192+
profileType: ProfilingType,
193+
lifecycle: SentryProfileOptions.SentryProfileLifecycle?
162194
) throws {
163195
if terminatePriorSession {
164196
app.terminate()
165197
app = newAppSession()
166198
}
167199

168-
app.launchArguments.append(contentsOf: [
169-
// these help avoid other profiles that'd be taken automatically, that interfere with the checking we do for the assertions later in the tests
170-
"--disable-swizzling",
171-
"--disable-auto-performance-tracing",
172-
"--disable-uiviewcontroller-tracing",
200+
setAppLaunchParameters(profileType, lifecycle, shouldProfileNextLaunch)
173201

174-
// sets a marker function to run in a load command that the launch profile should detect
175-
"--io.sentry.slow-load-method",
202+
launchApp(activateBeforeLaunch: false)
203+
goToProfiling()
176204

177-
// override full chunk completion before stoppage introduced in https://github.com/getsentry/sentry-cocoa/pull/4214
178-
"--io.sentry.continuous-profiler-immediate-stop"
179-
])
205+
let configFileExists = try checkLaunchProfileMarkerFileExistence()
180206

181-
if continuousProfiling {
182-
if v2TraceLifecycle {
183-
app.launchEnvironment["--io.sentry.profile-session-sample-rate"] = "1"
184-
} else if v2ManualLifecycle {
185-
app.launchArguments.append(contentsOf: [
186-
"--io.sentry.profile-lifecycle-manual"
187-
])
188-
app.launchEnvironment["--io.sentry.profile-session-sample-rate"] = "1"
189-
} else {
190-
app.launchArguments.append("--io.sentry.disable-ui-profiling")
191-
}
207+
if shouldProfileNextLaunch {
208+
XCTAssert(configFileExists, "A launch profile config file should be present on disk if SentrySDK.startWithOptions configured launch profiling for the next launch.")
192209
} else {
193-
app.launchEnvironment["--io.sentry.profilesSampleRate"] = "1"
210+
XCTAssertFalse(configFileExists, "Launch profile config files should be removed upon starting launch profiles. If SentrySDK.startWithOptions doesn't reconfigure launch profiling, the config file should not be present.")
194211
}
195212

196-
launchApp()
197-
goToProfiling()
198-
XCTAssert(try checkLaunchProfileMarkerFileExistence())
199-
200213
guard shouldProfileThisLaunch else {
201214
return
202215
}
203-
204-
if continuousProfiling {
205-
if !v2TraceLifecycle {
216+
217+
if profileType == .trace {
218+
retrieveLastProfileData()
219+
} else {
220+
if profileType == .continuous || (profileType == .ui && lifecycle == .manual) {
206221
stopContinuousProfiler()
207222
}
208223
retrieveFirstProfileChunkData()
209-
} else {
210-
retrieveLastProfileData()
211224
}
212-
225+
226+
try assertProfileContents()
227+
}
228+
229+
func assertProfileContents() throws {
213230
let lastProfile = try marshalJSONDictionaryFromApp()
214231
let sampledProfile = try XCTUnwrap(lastProfile["profile"] as? [String: Any])
215232
let stacks = try XCTUnwrap(sampledProfile["stacks"] as? [[Int]])
@@ -219,7 +236,7 @@ extension ProfilingUITests {
219236
frames[stackFrame]["function"]
220237
}
221238
})
222-
239+
223240
// grab the first stack that contained frames from the fixture code that simulates a slow +[load] method
224241
var stackID: Int?
225242
let stack = try XCTUnwrap(stackFunctions.enumerated().first { nextStack in
@@ -238,12 +255,12 @@ extension ProfilingUITests {
238255
XCTFail("Didn't find the ID of the stack containing the target function")
239256
return
240257
}
241-
258+
242259
// ensure that the stack doesn't contain any calls to main functions; this ensures we actually captured pre-main stacks
243260
XCTAssertFalse(stack.contains("main"))
244261
XCTAssertFalse(stack.contains("UIApplicationMain"))
245262
XCTAssertFalse(stack.contains("-[UIApplication _run]"))
246-
263+
247264
// ensure that the stack happened on the main thread; this is a cross-check to make sure we didn't accidentally grab a stack from a different thread that wouldn't have had a call to main() anyways, thereby possibly missing the real stack that may have contained main() calls (but shouldn't for this test)
248265
let samples = try XCTUnwrap(sampledProfile["samples"] as? [[String: Any]])
249266
let sample = try XCTUnwrap(samples.first { nextSample in

Samples/iOS-Swift/iOS-Swift/AppDelegate.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
77
var window: UIWindow?
88

99
var args: [String] {
10-
let args = ProcessInfo.processInfo.arguments
11-
print("[iOS-Swift] [debug] launch arguments: \(args)")
12-
return args
10+
ProcessInfo.processInfo.arguments
11+
}
12+
13+
var env: [String: String] {
14+
ProcessInfo.processInfo.environment
1315
}
1416

1517
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18+
print("[iOS-Swift] [debug] launch arguments: \(args)")
19+
print("[iOS-Swift] [debug] launch environment: \(env)")
20+
1621
if args.contains("--io.sentry.wipe-data") {
1722
removeAppData()
1823
}

Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ class ProfilingViewController: UIViewController, UITextFieldDelegate {
7575

7676
@IBAction func defineProfilesSampleRateToggled(_ sender: UISwitch) {
7777
sampleRateField.isEnabled = sender.isOn
78-
78+
7979
var sampleRate = SentrySDKOverrides.Profiling.sampleRate
8080
sampleRate.floatValue = getSampleRateOverride(field: sampleRateField)
8181
}
8282

8383
@IBAction func defineTracesSampleRateToggled(_ sender: UISwitch) {
8484
tracesSampleRateField.isEnabled = sender.isOn
85-
85+
8686
var sampleRate = SentrySDKOverrides.Tracing.sampleRate
8787
sampleRate.floatValue = getSampleRateOverride(field: tracesSampleRateField)
8888
}
@@ -109,7 +109,16 @@ private extension ProfilingViewController {
109109
let cachesDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
110110
let fm = FileManager.default
111111
let dir = "\(cachesDirectory)/io.sentry/" + (continuous ? "continuous-profiles" : "trace-profiles")
112-
let count = try! fm.contentsOfDirectory(atPath: dir).count
112+
113+
let count: Int
114+
do {
115+
count = try fm.contentsOfDirectory(atPath: dir).count
116+
} catch {
117+
print("[iOS-Swift] [debug] [ProfilingViewController] error reading directory \(dir): \(error)")
118+
profilingUITestDataMarshalingStatus.text = "<error>"
119+
return
120+
}
121+
113122
//swiftlint:disable empty_count
114123
guard continuous || count > 0 else {
115124
//swiftlint:enable empty_count
@@ -118,7 +127,7 @@ private extension ProfilingViewController {
118127
}
119128
let fileName = "profile\(continuous ? 0 : count - 1)"
120129
let fullPath = "\(dir)/\(fileName)"
121-
130+
122131
if fm.fileExists(atPath: fullPath) {
123132
let url = NSURL.fileURL(withPath: fullPath)
124133
block(url)
@@ -129,17 +138,17 @@ private extension ProfilingViewController {
129138
}
130139
return
131140
}
132-
141+
133142
block(nil)
134143
}
135-
144+
136145
func handleContents(file: URL?) {
137146
guard let file = file else {
138147
profilingUITestDataMarshalingTextField.text = "<missing>"
139148
profilingUITestDataMarshalingStatus.text = ""
140149
return
141150
}
142-
151+
143152
do {
144153
let data = try Data(contentsOf: file)
145154
let contents = data.base64EncodedString()

0 commit comments

Comments
 (0)