From 738de778af090178e371f31245937c5b720af76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 24 May 2023 17:33:25 +0200 Subject: [PATCH 1/6] feat: Try to optimize loading speed --- Mail/Views/Thread/MessageView.swift | 52 +++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Mail/Views/Thread/MessageView.swift b/Mail/Views/Thread/MessageView.swift index 9e2fda985..d9b7dd7b1 100644 --- a/Mail/Views/Thread/MessageView.swift +++ b/Mail/Views/Thread/MessageView.swift @@ -34,6 +34,9 @@ struct MessageView: View { @LazyInjectService var matomo: MatomoUtils + /// Something to process the inline images in the background + let inlineImageProcessing = TaskQueue(concurrency: max(4, ProcessInfo.processInfo.activeProcessorCount)) + init(message: Message, isMessageExpanded: Bool = false) { self.message = message presentableBody = PresentableBody(message: message) @@ -100,22 +103,45 @@ struct MessageView: View { } private func insertInlineAttachments() async throws { - let attachmentsArray = message.attachments.filter { $0.disposition == .inline }.toArray() - for attachment in attachmentsArray { - if let contentId = attachment.contentId { - let attachmentData = try await mailboxManager.attachmentData(attachment: attachment) - - presentableBody.body?.value = presentableBody.body?.value?.replacingOccurrences( - of: "cid:\(contentId)", - with: "data:\(attachment.mimeType);base64,\(attachmentData.base64EncodedString())" - ) - presentableBody.compactBody = presentableBody.compactBody?.replacingOccurrences( - of: "cid:\(contentId)", - with: "data:\(attachment.mimeType);base64,\(attachmentData.base64EncodedString())" - ) + Task { + print("insertInlineAttachments") + let start = CFAbsoluteTimeGetCurrent() + let attachmentsArray = message.attachments.filter { $0.disposition == .inline }.toArray() + + for attachment in attachmentsArray { + // background async multithread inline image processing + try await inlineImageProcessing.enqueue { + guard let contentId = attachment.contentId else { + return + } + + let data = try await mailboxManager.attachmentData(attachment: attachment) + var body = await presentableBody.body?.value + var compactBody = await presentableBody.compactBody + + body = body?.replacingOccurrences( + of: "cid:\(contentId)", + with: "data:\(attachment.mimeType);base64,\(data.base64EncodedString())" + ) + compactBody = compactBody?.replacingOccurrences( + of: "cid:\(contentId)", + with: "data:\(attachment.mimeType);base64,\(data.base64EncodedString())" + ) + + print("• render start") + await self.insertInlineAttachment(body: body, compactBody: compactBody) + print("• render end") + } } + let diff = CFAbsoluteTimeGetCurrent() - start + print("diff:\(diff)") } } + + @MainActor func insertInlineAttachment(body: String?, compactBody: String?) { + presentableBody.body?.value = body + presentableBody.compactBody = compactBody + } } struct MessageView_Previews: PreviewProvider { From 69b811fb112f3204b762167acdc0bbf3c1e095e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 24 May 2023 18:15:31 +0200 Subject: [PATCH 2/6] feat(performanceSpeedup): Batching loading of images --- Mail/Views/Thread/MessageView.swift | 80 ++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/Mail/Views/Thread/MessageView.swift b/Mail/Views/Thread/MessageView.swift index d9b7dd7b1..7e0f9767c 100644 --- a/Mail/Views/Thread/MessageView.swift +++ b/Mail/Views/Thread/MessageView.swift @@ -25,6 +25,40 @@ import RealmSwift import Shimmer import SwiftUI +// TODO: move to Core +extension Sequence { + func asyncMap( + _ transform: (Element) async throws -> T + ) async rethrows -> [T] { + var values = [T]() + + for element in self { + try await values.append(transform(element)) + } + + return values + } +} + +// TODO: move to core +extension Sequence { + func asyncForEach( + _ operation: (Element) async throws -> Void + ) async rethrows { + for element in self { + try await operation(element) + } + } +} + +// TODO: move to core +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} + struct MessageView: View { @ObservedRealmObject var message: Message @State var presentableBody: PresentableBody @@ -35,7 +69,7 @@ struct MessageView: View { @LazyInjectService var matomo: MatomoUtils /// Something to process the inline images in the background - let inlineImageProcessing = TaskQueue(concurrency: max(4, ProcessInfo.processInfo.activeProcessorCount)) + let inlineImageProcessing = TaskQueue() init(message: Message, isMessageExpanded: Bool = false) { self.message = message @@ -103,22 +137,31 @@ struct MessageView: View { } private func insertInlineAttachments() async throws { - Task { + try await inlineImageProcessing.enqueue { print("insertInlineAttachments") let start = CFAbsoluteTimeGetCurrent() - let attachmentsArray = message.attachments.filter { $0.disposition == .inline }.toArray() - - for attachment in attachmentsArray { - // background async multithread inline image processing - try await inlineImageProcessing.enqueue { - guard let contentId = attachment.contentId else { - return + let attachmentsArray = await message.attachments.filter { $0.disposition == .inline }.toArray() + + // Since mutation of the DOM is costly, I batch the processing of images, then mutate the DOM. + let chunks = attachmentsArray.chunked(into: 5) + + for chunk in chunks { + // Download images for the current chunk + let dataArray = try await chunk.asyncMap { + try await mailboxManager.attachmentData(attachment: $0) + } + + // Read the DOM once + var body = await presentableBody.body?.value + var compactBody = await presentableBody.compactBody + + // Prepare the new DOM with the loaded images + for (index, attachment) in chunk.enumerated() { + guard let contentId = attachment.contentId, + let data = dataArray[safe: index] else { + continue } - - let data = try await mailboxManager.attachmentData(attachment: attachment) - var body = await presentableBody.body?.value - var compactBody = await presentableBody.compactBody - + body = body?.replacingOccurrences( of: "cid:\(contentId)", with: "data:\(attachment.mimeType);base64,\(data.base64EncodedString())" @@ -127,17 +170,18 @@ struct MessageView: View { of: "cid:\(contentId)", with: "data:\(attachment.mimeType);base64,\(data.base64EncodedString())" ) - - print("• render start") - await self.insertInlineAttachment(body: body, compactBody: compactBody) - print("• render end") } + + // Mutate DOM + await self.insertInlineAttachment(body: body, compactBody: compactBody) } + let diff = CFAbsoluteTimeGetCurrent() - start print("diff:\(diff)") } } + /// Update the DOM in the main thread @MainActor func insertInlineAttachment(body: String?, compactBody: String?) { presentableBody.body?.value = body presentableBody.compactBody = compactBody From 73878aaa7f97cb18f43490f111458f29a46454fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 25 May 2023 08:18:35 +0200 Subject: [PATCH 3/6] feat: UI more responsive + cleanup --- Mail/Views/Thread/MessageView.swift | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/Mail/Views/Thread/MessageView.swift b/Mail/Views/Thread/MessageView.swift index 7e0f9767c..056eb561c 100644 --- a/Mail/Views/Thread/MessageView.swift +++ b/Mail/Views/Thread/MessageView.swift @@ -68,9 +68,6 @@ struct MessageView: View { @LazyInjectService var matomo: MatomoUtils - /// Something to process the inline images in the background - let inlineImageProcessing = TaskQueue() - init(message: Message, isMessageExpanded: Bool = false) { self.message = message presentableBody = PresentableBody(message: message) @@ -137,13 +134,10 @@ struct MessageView: View { } private func insertInlineAttachments() async throws { - try await inlineImageProcessing.enqueue { - print("insertInlineAttachments") - let start = CFAbsoluteTimeGetCurrent() - let attachmentsArray = await message.attachments.filter { $0.disposition == .inline }.toArray() - + Task { // Since mutation of the DOM is costly, I batch the processing of images, then mutate the DOM. - let chunks = attachmentsArray.chunked(into: 5) + let attachmentsArray = message.attachments.filter { $0.disposition == .inline }.toArray() + let chunks = attachmentsArray.chunked(into: 10) for chunk in chunks { // Download images for the current chunk @@ -152,8 +146,8 @@ struct MessageView: View { } // Read the DOM once - var body = await presentableBody.body?.value - var compactBody = await presentableBody.compactBody + var body = presentableBody.body?.value + var compactBody = presentableBody.compactBody // Prepare the new DOM with the loaded images for (index, attachment) in chunk.enumerated() { @@ -173,11 +167,11 @@ struct MessageView: View { } // Mutate DOM - await self.insertInlineAttachment(body: body, compactBody: compactBody) + self.insertInlineAttachment(body: body, compactBody: compactBody) + + // Delay between each chunk processing just enough, so the user feels the UI is responsive. + try await Task.sleep(nanoseconds: 4_000_000_000) } - - let diff = CFAbsoluteTimeGetCurrent() - start - print("diff:\(diff)") } } From e6fb0ecd0b3b2073f5321307077ec59223a79261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 25 May 2023 09:49:06 +0200 Subject: [PATCH 4/6] chore: Sonar Feedback --- Mail/Views/Thread/MessageView.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Mail/Views/Thread/MessageView.swift b/Mail/Views/Thread/MessageView.swift index 056eb561c..db369d66f 100644 --- a/Mail/Views/Thread/MessageView.swift +++ b/Mail/Views/Thread/MessageView.swift @@ -54,7 +54,7 @@ extension Sequence { // TODO: move to core extension Collection { /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript (safe index: Index) -> Element? { + subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } } @@ -138,25 +138,25 @@ struct MessageView: View { // Since mutation of the DOM is costly, I batch the processing of images, then mutate the DOM. let attachmentsArray = message.attachments.filter { $0.disposition == .inline }.toArray() let chunks = attachmentsArray.chunked(into: 10) - + for chunk in chunks { // Download images for the current chunk let dataArray = try await chunk.asyncMap { try await mailboxManager.attachmentData(attachment: $0) } - + // Read the DOM once - var body = presentableBody.body?.value + var mailBody = presentableBody.body?.value var compactBody = presentableBody.compactBody - + // Prepare the new DOM with the loaded images for (index, attachment) in chunk.enumerated() { guard let contentId = attachment.contentId, - let data = dataArray[safe: index] else { + let data = dataArray[safe: index] else { continue } - - body = body?.replacingOccurrences( + + mailBody = mailBody?.replacingOccurrences( of: "cid:\(contentId)", with: "data:\(attachment.mimeType);base64,\(data.base64EncodedString())" ) @@ -167,8 +167,8 @@ struct MessageView: View { } // Mutate DOM - self.insertInlineAttachment(body: body, compactBody: compactBody) - + self.insertInlineAttachment(body: mailBody, compactBody: compactBody) + // Delay between each chunk processing just enough, so the user feels the UI is responsive. try await Task.sleep(nanoseconds: 4_000_000_000) } From d9361ac2924bf28835f98e92930ae94c5206b35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 25 May 2023 10:46:07 +0200 Subject: [PATCH 5/6] feat: Detached task --- Mail/Views/Thread/MessageView.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Mail/Views/Thread/MessageView.swift b/Mail/Views/Thread/MessageView.swift index db369d66f..1874ea3fe 100644 --- a/Mail/Views/Thread/MessageView.swift +++ b/Mail/Views/Thread/MessageView.swift @@ -133,10 +133,10 @@ struct MessageView: View { presentableBody.quote = messageBodyQuote.quote } - private func insertInlineAttachments() async throws { - Task { + private func insertInlineAttachments() { + Task.detached() { // Since mutation of the DOM is costly, I batch the processing of images, then mutate the DOM. - let attachmentsArray = message.attachments.filter { $0.disposition == .inline }.toArray() + let attachmentsArray = await message.attachments.filter { $0.disposition == .inline }.toArray() let chunks = attachmentsArray.chunked(into: 10) for chunk in chunks { @@ -146,8 +146,8 @@ struct MessageView: View { } // Read the DOM once - var mailBody = presentableBody.body?.value - var compactBody = presentableBody.compactBody + var mailBody = await presentableBody.body?.value + var compactBody = await presentableBody.compactBody // Prepare the new DOM with the loaded images for (index, attachment) in chunk.enumerated() { @@ -167,7 +167,7 @@ struct MessageView: View { } // Mutate DOM - self.insertInlineAttachment(body: mailBody, compactBody: compactBody) + await self.mutate(body: mailBody, compactBody: compactBody) // Delay between each chunk processing just enough, so the user feels the UI is responsive. try await Task.sleep(nanoseconds: 4_000_000_000) @@ -176,7 +176,7 @@ struct MessageView: View { } /// Update the DOM in the main thread - @MainActor func insertInlineAttachment(body: String?, compactBody: String?) { + @MainActor func mutate(body: String?, compactBody: String?) { presentableBody.body?.value = body presentableBody.compactBody = compactBody } From 8c4ca96b628f418ec35de3dd5890f41b8b02e735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 25 May 2023 16:25:19 +0200 Subject: [PATCH 6/6] chore: bump ios-core --- Mail/Views/Thread/MessageView.swift | 34 ----------------------------- Project.swift | 2 +- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/Mail/Views/Thread/MessageView.swift b/Mail/Views/Thread/MessageView.swift index 1874ea3fe..ffbc34636 100644 --- a/Mail/Views/Thread/MessageView.swift +++ b/Mail/Views/Thread/MessageView.swift @@ -25,40 +25,6 @@ import RealmSwift import Shimmer import SwiftUI -// TODO: move to Core -extension Sequence { - func asyncMap( - _ transform: (Element) async throws -> T - ) async rethrows -> [T] { - var values = [T]() - - for element in self { - try await values.append(transform(element)) - } - - return values - } -} - -// TODO: move to core -extension Sequence { - func asyncForEach( - _ operation: (Element) async throws -> Void - ) async rethrows { - for element in self { - try await operation(element) - } - } -} - -// TODO: move to core -extension Collection { - /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript(safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil - } -} - struct MessageView: View { @ObservedRealmObject var message: Message @State var presentableBody: PresentableBody diff --git a/Project.swift b/Project.swift index 2d09c1734..aa6d3edb8 100644 --- a/Project.swift +++ b/Project.swift @@ -29,7 +29,7 @@ let project = Project(name: "Mail", packages: [ .package(url: "https://github.com/Infomaniak/ios-login", .upToNextMajor(from: "4.0.0")), .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "1.1.6")), - .package(url: "https://github.com/Infomaniak/ios-core", .revision("4c0c389de2e10e615ad79a854d2489977017efe5")), + .package(url: "https://github.com/Infomaniak/ios-core", .revision("9fc1a0f41ce0e830189b756d3b1a42d4e67421cc")), .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "2.3.0")), .package(url: "https://github.com/Infomaniak/ios-notifications", .upToNextMajor(from: "2.1.0")), .package(url: "https://github.com/Infomaniak/ios-create-account", .upToNextMajor(from: "1.1.0")),