Skip to content

Commit 9602c11

Browse files
authored
Move startup log writes off the main thread (#29103)
1 parent 8d5bf7d commit 9602c11

1 file changed

Lines changed: 42 additions & 37 deletions

File tree

shared/ios/Keybase/AppDelegate.swift

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate,
4848
var fsPaths: [String: String] = [:]
4949
var shutdownTask: UIBackgroundTaskIdentifier = .invalid
5050
var iph: ItemProviderHelper?
51-
var startupLogFileHandle: FileHandle?
51+
private var startupLogFileHandle: FileHandle?
52+
private let logQueue = DispatchQueue(label: "kb.startup.log", qos: .utility)
5253

5354
private var watchdog: MainThreadWatchdog?
5455
private var bgEnterWallTime: CFAbsoluteTime = 0
@@ -154,6 +155,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate,
154155
/////// KB specific
155156

156157
private static var appStartTime: CFAbsoluteTime = 0
158+
private static let initLog = OSLog(subsystem: "keybase", category: "init")
157159

158160
private static let logDateFormatter: DateFormatter = {
159161
let f = DateFormatter()
@@ -167,59 +169,62 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate,
167169
return
168170
}
169171

170-
if self.startupLogFileHandle == nil {
171-
if !FileManager.default.fileExists(atPath: logFilePath) {
172-
// Match the parent directory's protection class so the file remains accessible
173-
// in the background after the device has been unlocked once.
174-
FileManager.default.createFile(
175-
atPath: logFilePath, contents: nil,
176-
attributes: [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication])
177-
}
178-
if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
179-
fileHandle.seekToEndOfFile()
180-
self.startupLogFileHandle = fileHandle
181-
} else {
182-
NSLog("Error opening startup timing log file: \(logFilePath)")
183-
return
184-
}
185-
}
186-
187-
guard let fileHandle = self.startupLogFileHandle else {
188-
return
189-
}
190-
172+
// Capture timestamp on the calling thread so it reflects when the event actually occurred.
191173
let now = Date()
192174
let timeInterval = now.timeIntervalSince1970
193175
let microseconds = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1_000_000)
194176
let dateString = AppDelegate.logDateFormatter.string(from: now)
195177
let timestamp = String(format: "%@.%06dZ", dateString, microseconds)
196-
197178
let fileName = URL(fileURLWithPath: file).lastPathComponent
198179
let logMessage = String(
199180
format: "%@ ▶ [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message
200181
)
201-
202182
guard let logData = logMessage.data(using: .utf8) else {
203183
return
204184
}
205185

206-
do {
207-
try fileHandle.write(contentsOf: logData)
208-
try fileHandle.synchronize()
209-
} catch {
210-
NSLog("Error writing startup timing log: \(error)")
186+
// Dispatch file I/O (including fsync) to a background queue so the calling thread
187+
// (often the main thread) is never blocked by disk latency.
188+
logQueue.async { [weak self] in
189+
guard let self else { return }
190+
if self.startupLogFileHandle == nil {
191+
if !FileManager.default.fileExists(atPath: logFilePath) {
192+
// Match the parent directory's protection class so the file remains accessible
193+
// in the background after the device has been unlocked once.
194+
FileManager.default.createFile(
195+
atPath: logFilePath, contents: nil,
196+
attributes: [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication])
197+
}
198+
if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
199+
fileHandle.seekToEndOfFile()
200+
self.startupLogFileHandle = fileHandle
201+
} else {
202+
NSLog("Error opening startup timing log file: \(logFilePath)")
203+
return
204+
}
205+
}
206+
guard let fileHandle = self.startupLogFileHandle else { return }
207+
do {
208+
try fileHandle.write(contentsOf: logData)
209+
try fileHandle.synchronize()
210+
} catch {
211+
NSLog("Error writing startup timing log: \(error)")
212+
}
211213
}
212214
}
213215

214216
private func closeStartupLogFile() {
215-
if let fileHandle = self.startupLogFileHandle {
216-
do {
217-
try fileHandle.synchronize()
218-
try fileHandle.close()
219-
} catch {
220-
NSLog("Error closing startup timing log: \(error)")
217+
// Use sync so all pending async log writes are flushed before we close the handle.
218+
logQueue.sync {
219+
if let fileHandle = self.startupLogFileHandle {
220+
do {
221+
try fileHandle.synchronize()
222+
try fileHandle.close()
223+
} catch {
224+
NSLog("Error closing startup timing log: \(error)")
225+
}
226+
self.startupLogFileHandle = nil
221227
}
222-
self.startupLogFileHandle = nil
223228
}
224229
}
225230

@@ -251,7 +256,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate,
251256
if let err {
252257
// Log to system log with public annotation so it's not redacted in logarchive.
253258
os_log(
254-
.error, log: OSLog(subsystem: "keybase", category: "init"),
259+
.error, log: AppDelegate.initLog,
255260
"KeybaseInit FAILED: %{public}@ (code=%ld domain=%{public}@)",
256261
err.localizedDescription, err.code, err.domain)
257262
// Also write to ios.log so it's captured in xcappdata even without a device attached.

0 commit comments

Comments
 (0)