Refactor the NSE to create a new UserSession every time a payload comes in and pass the room down to the SDK so it can optimize how many rooms it loads from the store and how much memory it's using.

- this will undo creating at most one user session pe process as that shoulnd't be a problem anymore
- it also renames some methods and improves the logging
This commit is contained in:
Stefan Ceriu
2025-04-03 15:47:23 +03:00
parent 5500cc2335
commit 7657f0f138
2 changed files with 53 additions and 68 deletions
+37 -57
View File
@@ -28,25 +28,24 @@ import UserNotifications
// but it will always be called on different threads. It may or may not be // but it will always be called on different threads. It may or may not be
// called on the same instance of `NotificationService` as a previous // called on the same instance of `NotificationService` as a previous
// notification. // notification.
//
// We keep a global `environment` singleton to ensure that our app context,
// database, logging, etc. are only ever setup once per *process*
private let settings: CommonSettingsProtocol = AppSettings() private let settings: CommonSettingsProtocol = AppSettings()
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), destination: .notification),
settings: settings)
private let keychainController = KeychainController(service: .sessions, private let keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
private let eventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()),
destination: .notification)
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder,
settings: settings)
class NotificationServiceExtension: UNNotificationServiceExtension { class NotificationServiceExtension: UNNotificationServiceExtension {
private var handler: ((UNNotificationContent) -> Void)? private var handler: ((UNNotificationContent) -> Void)?
private var modifiedContent: UNMutableNotificationContent? private var modifiedContent: UNMutableNotificationContent?
private let appHooks = AppHooks() private let appHooks = AppHooks()
// Used to create one single UserSession across process/instances/runs
private static let serialQueue = DispatchQueue(label: "io.element.elementx.nse")
deinit { deinit {
cleanUp() cleanUp()
ExtensionLogger.logMemory(with: tag) ExtensionLogger.logMemory(with: tag)
@@ -74,63 +73,49 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent
MXLog.info("\(tag) #########################################") MXLog.info("\(tag) #########################################")
ExtensionLogger.logMemory(with: tag)
MXLog.info("\(tag) Payload came: \(request.content.userInfo)")
Self.serialQueue.sync { ExtensionLogger.logMemory(with: tag)
// If the session directories have changed, the user has logged out and back in (even if they entered the same user ID). MXLog.info("\(tag) Received payload: \(request.content.userInfo)")
// We can't do this comparison with the access token of the existing session here due to token refresh when using OIDC.
if Self.userSession == nil || Self.userSession?.sessionDirectories != credentials.restorationToken.sessionDirectories { Task {
// This function might be run concurrently and from different processes
// It's imperative that we create **at most** one UserSession/Client per process
Task.synchronous { [appHooks] in
do { do {
Self.userSession = try await NSEUserSession(credentials: credentials, let userSession = try await NSEUserSession(credentials: credentials,
roomID: roomID,
clientSessionDelegate: keychainController, clientSessionDelegate: keychainController,
appHooks: appHooks, appHooks: appHooks,
appSettings: settings) appSettings: settings)
ExtensionLogger.logMemory(with: tag)
MXLog.info("\(tag) Configured user session")
await processEvent(eventID,
roomID: roomID,
unreadCount: request.unreadCount,
userSession: userSession)
} catch { } catch {
MXLog.error("Failed creating user session with error: \(error)") MXLog.error("Failed creating user session with error: \(error)")
} }
} }
} }
if Self.userSession == nil {
return discard(unreadCount: request.unreadCount)
}
}
Task {
await run(with: credentials,
roomID: roomID,
eventID: eventID,
unreadCount: request.unreadCount)
}
}
override func serviceExtensionTimeWillExpire() { override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system. // Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
MXLog.warning("\(tag) serviceExtensionTimeWillExpire") MXLog.warning("\(tag) Extension time will expire")
notify(unreadCount: nil) notify(unreadCount: nil)
} }
// MARK: - Private // MARK: - Private
private func run(with credentials: KeychainCredentials, private func processEvent(_ eventID: String,
roomID: String, roomID: String,
eventID: String, unreadCount: Int?,
unreadCount: Int?) async { userSession: NSEUserSession) async {
MXLog.info("\(tag) run with roomId: \(roomID), eventId: \(eventID)") MXLog.info("\(tag) Processing event: \(eventID) in room: \(roomID)")
guard let userSession = Self.userSession else {
MXLog.error("Invalid NSE User Session, discarding.")
return discard(unreadCount: unreadCount)
}
do { do {
guard let itemProxy = await userSession.notificationItemProxy(roomID: roomID, eventID: eventID) else { guard let itemProxy = await userSession.notificationItemProxy(roomID: roomID, eventID: eventID) else {
MXLog.info("\(tag) no notification for the event, discard") MXLog.error("\(tag) Failed retrieving notification item")
return discard(unreadCount: unreadCount) return discard(unreadCount: unreadCount)
} }
@@ -141,40 +126,35 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
break break
} }
// After the first processing, update the modified content
modifiedContent = try await notificationContentBuilder.content(for: itemProxy, mediaProvider: nil) modifiedContent = try await notificationContentBuilder.content(for: itemProxy, mediaProvider: nil)
guard itemProxy.hasMedia else { guard itemProxy.hasMedia else {
MXLog.info("\(tag) no media needed") MXLog.info("\(tag) Notification item doesn't contain media")
// We've processed the item and no media operations needed, so no need to go further
return notify(unreadCount: unreadCount) return notify(unreadCount: unreadCount)
} }
MXLog.info("\(tag) process with media") MXLog.info("\(tag) Processing media")
// There is some media to load, process it again
if let latestContent = try? await notificationContentBuilder.content(for: itemProxy, mediaProvider: userSession.mediaProvider) { if let latestContent = try? await notificationContentBuilder.content(for: itemProxy, mediaProvider: userSession.mediaProvider) {
// Processing finished, hopefully with some media
modifiedContent = latestContent modifiedContent = latestContent
} else {
MXLog.error("\(tag) Failed processing notification media")
} }
// We still notify, but without the media attachment if it fails to load
return notify(unreadCount: unreadCount) return notify(unreadCount: unreadCount)
} catch { } catch {
MXLog.error("NSE run error: \(error)") MXLog.error("Failed processing with error: \(error)")
return discard(unreadCount: unreadCount) return discard(unreadCount: unreadCount)
} }
} }
private func notify(unreadCount: Int?) { private func notify(unreadCount: Int?) {
MXLog.info("\(tag) notify")
guard let modifiedContent else { guard let modifiedContent else {
MXLog.info("\(tag) notify: no modified content") MXLog.error("\(tag) Notification modified content invalid")
return discard(unreadCount: unreadCount) return discard(unreadCount: unreadCount)
} }
MXLog.info("\(tag) Displaying notification")
if let unreadCount { if let unreadCount {
modifiedContent.badge = NSNumber(value: unreadCount) modifiedContent.badge = NSNumber(value: unreadCount)
} }
@@ -184,7 +164,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
} }
private func discard(unreadCount: Int?) { private func discard(unreadCount: Int?) {
MXLog.info("\(tag) discard") MXLog.info("\(tag) Discarding notification")
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
@@ -226,7 +206,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
} }
case .roomRedaction(let redactedEventID, _): case .roomRedaction(let redactedEventID, _):
guard let redactedEventID else { guard let redactedEventID else {
MXLog.error("Unable to handle redact notification due to missing event ID.") MXLog.error("Unable to handle redact notification due to missing event ID")
return .processedShouldDiscard return .processedShouldDiscard
} }
+9 -4
View File
@@ -19,7 +19,11 @@ final class NSEUserSession {
networkMonitor: nil) networkMonitor: nil)
private let delegateHandle: TaskHandle? private let delegateHandle: TaskHandle?
init(credentials: KeychainCredentials, clientSessionDelegate: ClientSessionDelegate, appHooks: AppHooks, appSettings: CommonSettingsProtocol) async throws { init(credentials: KeychainCredentials,
roomID: String,
clientSessionDelegate: ClientSessionDelegate,
appHooks: AppHooks,
appSettings: CommonSettingsProtocol) async throws {
sessionDirectories = credentials.restorationToken.sessionDirectories sessionDirectories = credentials.restorationToken.sessionDirectories
userID = credentials.userID userID = credentials.userID
@@ -34,8 +38,7 @@ final class NSEUserSession {
slidingSync: .restored, slidingSync: .restored,
sessionDelegate: clientSessionDelegate, sessionDelegate: clientSessionDelegate,
appHooks: appHooks, appHooks: appHooks,
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode, enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode)
eventCacheEnabled: appSettings.eventCacheEnabled)
.sessionPaths(dataPath: credentials.restorationToken.sessionDirectories.dataPath, .sessionPaths(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
cachePath: credentials.restorationToken.sessionDirectories.cachePath) cachePath: credentials.restorationToken.sessionDirectories.cachePath)
.username(username: credentials.userID) .username(username: credentials.userID)
@@ -44,7 +47,9 @@ final class NSEUserSession {
baseClient = try await clientBuilder.build() baseClient = try await clientBuilder.build()
delegateHandle = baseClient.setDelegate(delegate: ClientDelegateWrapper()) delegateHandle = baseClient.setDelegate(delegate: ClientDelegateWrapper())
try await baseClient.restoreSession(session: credentials.restorationToken.session)
try await baseClient.restoreSessionWith(session: credentials.restorationToken.session,
roomLoadSettings: .one(roomId: roomID))
notificationClient = try await baseClient.notificationClient(processSetup: .multipleProcesses) notificationClient = try await baseClient.notificationClient(processSetup: .multipleProcesses)
} }