mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Various performance tweaks (#474)
* Store and reuse room list placeholder avatars and last messages * Cache and reuse HomeScreenRooms * Reduce RoomSummaryProvider diff collection time * Promote more logs to info * Slighty tweak RustTracing to make it easier to configure * Move TimelineProvider and RoomTimelineController item processing to background queues * Prevent the timeline from stopping an ogoing decelerating scroll when starting backpaginating
This commit is contained in:
@@ -14,48 +14,60 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Collections
|
||||
import MatrixRustSDK
|
||||
|
||||
// This exposes the full Rust side tracing subscriber filter for more flexibility.
|
||||
// We can filter by level, crate and even file. See more details here:
|
||||
// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples
|
||||
struct TracingConfiguration {
|
||||
static var release = TracingConfiguration(common: .info)
|
||||
static var debug = TracingConfiguration()
|
||||
static var full = TracingConfiguration(common: .info,
|
||||
targets: [
|
||||
.hyper: .warn,
|
||||
.sled: .warn,
|
||||
.matrix_sdk_sled: .warn,
|
||||
.matrix_sdk_http_client: .trace,
|
||||
.matrix_sdk_ffi_uniffi_api: .warn,
|
||||
.matrix_sdk_ffi: .warn,
|
||||
.matrix_sdk_sliding_sync: .warn,
|
||||
.matrix_sdk_base_sliding_sync: .warn,
|
||||
.matrix_sdk_crypto: .trace
|
||||
])
|
||||
static var release = TracingConfiguration(overrides: [.common: .info])
|
||||
static var debug = TracingConfiguration(overrides: [.common: .info])
|
||||
static func custom(overrides: [Target: LogLevel]) -> TracingConfiguration {
|
||||
TracingConfiguration(overrides: overrides)
|
||||
}
|
||||
|
||||
enum LogLevel: String { case error, warn, info, debug, trace }
|
||||
|
||||
enum Target: String {
|
||||
case common = ""
|
||||
|
||||
case hyper, sled, matrix_sdk_sled, matrix_sdk_ffi, matrix_sdk_crypto
|
||||
|
||||
case matrix_sdk_http_client = "matrix_sdk::http_client"
|
||||
case matrix_sdk_ffi_uniffi_api = "matrix_sdk_ffi::uniffi_api"
|
||||
case matrix_sdk_sliding_sync = "matrix_sdk::sliding_sync"
|
||||
case matrix_sdk_base_sliding_sync = "matrix_sdk_base::sliding_sync"
|
||||
}
|
||||
|
||||
enum LogLevel: String { case error, warn, info, debug, trace }
|
||||
|
||||
var common = LogLevel.warn
|
||||
var targets: [Target: LogLevel] = [
|
||||
let targets: OrderedDictionary<Target, LogLevel> = [
|
||||
.common: .warn,
|
||||
.hyper: .warn,
|
||||
.sled: .warn,
|
||||
.matrix_sdk_sled: .warn,
|
||||
.matrix_sdk_crypto: .debug,
|
||||
.matrix_sdk_http_client: .debug
|
||||
.matrix_sdk_http_client: .debug,
|
||||
.matrix_sdk_sliding_sync: .trace,
|
||||
.matrix_sdk_base_sliding_sync: .trace
|
||||
]
|
||||
|
||||
var overrides = [Target: LogLevel]()
|
||||
|
||||
var filter: String {
|
||||
"\(common),\(targets.map { "\($0.key.rawValue)=\($0.value.rawValue)" }.joined(separator: ","))"
|
||||
var newTargets = targets
|
||||
for (target, logLevel) in overrides {
|
||||
newTargets.updateValue(logLevel, forKey: target)
|
||||
}
|
||||
|
||||
let components = newTargets.map { (target: Target, logLevel: LogLevel) in
|
||||
guard !target.rawValue.isEmpty else {
|
||||
return logLevel.rawValue
|
||||
}
|
||||
|
||||
return "\(target.rawValue)=\(logLevel.rawValue)"
|
||||
}
|
||||
|
||||
return components.joined(separator: ",")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,9 @@ struct HomeScreenViewStateBindings {
|
||||
}
|
||||
|
||||
struct HomeScreenRoom: Identifiable, Equatable {
|
||||
private static let placeholderLastMessage = AttributedString("Last message")
|
||||
private static let placeholderAvatar = UIImage(systemName: "photo")
|
||||
|
||||
/// The list item identifier can be a real room identifier, a custom one for invalidated entries
|
||||
/// or a completely unique one for empty items and skeletons
|
||||
let id: String
|
||||
@@ -102,11 +105,11 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
/// The real room identifier this item points to
|
||||
let roomId: String?
|
||||
|
||||
let name: String
|
||||
var name = ""
|
||||
|
||||
let hasUnreads: Bool
|
||||
var hasUnreads = false
|
||||
|
||||
let timestamp: String?
|
||||
var timestamp: String?
|
||||
|
||||
var lastMessage: AttributedString?
|
||||
|
||||
@@ -122,8 +125,8 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
name: "Placeholder room name",
|
||||
hasUnreads: false,
|
||||
timestamp: "Now",
|
||||
lastMessage: AttributedString("Last message"),
|
||||
avatar: UIImage(systemName: "photo"),
|
||||
lastMessage: Self.placeholderLastMessage,
|
||||
avatar: Self.placeholderAvatar,
|
||||
isPlaceholder: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
private let visibleItemRangePublisher = CurrentValueSubject<Range<Int>, Never>(0..<0)
|
||||
|
||||
private var roomsForIdentifiers = [String: HomeScreenRoom]()
|
||||
|
||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
@@ -178,7 +180,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||
guard let room = state.rooms.first(where: { $0.roomId == identifier }),
|
||||
guard let room = roomsForIdentifiers[identifier],
|
||||
room.avatar == nil,
|
||||
let avatarURL = room.avatarURL else {
|
||||
return
|
||||
@@ -206,6 +208,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.info("Updating rooms")
|
||||
|
||||
var rooms = [HomeScreenRoom]()
|
||||
|
||||
// Try merging together results from both the visibleRoomsSummaryProvider and the allRoomsSummaryProvider
|
||||
@@ -235,24 +239,34 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
|
||||
state.rooms = rooms
|
||||
|
||||
MXLog.info("Finished updating rooms")
|
||||
}
|
||||
|
||||
private func buildRoom(with details: RoomSummaryDetails) -> HomeScreenRoom {
|
||||
let avatarImage = details.avatarURL.flatMap { userSession.mediaProvider.imageFromURL($0, avatarSize: .room(on: .home)) }
|
||||
var room: HomeScreenRoom! = roomsForIdentifiers[details.id]
|
||||
|
||||
var timestamp: String?
|
||||
if let lastMessageTimestamp = details.lastMessageTimestamp {
|
||||
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
||||
if room == nil {
|
||||
room = HomeScreenRoom(id: details.id,
|
||||
roomId: details.id,
|
||||
avatarURL: details.avatarURL)
|
||||
}
|
||||
|
||||
return HomeScreenRoom(id: details.id,
|
||||
roomId: details.id,
|
||||
name: details.name,
|
||||
hasUnreads: details.unreadNotificationCount > 0,
|
||||
timestamp: timestamp,
|
||||
lastMessage: details.lastMessage,
|
||||
avatarURL: details.avatarURL,
|
||||
avatar: avatarImage)
|
||||
room.name = details.name
|
||||
room.hasUnreads = details.unreadNotificationCount > 0
|
||||
room.lastMessage = details.lastMessage
|
||||
|
||||
if let avatarURL = details.avatarURL {
|
||||
room.avatar = userSession.mediaProvider.imageFromURL(avatarURL, avatarSize: .room(on: .home))
|
||||
}
|
||||
|
||||
if let lastMessageTimestamp = details.lastMessageTimestamp {
|
||||
room.timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
roomsForIdentifiers[details.id] = room
|
||||
|
||||
return room
|
||||
}
|
||||
|
||||
private func updateVisibleRange(_ range: Range<Int>) {
|
||||
|
||||
@@ -69,7 +69,6 @@ class TimelineTableViewController: UIViewController {
|
||||
didSet {
|
||||
// Paginate again if the threshold hasn't been satisfied.
|
||||
paginateBackwardsPublisher.send(())
|
||||
applySnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
.store(in: &cancellables)
|
||||
|
||||
slidingSyncViewProxy.diffPublisher
|
||||
.collect(.byTime(serialDispatchQueue, 0.1))
|
||||
.collect(.byTime(serialDispatchQueue, 0.025))
|
||||
.sink { [weak self] in self?.updateRoomsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.verbose("Updating \(identifiers.count) rooms")
|
||||
MXLog.info("Updating \(identifiers.count) rooms")
|
||||
|
||||
guard statePublisher.value == .live else {
|
||||
MXLog.warning("Sliding sync not live yet, ignoring update.")
|
||||
@@ -117,7 +117,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
}
|
||||
|
||||
fileprivate func updateRoomsWithDiffs(_ diffs: [SlidingSyncViewRoomsListDiff]) {
|
||||
MXLog.verbose("Received \(diffs.count) diffs")
|
||||
MXLog.info("Received \(diffs.count) diffs")
|
||||
|
||||
rooms = diffs
|
||||
.reduce(rooms) { currentItems, diff in
|
||||
@@ -136,7 +136,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
return updatedItems
|
||||
}
|
||||
|
||||
MXLog.verbose("Finished applying \(diffs.count) diffs")
|
||||
MXLog.info("Finished applying \(diffs.count) diffs")
|
||||
}
|
||||
|
||||
private func buildRoomSummaryForIdentifier(_ identifier: String, invalidated: Bool) -> RoomSummary {
|
||||
|
||||
@@ -29,6 +29,7 @@ private class RoomTimelineListener: TimelineListener {
|
||||
class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
|
||||
let itemsPublisher = CurrentValueSubject<[TimelineItemProxy], Never>([])
|
||||
|
||||
@@ -40,6 +41,7 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider")
|
||||
itemProxies = []
|
||||
|
||||
Task {
|
||||
@@ -47,8 +49,8 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
|
||||
roomTimelineListener
|
||||
.itemsUpdatePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in self?.updateItemsWithDiff($0) }
|
||||
.collect(.byTime(serialDispatchQueue, 0.025))
|
||||
.sink { [weak self] in self?.updateItemsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
switch await roomProxy.addTimelineListener(listener: roomTimelineListener) {
|
||||
@@ -63,22 +65,25 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateItemsWithDiff(_ diff: TimelineDiff) {
|
||||
private func updateItemsWithDiffs(_ diffs: [TimelineDiff]) {
|
||||
MXLog.verbose("Received timeline diff")
|
||||
|
||||
guard let collectionDiff = buildDiff(from: diff, on: itemProxies) else {
|
||||
MXLog.error("Failed building CollectionDifference from \(diff)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let updatedItems = itemProxies.applying(collectionDiff) else {
|
||||
MXLog.error("Failed applying diff: \(collectionDiff)")
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.verbose("Applied diff \(collectionDiff), new count: \(updatedItems.count)")
|
||||
|
||||
itemProxies = updatedItems
|
||||
itemProxies = diffs
|
||||
.reduce(itemProxies) { currentItems, diff in
|
||||
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
|
||||
MXLog.error("Failed building CollectionDifference from \(diff)")
|
||||
return currentItems
|
||||
}
|
||||
|
||||
guard let updatedItems = currentItems.applying(collectionDiff) else {
|
||||
MXLog.error("Failed applying diff: \(collectionDiff)")
|
||||
return currentItems
|
||||
}
|
||||
|
||||
MXLog.verbose("Applied diff \(collectionDiff), new count: \(updatedItems.count)")
|
||||
|
||||
return updatedItems
|
||||
}
|
||||
|
||||
MXLog.verbose("Finished applying diff")
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@ import UniformTypeIdentifiers
|
||||
|
||||
class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let userId: String
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let timelineProvider: RoomTimelineProviderProtocol
|
||||
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
let roomProxy: RoomProxyProtocol
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var timelineItemsUpdateTask: Task<Void, Never>? {
|
||||
@@ -51,10 +52,11 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
self.mediaProvider = mediaProvider
|
||||
self.roomProxy = roomProxy
|
||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider")
|
||||
|
||||
self.timelineProvider
|
||||
.itemsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.receive(on: serialDispatchQueue)
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -236,14 +238,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
updateTimelineItems()
|
||||
}
|
||||
|
||||
private func updateTimelineItems() {
|
||||
timelineItemsUpdateTask = Task {
|
||||
await asyncUpdateTimelineItems()
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func asyncUpdateTimelineItems() async {
|
||||
private func updateTimelineItems() {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
var canBackPaginate = true
|
||||
var isBackPaginating = false
|
||||
|
||||
Reference in New Issue
Block a user