Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check Performance Degration Root Cause #3348

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## StreamChat
### ⚡ Performance
- Improve performance of `ChatChannel` database model conversions more than 7 times [#3325](https://github.com/GetStream/stream-chat-swift/pull/3325)
- Improve performance of `ChatChannel` and `ChatMessage` equality checks [#3335](https://github.com/GetStream/stream-chat-swift/pull/3335)
### ✅ Added
- Expose `MissingConnectionId` + `InvalidURL` + `InvalidJSON` Errors [#3332](https://github.com/GetStream/stream-chat-swift/pull/3332)
Expand Down
101 changes: 48 additions & 53 deletions Sources/StreamChat/Database/DTOs/ChannelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -445,75 +445,70 @@ extension ChatChannel {
)
extraData = [:]
}

let sortedMessageDTOs = dto.messages.sorted(by: { $0.createdAt.bridgeDate > $1.createdAt.bridgeDate })

let reads: [ChatChannelRead] = try dto.reads.map { try $0.asModel() }

let unreadCount: ChannelUnreadCount = {
guard let currentUserDTO = context.currentUser else {
guard let currentUser = context.currentUser else {
return .noUnread
}
let currentUserRead = reads.first(where: { $0.user.id == currentUserDTO.user.id })

let currentUserRead = reads.first(where: { $0.user.id == currentUser.user.id })

let allUnreadMessages = currentUserRead?.unreadMessagesCount ?? 0
// Therefore, no unread messages with mentions and we can skip the fetch
if allUnreadMessages == 0 {

// Fetch count of all mentioned messages after last read
// (this is not 100% accurate but it's the best we have)
let unreadMentionsRequest = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
unreadMentionsRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
MessageDTO.channelMessagesPredicate(
for: dto.cid,
deletedMessagesVisibility: context.deletedMessagesVisibility ?? .visibleForCurrentUser,
shouldShowShadowedMessages: context.shouldShowShadowedMessages ?? false
),
NSPredicate(format: "createdAt > %@", currentUserRead?.lastReadAt.bridgeDate ?? DBDate(timeIntervalSince1970: 0)),
NSPredicate(format: "%@ IN mentionedUsers", currentUser.user)
])

do {
return ChannelUnreadCount(
messages: allUnreadMessages,
mentions: try context.count(for: unreadMentionsRequest)
)
} catch {
log.error("Failed to fetch unread counts for channel `\(cid)`. Error: \(error)")
return .noUnread
}
let unreadMentionsCount = sortedMessageDTOs
.prefix(allUnreadMessages)
.filter { $0.mentionedUsers.contains(currentUserDTO.user) }
.count
return ChannelUnreadCount(
messages: allUnreadMessages,
mentions: unreadMentionsCount
)
}()

let latestMessages: [ChatMessage] = {
var messages = sortedMessageDTOs
.prefix(dto.managedObjectContext?.localCachingSettings?.chatChannel.latestMessagesLimit ?? 25)
let messages: [ChatMessage] = {
MessageDTO
.load(
for: dto.cid,
limit: dto.managedObjectContext?.localCachingSettings?.chatChannel.latestMessagesLimit ?? 25,
deletedMessagesVisibility: dto.managedObjectContext?.deletedMessagesVisibility ?? .visibleForCurrentUser,
shouldShowShadowedMessages: dto.managedObjectContext?.shouldShowShadowedMessages ?? false,
context: context
)
.compactMap { try? $0.relationshipAsModel(depth: depth) }
if let oldest = dto.oldestMessageAt?.bridgeDate {
messages = messages.filter { $0.createdAt >= oldest }
}
if let truncated = dto.truncatedAt?.bridgeDate {
messages = messages.filter { $0.createdAt >= truncated }
}
return messages
}()

let latestMessageFromUser: ChatMessage? = {
guard let currentUserId = context.currentUser?.user.id else { return nil }
return try? sortedMessageDTOs
.first(where: { messageDTO in
guard messageDTO.user.id == currentUserId else { return false }
guard messageDTO.localMessageState == nil else { return false }
return messageDTO.type != MessageType.ephemeral.rawValue
})?
guard let currentUser = context.currentUser else { return nil }

return try? MessageDTO
.loadLastMessage(
from: currentUser.user.id,
in: dto.cid,
context: context
)?
.relationshipAsModel(depth: depth)
}()

let watchers = dto.watchers
.sorted { lhs, rhs in
let lhsActivity = lhs.lastActivityAt?.bridgeDate ?? .distantPast
let rhsActivity = rhs.lastActivityAt?.bridgeDate ?? .distantPast
if lhsActivity == rhsActivity {
return lhs.id > rhs.id
}
return lhsActivity > rhsActivity
}
.prefix(context.localCachingSettings?.chatChannel.lastActiveWatchersLimit ?? 100)

let watchers = UserDTO.loadLastActiveWatchers(cid: cid, context: context)
.compactMap { try? $0.asModel() }

let members = dto.members
.sorted { lhs, rhs in
let lhsActivity = lhs.user.lastActivityAt?.bridgeDate ?? .distantPast
let rhsActivity = rhs.user.lastActivityAt?.bridgeDate ?? .distantPast
if lhsActivity == rhsActivity {
return lhs.id > rhs.id
}
return lhsActivity > rhsActivity
}
.prefix(context.localCachingSettings?.chatChannel.lastActiveMembersLimit ?? 100)
let members = MemberDTO.loadLastActiveMembers(cid: cid, context: context)
.compactMap { try? $0.asModel() }

let muteDetails: MuteDetails? = {
Expand Down Expand Up @@ -555,7 +550,7 @@ extension ChatChannel {
reads: reads,
cooldownDuration: Int(dto.cooldownDuration),
extraData: extraData,
latestMessages: latestMessages,
latestMessages: messages,
lastMessageFromCurrentUser: latestMessageFromUser,
pinnedMessages: pinnedMessages,
muteDetails: muteDetails,
Expand Down
11 changes: 11 additions & 0 deletions Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ extension MemberDTO {
new.id = memberId
return new
}

static func loadLastActiveMembers(cid: ChannelId, context: NSManagedObjectContext) -> [MemberDTO] {
let request = NSFetchRequest<MemberDTO>(entityName: MemberDTO.entityName)
request.predicate = NSPredicate(format: "channel.cid == %@", cid.rawValue)
request.sortDescriptors = [
ChannelMemberListSortingKey.lastActiveSortDescriptor,
ChannelMemberListSortingKey.defaultSortDescriptor
]
request.fetchLimit = context.localCachingSettings?.chatChannel.lastActiveMembersLimit ?? 100
return load(by: request, context: context)
}
}

extension NSManagedObjectContext {
Expand Down
46 changes: 25 additions & 21 deletions Sources/StreamChat/Database/DTOs/MessageDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,19 @@ class MessageDTO: NSManagedObject {
return (try? context.count(for: request)) ?? 0
}

static func loadLastMessage(from userId: String, in cid: String, context: NSManagedObjectContext) -> MessageDTO? {
let request = NSFetchRequest<MessageDTO>(entityName: entityName)
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
channelPredicate(with: cid),
.init(format: "user.id == %@", userId),
.init(format: "type != %@", MessageType.ephemeral.rawValue),
messageSentPredicate()
])
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.createdAt, ascending: false)]
request.fetchLimit = 1
return load(by: request, context: context).first
}

static func loadSendingMessages(context: NSManagedObjectContext) -> [MessageDTO] {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.locallyCreatedAt, ascending: false)]
Expand Down Expand Up @@ -1296,28 +1309,21 @@ private extension ChatMessage {

if let currentUser = context.currentUser {
isSentByCurrentUser = currentUser.user.id == dto.user.id
if !dto.ownReactions.isEmpty {
currentUserReactions = Set(
MessageReactionDTO
.loadReactions(ids: dto.ownReactions, context: context)
.compactMap { try? $0.asModel() }
)
} else {
currentUserReactions = []
}
currentUserReactions = Set(
MessageReactionDTO
.loadReactions(ids: dto.ownReactions, context: context)
.compactMap { try? $0.asModel() }
)
} else {
isSentByCurrentUser = false
currentUserReactions = []
}

latestReactions = {
guard !dto.latestReactions.isEmpty else { return Set() }
return Set(
MessageReactionDTO
.loadReactions(ids: dto.latestReactions, context: context)
.compactMap { try? $0.asModel() }
)
}()
latestReactions = Set(
MessageReactionDTO
.loadReactions(ids: dto.latestReactions, context: context)
.compactMap { try? $0.asModel() }
)

threadParticipants = dto.threadParticipants.array
.compactMap { $0 as? UserDTO }
Expand All @@ -1331,10 +1337,8 @@ private extension ChatMessage {
.sorted { $0.id.index < $1.id.index }

latestReplies = {
guard dto.replyCount > 0 else { return [] }
return dto.replies
.sorted(by: { $0.createdAt.bridgeDate > $1.createdAt.bridgeDate })
.prefix(5)
guard !dto.replies.isEmpty else { return [] }
return MessageDTO.loadReplies(for: dto.id, limit: 5, context: context)
.compactMap { try? ChatMessage(fromDTO: $0, depth: depth) }
}()

Expand Down
11 changes: 11 additions & 0 deletions Sources/StreamChat/Database/DTOs/UserDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ extension UserDTO {
new.teams = []
return new
}

static func loadLastActiveWatchers(cid: ChannelId, context: NSManagedObjectContext) -> [UserDTO] {
let request = NSFetchRequest<UserDTO>(entityName: UserDTO.entityName)
request.sortDescriptors = [
UserListSortingKey.lastActiveSortDescriptor,
UserListSortingKey.defaultSortDescriptor
]
request.predicate = NSPredicate(format: "ANY watchedChannels.cid == %@", cid.rawValue)
request.fetchLimit = context.localCachingSettings?.chatChannel.lastActiveWatchersLimit ?? 100
return load(by: request, context: context)
}
}

extension NSManagedObjectContext: UserDatabaseSession {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ extension ChannelMemberListSortingKey {
return .init(keyPath: dateKeyPath, ascending: false)
}()

static let lastActiveSortDescriptor: NSSortDescriptor = {
let dateKeyPath: KeyPath<MemberDTO, DBDate?> = \MemberDTO.user.lastActivityAt
return .init(keyPath: dateKeyPath, ascending: false)
}()

func sortDescriptor(isAscending: Bool) -> NSSortDescriptor {
.init(key: rawValue, ascending: isAscending)
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/StreamChat/Query/Sorting/UserListSortingKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ extension UserListSortingKey {
return .init(keyPath: stringKeyPath, ascending: false)
}()

static let lastActiveSortDescriptor: NSSortDescriptor = {
let dateKeyPath: KeyPath<UserDTO, DBDate?> = \UserDTO.lastActivityAt
return .init(keyPath: dateKeyPath, ascending: false)
}()

func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? {
.init(key: rawValue, ascending: isAscending)
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1322,7 +1322,7 @@ final class ChannelDTO_Tests: XCTestCase {
XCTAssertEqual(channel.unreadCount.messages, 0)
}

func test_asModel_populatesLatestMessage_withoutFilteringDeletedMessages() throws {
func test_asModel_populatesLatestMessage() throws {
// GIVEN
database = DatabaseContainer_Spy(
kind: .inMemory,
Expand Down Expand Up @@ -1411,7 +1411,7 @@ final class ChannelDTO_Tests: XCTestCase {
// THEN
XCTAssertEqual(
Set(channel.latestMessages.map(\.id)),
Set([message1.id, deletedMessageFromCurrentUser.id, deletedMessageFromAnotherUser.id])
Set([message1.id, deletedMessageFromCurrentUser.id, shadowedMessageFromAnotherUser.id])
)
}

Expand Down
Loading