Skip to content

Commit ad1f6e6

Browse files
authored
Update OOM handling (#163)
* Offload all BacktraceOomWatcher I/O to dedicated serial DispatchQueue and improve oom handling Perform BacktraceOomWatcher operations on DispatchQueue Add `flushQueue()` internal helper for unit tests Add early‑return in `start()` when oomMode == .none Add and persist resident memory footprint attribute at warning time * Add BacktraceOomMode reporting strategy Introduce @objc enum BacktraceOomMode [ .none, .light, .full ] Add property `oomMode` to BacktraceClientConfiguration Deprecate legacy `detectOom` Bool with @available(renamed) Provide deprecated convenience init that maps detectOOM to oomMode * Adapt tests to async design Call `flushQueue()` after each async method before asserting state Add new context for .none and .light oom modes Ensure no state file is written in .none mode
1 parent 1834a6e commit ad1f6e6

File tree

9 files changed

+638
-467
lines changed

9 files changed

+638
-467
lines changed

Backtrace.xcodeproj/project.pbxproj

Lines changed: 257 additions & 257 deletions
Large diffs are not rendered by default.

Examples/Example-iOS/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
3030
dbSettings: backtraceDatabaseSettings,
3131
reportsPerMin: 10,
3232
allowsAttachingDebugger: true,
33-
detectOOM: true)
33+
oomMode: .full)
3434

3535
// Customize PLCrashReporterConfig with custom basePath https://docs.saucelabs.com/error-reporting/platform-integrations/ios/configuration/#plcrashreporter
3636
guard let plcrashReporterConfig = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all, basePath: crashDirectory.path) else {

Sources/Features/Client/BacktraceOomWatcher.swift

Lines changed: 233 additions & 184 deletions
Large diffs are not rendered by default.

Sources/Features/Client/BacktraceReporter.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@ final class BacktraceReporter {
1313
private let watcher: BacktraceWatcher<PersistentRepository<BacktraceReport>>
1414
private(set) var attributesProvider: SignalContext
1515
private(set) var backtraceOomWatcher: BacktraceOomWatcher
16+
private let oomMode: BacktraceOomMode
1617
let repository: PersistentRepository<BacktraceReport>
1718

1819
init(reporter: CrashReporting,
1920
api: BacktraceApi,
2021
dbSettings: BacktraceDatabaseSettings,
2122
credentials: BacktraceCredentials,
23+
oomMode: BacktraceOomMode,
2224
urlSession: URLSession = URLSession(configuration: .ephemeral)) throws {
2325
self.reporter = reporter
2426
self.api = api
27+
self.oomMode = oomMode
2528
self.watcher =
2629
BacktraceWatcher(settings: dbSettings,
2730
networkClient: BacktraceNetworkClient(urlSession: urlSession),
@@ -34,7 +37,8 @@ final class BacktraceReporter {
3437
repository: self.repository,
3538
crashReporter: self.reporter,
3639
attributes: attributesProvider,
37-
backtraceApi: self.api)
40+
backtraceApi: self.api,
41+
oomMode: oomMode)
3842
self.reporter.signalContext(&self.attributesProvider)
3943
}
4044
}
@@ -140,6 +144,8 @@ typealias Application = NSApplication
140144
extension BacktraceReporter {
141145

142146
internal func enableOomWatcher() {
147+
guard oomMode != .none else { return }
148+
143149
self.backtraceOomWatcher.start()
144150

145151
NotificationCenter.default.addObserver(self,

Sources/Public/BacktraceClient.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import Foundation
5353
let api = BacktraceApi(credentials: configuration.credentials,
5454
reportsPerMin: configuration.reportsPerMin)
5555
let reporter = try BacktraceReporter(reporter: BacktraceCrashReporter(), api: api, dbSettings: configuration.dbSettings,
56-
credentials: configuration.credentials)
56+
credentials: configuration.credentials, oomMode: configuration.oomMode)
5757
try self.init(configuration: configuration, debugger: DebuggerChecker.self, reporter: reporter,
5858
dispatcher: Dispatcher(), api: api)
5959
}
@@ -68,7 +68,7 @@ import Foundation
6868
let api = BacktraceApi(credentials: configuration.credentials,
6969
reportsPerMin: configuration.reportsPerMin)
7070
let reporter = try BacktraceReporter(reporter: crashReporter, api: api, dbSettings: configuration.dbSettings,
71-
credentials: configuration.credentials)
71+
credentials: configuration.credentials, oomMode: configuration.oomMode)
7272

7373
try self.init(configuration: configuration, debugger: DebuggerChecker.self, reporter: reporter,
7474
dispatcher: Dispatcher(), api: api)
@@ -175,6 +175,7 @@ extension BacktraceClient: BacktraceReporting {
175175
}
176176

177177
try reporter.enableCrashReporter()
178+
178179
dispatcher.dispatch({ [weak self] in
179180
guard let self = self else { return }
180181
do {
@@ -186,7 +187,7 @@ extension BacktraceClient: BacktraceReporting {
186187
BacktraceLogger.debug("Started error reporter.")
187188
})
188189

189-
if self.configuration.detectOom {
190+
if self.configuration.oomMode != .none {
190191
dispatcher.dispatch({ [weak self] in
191192
guard let self = self else { return }
192193
self.reporter.enableOomWatcher()

Sources/Public/BacktraceClientConfiguration.swift

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import Foundation
22

3+
/// Determines how the SDK should handle OOM (Out‑Of‑Memory) events.
4+
@objc public enum BacktraceOomMode: Int {
5+
/// Disable OOM tracking (identical to legacy `detectOOM = false`).
6+
case none = 0
7+
/// Lightweight report (no symbolication, current thread).
8+
case light = 1
9+
/// Full crash report (all threads, symbolicated) – legacy default.
10+
case full = 2
11+
}
12+
313
/// Backtrace client configuration settings.
414
@objc public class BacktraceClientConfiguration: NSObject {
515

@@ -24,9 +34,18 @@ import Foundation
2434

2535
/// Flag indicating if the Backtrace client should report reports when the debugger is attached. Default `false`.
2636
@objc public var allowsAttachingDebugger: Bool = false
37+
38+
/// How the SDK should handle OOM detection.
39+
/// Default is `.none` to preserve launch‑time performance unless the integrator opts‑in.
40+
@objc public var oomMode: BacktraceOomMode = .none
2741

28-
/// Flag responsible for detecting and sending possible OOM cashes
29-
@objc public var detectOom: Bool = false
42+
/// The legacy `detectOom` boolean remains for source compatibility but is now deprecated.
43+
@available(*, deprecated, renamed: "oomMode")
44+
@objc public var detectOom: Bool {
45+
get { oomMode != .none }
46+
set { oomMode = newValue ? .full : .none }
47+
}
48+
3049
/// Produces Backtrace client configuration settings.
3150
///
3251
/// - Parameters:
@@ -41,18 +60,39 @@ import Foundation
4160
/// - credentials: Backtrace server API credentials.
4261
/// - dbSettings: Backtrace database settings.
4362
/// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: `30`.
44-
/// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger
45-
/// is attached. Default: `false`.
46-
/// - detectOOM: if set to `true` BacktraceClient will detect when the app is out of memory. Default: `false`.
63+
/// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger is attached. Default: `false`.
64+
/// - oomMode: BacktraceOomMode [.none, .light, .full]
4765
@objc public init(credentials: BacktraceCredentials,
4866
dbSettings: BacktraceDatabaseSettings = BacktraceDatabaseSettings(),
4967
reportsPerMin: Int = 30,
5068
allowsAttachingDebugger: Bool = false,
51-
detectOOM: Bool = false) {
69+
oomMode: BacktraceOomMode = .none) {
5270
self.credentials = credentials
5371
self.dbSettings = dbSettings
5472
self.reportsPerMin = reportsPerMin
5573
self.allowsAttachingDebugger = allowsAttachingDebugger
56-
self.detectOom = detectOOM
74+
self.oomMode = oomMode
75+
}
76+
77+
/// Legacy Initialiser for compatibility.
78+
/// Produces Backtrace client configuration settings.
79+
///
80+
/// - Parameters:
81+
/// - credentials: Backtrace server API credentials.
82+
/// - dbSettings: Backtrace database settings.
83+
/// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: `30`.
84+
/// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger is attached. Default: `false`.
85+
/// - detectOOM: if set to `true` BacktraceClient will detect when the app is out of memory. Default: `false`.
86+
@available(*, deprecated, message: "Use init(credentials:dbSettings:reportsPerMin:allowsAttachingDebugger:oomMode:) instead")
87+
@objc public convenience init(credentials: BacktraceCredentials,
88+
dbSettings: BacktraceDatabaseSettings = .init(),
89+
reportsPerMin: Int = 30,
90+
allowsAttachingDebugger: Bool = false,
91+
detectOOM: Bool = false) {
92+
self.init(credentials: credentials,
93+
dbSettings: dbSettings,
94+
reportsPerMin: reportsPerMin,
95+
allowsAttachingDebugger: allowsAttachingDebugger,
96+
oomMode: detectOOM ? .full : .none)
5797
}
5898
}

Tests/BacktraceOomWatcherTests.swift

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ import Quick
66

77
@testable import Backtrace
88

9+
extension BacktraceOomWatcher {
10+
/// BacktraceOomWatcher now performs all work asynchronously on its dedicated serial queue.
11+
/// The original unit‑tests assume that the start(), handleLowMemoryWarning() and appChangedState(_:) invocations finish synchronously, so their assertions run before the queue has persisted state or updated the static attributes/attachments.
12+
/// **test‑only**
13+
/// Blocks until all queued tasks have completed.
14+
func flushQueue() {
15+
queue.sync(flags: .barrier) { }
16+
}
17+
}
18+
919
class BacktraceOomWatcherTests: QuickSpec {
1020

1121
override func spec() {
@@ -28,7 +38,8 @@ class BacktraceOomWatcherTests: QuickSpec {
2838
oomWatcher = BacktraceOomWatcher(repository: repository,
2939
crashReporter: crashReporter,
3040
attributes: attributesProvider,
31-
backtraceApi: backtraceApi)
41+
backtraceApi: backtraceApi,
42+
oomMode: .full)
3243
BacktraceOomWatcher.clean()
3344

3445
urlSession.response = MockConnectionErrorResponse()
@@ -37,11 +48,12 @@ class BacktraceOomWatcherTests: QuickSpec {
3748
context("when enabled") {
3849
it("it saves the state with properties set") {
3950
oomWatcher?.start()
40-
let savedState = oomWatcher?.loadPreviousState()
51+
oomWatcher?.flushQueue()
52+
let savedState = oomWatcher?._loadPreviousState()
4153

4254
expect { savedState?.state }.to(equal(BacktraceOomWatcher.ApplicationState.active))
43-
expect { savedState?.appVersion }.to(equal(BacktraceOomWatcher.getAppVersion()))
44-
expect { savedState?.version }.to(equal(ProcessInfo.processInfo.operatingSystemVersionString))
55+
expect { savedState?.appVersion }.to(equal(BacktraceOomWatcher.appVersion()))
56+
expect { savedState?.osVersion }.to(equal(ProcessInfo.processInfo.operatingSystemVersionString))
4557
expect { savedState?.debugger }.to(equal(DebuggerChecker.isAttached()))
4658
expect { savedState?.memoryWarningReceived }.to(beFalse())
4759
expect { BacktraceOomWatcher.reportAttributes }.to(beNil())
@@ -51,14 +63,16 @@ class BacktraceOomWatcherTests: QuickSpec {
5163

5264
oomWatcher?.start()
5365
oomWatcher?.appChangedState(BacktraceOomWatcher.ApplicationState.background)
54-
let savedState = oomWatcher?.loadPreviousState()
66+
oomWatcher?.flushQueue()
67+
let savedState = oomWatcher?._loadPreviousState()
5568

5669
expect { savedState?.state }.to(equal(BacktraceOomWatcher.ApplicationState.background))
5770
}
5871
it("low memory warning results in updated state file with resource and attributes") {
5972
oomWatcher?.start()
6073
oomWatcher?.handleLowMemoryWarning()
61-
let savedState = oomWatcher?.loadPreviousState()
74+
oomWatcher?.flushQueue()
75+
let savedState = oomWatcher?._loadPreviousState()
6276

6377
expect { savedState?.memoryWarningReceived }.to(beTrue())
6478
expect { BacktraceOomWatcher.reportAttributes }.toNot(beNil())
@@ -72,6 +86,7 @@ class BacktraceOomWatcherTests: QuickSpec {
7286

7387
oomWatcher?.start()
7488
oomWatcher?.handleLowMemoryWarning()
89+
oomWatcher?.flushQueue()
7590

7691
let shouldNotBeAddedFile = URL(fileURLWithPath: "should-not-be-added")
7792
try "".write(to: shouldNotBeAddedFile, atomically: true, encoding: .utf8)
@@ -84,6 +99,8 @@ class BacktraceOomWatcherTests: QuickSpec {
8499
oomWatcher?.handleLowMemoryWarning()
85100
oomWatcher?.handleLowMemoryWarning()
86101
oomWatcher?.handleLowMemoryWarning()
102+
103+
oomWatcher?.flushQueue()
87104

88105
expect { BacktraceOomWatcher.reportAttachments }.to(beEmpty())
89106
expect { BacktraceOomWatcher.reportAttributes?["should-not"] }.to(beNil())
@@ -99,10 +116,61 @@ class BacktraceOomWatcherTests: QuickSpec {
99116
oomWatcher?.attributesProvider.attributes["should"] = "be-added"
100117

101118
oomWatcher?.handleLowMemoryWarning()
119+
oomWatcher?.flushQueue()
102120
expect { BacktraceOomWatcher.reportAttachments?.first?.path }.to(contain("should-be-added"))
103121
expect { BacktraceOomWatcher.reportAttributes?["should"] }.toNot(beNil())
104122
}
105123
}
124+
125+
context("when oomMode == .none") {
126+
it("does not create a state file or send reports") {
127+
oomWatcher = BacktraceOomWatcher(repository: repository,
128+
crashReporter: crashReporter,
129+
attributes: AttributesProvider(),
130+
backtraceApi: backtraceApi,
131+
oomMode: .none)
132+
133+
oomWatcher?.start()
134+
oomWatcher?.flushQueue()
135+
136+
expect(FileManager.default.fileExists(atPath: BacktraceOomWatcher.oomFileURL!.path)).to(beFalse())
137+
}
138+
}
139+
140+
context("when oomMode == .light") {
141+
142+
it("reports exactly once and off the main thread") {
143+
urlSession.response = MockOkResponse()
144+
var willSendCalls = 0
145+
146+
let attrsProvider = AttributesProvider()
147+
try "".write(to: newFile, atomically: true, encoding: .utf8)
148+
attrsProvider.attachments.append(newFile)
149+
150+
oomWatcher = BacktraceOomWatcher(repository: repository,
151+
crashReporter: crashReporter,
152+
attributes: attrsProvider,
153+
backtraceApi: backtraceApi,
154+
oomMode: .light)
155+
156+
let delegate = BacktraceClientDelegateMock()
157+
delegate.willSendClosure = { report in
158+
willSendCalls += 1
159+
return report
160+
}
161+
backtraceApi.delegate = delegate
162+
163+
oomWatcher?.start()
164+
oomWatcher?.state.debugger = false
165+
oomWatcher?.handleLowMemoryWarning()
166+
oomWatcher?.flushQueue()
167+
oomWatcher?._sendPendingOomReports()
168+
oomWatcher?.flushQueue()
169+
170+
expect(willSendCalls).to(equal(1))
171+
}
172+
}
173+
106174
context("with sending mocks") {
107175
var calledWillSend = 0
108176
var delegate = BacktraceClientDelegateMock()
@@ -133,12 +201,14 @@ class BacktraceOomWatcherTests: QuickSpec {
133201
oomWatcher?.state.debugger = false
134202

135203
oomWatcher?.handleLowMemoryWarning()
204+
oomWatcher?.flushQueue()
136205

137206
backtraceApi.delegate = delegate
138-
oomWatcher?.sendPendingOomReports()
207+
oomWatcher?._sendPendingOomReports()
139208

140209
expect { calledWillSend }.to(equal(1))
141210
}
211+
142212
it("can handle missing attributes and attachments") {
143213
urlSession.response = MockOkResponse()
144214

@@ -148,6 +218,7 @@ class BacktraceOomWatcherTests: QuickSpec {
148218
oomWatcher?.state.debugger = false
149219

150220
oomWatcher?.handleLowMemoryWarning()
221+
oomWatcher?.flushQueue()
151222

152223
BacktraceOomWatcher.reportAttributes = nil
153224
BacktraceOomWatcher.reportAttachments = nil
@@ -163,8 +234,8 @@ class BacktraceOomWatcherTests: QuickSpec {
163234
}
164235

165236
backtraceApi.delegate = delegate
166-
oomWatcher?.sendPendingOomReports()
167-
237+
oomWatcher?._sendPendingOomReports()
238+
168239
expect { calledWillSend }.to(equal(1))
169240
}
170241
it("results in oom report NOT being sent when oom requirements NOT met: no warning") {
@@ -175,7 +246,7 @@ class BacktraceOomWatcherTests: QuickSpec {
175246
oomWatcher?.handleLowMemoryWarning()
176247

177248
backtraceApi.delegate = delegate
178-
oomWatcher?.sendPendingOomReports()
249+
oomWatcher?._sendPendingOomReports()
179250

180251
expect { calledWillSend }.to(equal(0))
181252
}
@@ -185,7 +256,7 @@ class BacktraceOomWatcherTests: QuickSpec {
185256
oomWatcher?.state.debugger = false
186257

187258
backtraceApi.delegate = delegate
188-
oomWatcher?.sendPendingOomReports()
259+
oomWatcher?._sendPendingOomReports()
189260

190261
expect { calledWillSend }.to(equal(0))
191262
}
@@ -197,19 +268,19 @@ class BacktraceOomWatcherTests: QuickSpec {
197268
oomWatcher?.handleLowMemoryWarning()
198269

199270
backtraceApi.delegate = delegate
200-
oomWatcher?.sendPendingOomReports()
271+
oomWatcher?._sendPendingOomReports()
201272

202273
expect { calledWillSend }.to(equal(0))
203274
}
204275
it("results in oom report NOT being sent when oom requirements NOT met: other OS version") {
205276
// OS version different: no report.
206277
oomWatcher?.start()
207278
oomWatcher?.state.debugger = false
208-
oomWatcher?.state.version = "1.2.3"
279+
oomWatcher?.state.osVersion = "1.2.3"
209280
oomWatcher?.handleLowMemoryWarning()
210281

211282
backtraceApi.delegate = delegate
212-
oomWatcher?.sendPendingOomReports()
283+
oomWatcher?._sendPendingOomReports()
213284

214285
expect { calledWillSend }.to(equal(0))
215286
}

0 commit comments

Comments
 (0)