diff --git a/Signal/src/Models/ThreadMapping.swift b/Signal/src/Models/ThreadMapping.swift index 57952171c1..50f1c766ec 100644 --- a/Signal/src/Models/ThreadMapping.swift +++ b/Signal/src/Models/ThreadMapping.swift @@ -86,6 +86,14 @@ public class ThreadMappingDiff: NSObject { @objc class ThreadMapping: NSObject { + // MARK: - Dependencies + + private var threadReadCache: ThreadReadCache { + SSKEnvironment.shared.modelReadCaches.threadReadCache + } + + // MARK: - + private var threads: [TSThread] = [] private let kSection: Int = ConversationListViewControllerSection.conversations.rawValue @@ -169,12 +177,76 @@ class ThreadMapping: NSObject { try Bench(title: "update thread mapping (\(isViewingArchive ? "archive" : "inbox"))") { archiveCount = try threadFinder.visibleThreadCount(isArchived: true, transaction: transaction) inboxCount = try threadFinder.visibleThreadCount(isArchived: false, transaction: transaction) + threads = try self.loadThreads(isViewingArchive: isViewingArchive, transaction: transaction) + } + } + + private let shouldUseModelCache = false + + private func loadThreads(isViewingArchive: Bool, transaction: SDSAnyReadTransaction) throws -> [TSThread] { + + // This method is a perf hotspot. To improve perf, we try to leverage + // the model cache. If any problems arise, we fall back to using + // threadFinder.enumerateVisibleThreads() which is robust but expensive. + let loadWithoutCache: () throws -> [TSThread] = { var newThreads: [TSThread] = [] - try threadFinder.enumerateVisibleThreads(isArchived: isViewingArchive, transaction: transaction) { thread in + try self.threadFinder.enumerateVisibleThreads(isArchived: isViewingArchive, transaction: transaction) { thread in newThreads.append(thread) } - threads = newThreads + return newThreads } + + guard shouldUseModelCache else { + return try loadWithoutCache() + } + + // Loading the mapping from the cache has the following steps: + // + // 1. Fetch the uniqueIds for the visible threads. + let threadIds = try threadFinder.visibleThreadIds(isArchived: isViewingArchive, transaction: transaction) + guard !threadIds.isEmpty else { + return [] + } + + // 2. Try to pull as many threads as possible from the cache. + var threadIdToModelMap: [String: TSThread] = threadReadCache.getThreadsIfInCache(forUniqueIds: threadIds, + transaction: transaction) + var threadsToLoad = Set(threadIds) + threadsToLoad.subtract(threadIdToModelMap.keys) + + // 3. Bulk load any threads that are not in the cache in a + // single query. + // + // NOTE: There's an upper bound on how long SQL queries should be. + // We use kMaxIncrementalRowChanges to limit query size. + guard threadsToLoad.count <= UIDatabaseObserver.kMaxIncrementalRowChanges else { + return try loadWithoutCache() + } + if !threadsToLoad.isEmpty { + let loadedThreads = try threadFinder.threads(withThreadIds: threadsToLoad, transaction: transaction) + guard loadedThreads.count == threadsToLoad.count else { + owsFailDebug("Loading threads failed.") + return try loadWithoutCache() + } + for thread in loadedThreads { + threadIdToModelMap[thread.uniqueId] = thread + } + } + guard threadIds.count == threadIdToModelMap.count else { + owsFailDebug("Missing threads.") + return try loadWithoutCache() + } + + // 4. Build the ordered list of threads. + var threads = [TSThread]() + for threadId in threadIds { + guard let thread = threadIdToModelMap[threadId] else { + owsFailDebug("Couldn't read thread: \(threadId)") + return try loadWithoutCache() + } + threads.append(thread) + } + return threads } @objc @@ -318,6 +390,8 @@ class ThreadMapping: NSObject { } // Once the moves are complete, the new ordering should be correct. guard newThreadIds == naiveThreadIdOrdering else { + Logger.verbose("newThreadIds: \(newThreadIds)") + Logger.verbose("naiveThreadIdOrdering: \(naiveThreadIdOrdering)") throw OWSAssertionError("Could not reorder contents.") } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationMessageMapping.swift b/Signal/src/ViewControllers/ConversationView/ConversationMessageMapping.swift index 2832f604ef..611ea31dd7 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationMessageMapping.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationMessageMapping.swift @@ -7,6 +7,14 @@ import Foundation @objc public class ConversationMessageMapping: NSObject { + // MARK: - Dependencies + + private var interactionReadCache: InteractionReadCache { + SSKEnvironment.shared.modelReadCaches.interactionReadCache + } + + // MARK: - + private let interactionFinder: InteractionFinder @objc @@ -269,12 +277,74 @@ public class ConversationMessageMapping: NSObject { Logger.verbose("canLoadOlder: \(canLoadOlder) canLoadNewer: \(canLoadNewer)") } + private let shouldUseModelCache = true + private func fetchInteractions(nsRange: NSRange, transaction: SDSAnyReadTransaction) throws -> [TSInteraction] { - var newItems: [TSInteraction] = [] - try interactionFinder.enumerateInteractions(range: nsRange, transaction: transaction) { (interaction: TSInteraction, _) in - newItems.append(interaction) + + // This method is a perf hotspot. To improve perf, we try to leverage + // the model cache. If any problems arise, we fall back to using + // interactionFinder.enumerateInteractions() which is robust but expensive. + let loadWithoutCache: () throws -> [TSInteraction] = { + + var newItems: [TSInteraction] = [] + try self.interactionFinder.enumerateInteractions(range: nsRange, transaction: transaction) { (interaction: TSInteraction, _) in + newItems.append(interaction) + } + return newItems } - return newItems + + guard shouldUseModelCache else { + return try loadWithoutCache() + } + + // Loading the mapping from the cache has the following steps: + // + // 1. Fetch the uniqueIds for the interactions in the load window/mapping. + let interactionIds = try interactionFinder.interactionIds(inRange: nsRange, transaction: transaction) + guard !interactionIds.isEmpty else { + return [] + } + + // 2. Try to pull as many interactions as possible from the cache. + var interactionIdToModelMap: [String: TSInteraction] = interactionReadCache.getInteractionsIfInCache(forUniqueIds: interactionIds, + transaction: transaction) + var interactionsToLoad = Set(interactionIds) + interactionsToLoad.subtract(interactionIdToModelMap.keys) + let cachedCount = interactionIdToModelMap.count + + // 3. Bulk load any interactions that are not in the cache in a + // single query. + // + // NOTE: There's an upper bound on how long SQL queries should be. + // We use kMaxIncrementalRowChanges to limit query size. + guard interactionsToLoad.count <= UIDatabaseObserver.kMaxIncrementalRowChanges else { + return try loadWithoutCache() + } + if !interactionsToLoad.isEmpty { + let loadedInteractions = InteractionFinder.interactions(withInteractionIds: interactionsToLoad, transaction: transaction) + guard loadedInteractions.count == interactionsToLoad.count else { + owsFailDebug("Loading interactions failed.") + return try loadWithoutCache() + } + for interaction in loadedInteractions { + interactionIdToModelMap[interaction.uniqueId] = interaction + } + } + guard interactionIds.count == interactionIdToModelMap.count else { + owsFailDebug("Missing interactions.") + return try loadWithoutCache() + } + + // 4. Build the ordered list of interactions. + var interactions = [TSInteraction]() + for interactionId in interactionIds { + guard let interaction = interactionIdToModelMap[interactionId] else { + owsFailDebug("Couldn't read interaction: \(interactionId)") + return try loadWithoutCache() + } + interactions.append(interaction) + } + return interactions } @objc diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index 99de130e16..3e6538a5e2 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -64,6 +64,11 @@ ConversationColorName const ConversationColorNameDefault = ConversationColorName return SSKEnvironment.shared.tsAccountManager; } +- (ThreadReadCache *)threadReadCache +{ + return SSKEnvironment.shared.modelReadCaches.threadReadCache; +} + #pragma mark - + (NSString *)collection { @@ -196,6 +201,22 @@ ConversationColorName const ConversationColorNameDefault = ConversationColorName if (self.shouldThreadBeVisible && ![SSKPreferences hasSavedThreadWithTransaction:transaction]) { [SSKPreferences setHasSavedThread:YES transaction:transaction]; } + + [self.threadReadCache didInsertOrUpdateThread:self transaction:transaction]; +} + +- (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction +{ + [super anyDidUpdateWithTransaction:transaction]; + + [self.threadReadCache didInsertOrUpdateThread:self transaction:transaction]; +} + +- (void)anyDidRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction +{ + [super anyDidRemoveWithTransaction:transaction]; + + [self.threadReadCache didRemoveThread:self transaction:transaction]; } - (void)anyWillRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachment.m b/SignalServiceKit/src/Messages/Attachments/TSAttachment.m index 1b070d1947..5b435af35f 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachment.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachment.m @@ -30,6 +30,15 @@ NSUInteger const TSAttachmentSchemaVersion = 5; @implementation TSAttachment +#pragma mark - Dependencies + +- (AttachmentReadCache *)attachmentReadCache +{ + return SSKEnvironment.shared.modelReadCaches.attachmentReadCache; +} + +#pragma mark - + // This constructor is used for new instances of TSAttachmentPointer, // i.e. undownloaded incoming attachments. - (instancetype)initWithServerId:(UInt64)serverId @@ -392,6 +401,29 @@ NSUInteger const TSAttachmentSchemaVersion = 5; #pragma mark - Update With... +- (void)anyDidInsertWithTransaction:(SDSAnyWriteTransaction *)transaction +{ + [super anyDidInsertWithTransaction:transaction]; + + [self.attachmentReadCache didInsertOrUpdateAttachment:self transaction:transaction]; +} + +- (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction +{ + [super anyDidUpdateWithTransaction:transaction]; + + [self.attachmentReadCache didInsertOrUpdateAttachment:self transaction:transaction]; +} + +- (void)anyDidRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction +{ + [super anyDidRemoveWithTransaction:transaction]; + + [self.attachmentReadCache didRemoveAttachment:self transaction:transaction]; +} + +#pragma mark - Update With... + - (void)updateWithBlurHash:(NSString *)blurHash transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(blurHash.length > 0); diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m index 07dc63e61e..8c89238212 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m @@ -49,6 +49,15 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) @implementation TSInteraction +#pragma mark - Dependencies + +- (InteractionReadCache *)interactionReadCache +{ + return SSKEnvironment.shared.modelReadCaches.interactionReadCache; +} + +#pragma mark - + + (BOOL)shouldBeIndexedForFTS { return YES; @@ -327,6 +336,8 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) TSThread *fetchedThread = [self threadWithTransaction:transaction]; [fetchedThread updateWithInsertedMessage:self transaction:transaction]; + + [self.interactionReadCache didInsertOrUpdateInteraction:self transaction:transaction]; } - (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction @@ -335,6 +346,8 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) TSThread *fetchedThread = [self threadWithTransaction:transaction]; [fetchedThread updateWithUpdatedMessage:self transaction:transaction]; + + [self.interactionReadCache didInsertOrUpdateInteraction:self transaction:transaction]; } - (void)anyDidRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction @@ -345,6 +358,8 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) TSThread *fetchedThread = [self threadWithTransaction:transaction]; [fetchedThread updateWithRemovedMessage:self transaction:transaction]; } + + [self.interactionReadCache didRemoveInteraction:self transaction:transaction]; } #pragma mark - diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.h b/SignalServiceKit/src/Messages/Interactions/TSMessage.h index 8e9a21c4fa..8976b56b76 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.h @@ -2,7 +2,9 @@ // Copyright (c) 2020 Open Whisper Systems. All rights reserved. // +#import "OWSContact.h" #import "TSInteraction.h" +#import "TSQuotedMessage.h" NS_ASSUME_NONNULL_BEGIN @@ -12,7 +14,6 @@ NS_ASSUME_NONNULL_BEGIN @class GRDBReadTransaction; @class MessageSticker; -@class OWSContact; @class OWSLinkPreview; @class SDSAnyReadTransaction; @class SDSAnyWriteTransaction; diff --git a/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h b/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h index 84ff1c2739..23df30dec9 100644 --- a/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h +++ b/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h @@ -8,7 +8,6 @@ @class SDSAnyWriteTransaction; @class SignalServiceAddress; @class TSThread; -@class UserProfileReadCache; NS_ASSUME_NONNULL_BEGIN @@ -102,7 +101,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)warmCaches; -@property (nonatomic, readonly) UserProfileReadCache *userProfileReadCache; @property (nonatomic, readonly) BOOL hasProfileName; @end diff --git a/SignalServiceKit/src/Storage/Database/DeepCopy.swift b/SignalServiceKit/src/Storage/Database/DeepCopy.swift new file mode 100644 index 0000000000..0716e0e42b --- /dev/null +++ b/SignalServiceKit/src/Storage/Database/DeepCopy.swift @@ -0,0 +1,276 @@ +// +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public protocol DeepCopyable { + func deepCopy() throws -> AnyObject +} + +// MARK: - + +public class DeepCopies { + + @available(*, unavailable, message:"Do not instantiate this class.") + private init() { + } + + static func deepCopy(_ objectToCopy: T) throws -> T { + guard let newCopy = try objectToCopy.deepCopy() as? T else { + Logger.verbose("Could not copy: \(objectToCopy)") + throw OWSAssertionError("Could not copy: \(type(of: objectToCopy))") + } + return newCopy + } + + static func deepCopy(_ arrayToCopy: [T]) throws -> [T] { + return try arrayToCopy.deepCopy() + } + + static func deepCopy(_ dictToCopy: [K: V]) throws -> [K: V] { + return try dictToCopy.deepCopy() + } + + // NOTE: We get not compile-time type safety with NSOrderedSet. + static func deepCopy(_ setToCopy: NSOrderedSet) throws -> NSOrderedSet { + return try setToCopy.deepCopy() + } + + // Swift does not appear to offer a way to let Array conform + // to DeepCopyable IFF Array.Element conforms to DeepCopyable. + // This if unfortunate and poses a decision. + // + // We can makes Array generically conform to DeepCopyable, + // but then we get not compile-time type checking. + // + // Instead we create a variety of specializations of + // DeepCopies.deepCopy(). This is tedious but safer. + static func deepCopy(_ dictToCopy: [K: [V]]) throws -> [K: [V]] { + return Dictionary(uniqueKeysWithValues: try dictToCopy.map({ (key, value) in + let keyCopy: K = try DeepCopies.deepCopy(key) + let valueCopy: [V] = try value.deepCopy() + return (keyCopy, valueCopy) + })) + } + + // NOTE: We get not compile-time type safety with Any. + static func deepCopy(_ dictToCopy: [InfoMessageUserInfoKey: Any]) throws -> [InfoMessageUserInfoKey: Any] { + return Dictionary(uniqueKeysWithValues: try dictToCopy.map({ (key, value) in + let keyCopy: String = try DeepCopies.deepCopy(key.rawValue as String) + let valueCopy: AnyObject + if let objectToCopy = value as? DeepCopyable { + valueCopy = try objectToCopy.deepCopy() + } else { + Logger.verbose("Could not copy: \(value)") + throw OWSAssertionError("Could not copy: \(type(of: value))") + } + return (InfoMessageUserInfoKey(rawValue: keyCopy), valueCopy) + })) + } + + // "Cannot explicitly specialize a generic function." + fileprivate static func shallowCopy(_ objectToCopy: T) throws -> T { + guard let newCopy = objectToCopy.copy() as? T else { + Logger.verbose("Could not copy: \(objectToCopy)") + throw OWSAssertionError("Could not copy: \(type(of: objectToCopy))") + } + return newCopy + } +} + +// MARK: - + +extension Array where Element: DeepCopyable { + public func deepCopy() throws -> [Element] { + return try map { element in + return try DeepCopies.deepCopy(element) + } + } +} + +// MARK: - + +extension Dictionary where Key: DeepCopyable, Value: DeepCopyable { + public func deepCopy() throws -> [Key: Value] { + return Dictionary(uniqueKeysWithValues: try self.map({ (key, value) in + let keyCopy: Key = try DeepCopies.deepCopy(key) + let valueCopy: Value = try DeepCopies.deepCopy(value) + return (keyCopy, valueCopy) + })) + } +} + +// MARK: - + +// NOTE: We get not compile-time type safety with NSOrderedSet. +extension NSOrderedSet { + public func deepCopy() throws -> NSOrderedSet { + let copiedElements: [AnyObject] = try self.array.map { (element: Any) throws -> AnyObject in + guard let objectToCopy = element as? DeepCopyable else { + Logger.verbose("Could not copy: \(element)") + throw OWSAssertionError("Could not copy: \(type(of: element))") + } + return try objectToCopy.deepCopy() + } + return NSOrderedSet(array: copiedElements) + } +} + +// MARK: - + +extension NSNumber: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // No need to copy; NSNumber is immutable. + return self + } +} + +// MARK: - + +extension String: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self as NSString) + } +} + +// MARK: - + +@objc +extension StickerInfo: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension OWSLinkPreview: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension SignalServiceAddress: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension MessageSticker: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension TSQuotedMessage: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension OWSContact: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension StickerPackInfo: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension StickerPackItem: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension Contact: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension OWSAES256Key: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension TSGroupModel: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension PreKeyBundle: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension TSOutgoingMessageRecipientState: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} + +// MARK: - + +@objc +extension DisappearingMessageToken: DeepCopyable { + public func deepCopy() throws -> AnyObject { + // This class can use shallow copies. + return try DeepCopies.shallowCopy(self) + } +} diff --git a/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift b/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift index d044329c5c..7d6494ad63 100644 --- a/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift +++ b/SignalServiceKit/src/Storage/Database/Records/InteractionFinder.swift @@ -27,6 +27,8 @@ protocol InteractionFinderAdapter { static func enumerateMessagesWhichFailedToStartExpiring(transaction: ReadTransaction, block: @escaping (TSMessage, UnsafeMutablePointer) -> Void) + static func interactions(withInteractionIds interactionIds: Set, transaction: ReadTransaction) -> Set + // MARK: - instance methods func mostRecentInteractionForInbox(transaction: ReadTransaction) -> TSInteraction? @@ -38,6 +40,7 @@ protocol InteractionFinderAdapter { func enumerateInteractionIds(transaction: ReadTransaction, block: @escaping (String, UnsafeMutablePointer) throws -> Void) throws func enumerateRecentInteractions(transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws func enumerateInteractions(range: NSRange, transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer) -> Void) throws + func interactionIds(inRange range: NSRange, transaction: ReadTransaction) throws -> [String] func existsOutgoingMessage(transaction: ReadTransaction) -> Bool func outgoingMessageCount(transaction: ReadTransaction) -> UInt @@ -179,6 +182,16 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { } } + @objc + public class func interactions(withInteractionIds interactionIds: Set, transaction: SDSAnyReadTransaction) -> Set { + switch transaction.readTransaction { + case .yapRead(let yapRead): + return YAPDBInteractionFinderAdapter.interactions(withInteractionIds: interactionIds, transaction: yapRead) + case .grdbRead(let grdbRead): + return GRDBInteractionFinder.interactions(withInteractionIds: interactionIds, transaction: grdbRead) + } + } + @objc public class func findMessage( withTimestamp timestamp: UInt64, @@ -340,6 +353,16 @@ public class InteractionFinder: NSObject, InteractionFinderAdapter { } } + public func interactionIds(inRange range: NSRange, transaction: SDSAnyReadTransaction) throws -> [String] { + switch transaction.readTransaction { + case .yapRead(let yapRead): + owsFailDebug("Invalid transaction.") + return try yapAdapter.interactionIds(inRange: range, transaction: yapRead) + case .grdbRead(let grdbRead): + return try grdbAdapter.interactionIds(inRange: range, transaction: grdbRead) + } + } + /// Returns all the unread interactions in this thread @objc public func allUnreadMessages(transaction: GRDBReadTransaction) -> [OWSReadTracking] { @@ -583,6 +606,10 @@ struct YAPDBInteractionFinderAdapter: InteractionFinderAdapter { OWSDisappearingMessagesFinder.ydb_enumerateMessagesWhichFailedToStartExpiring(block, transaction: transaction) } + static func interactions(withInteractionIds interactionIds: Set, transaction: YapDatabaseReadTransaction) -> Set { + owsFail("Not implemented.") + } + // MARK: - instance methods func mostRecentInteractionForInbox(transaction: YapDatabaseReadTransaction) -> TSInteraction? { @@ -691,6 +718,11 @@ struct YAPDBInteractionFinderAdapter: InteractionFinderAdapter { } } + public func interactionIds(inRange range: NSRange, transaction: YapDatabaseReadTransaction) throws -> [String] { + owsFailDebug("Invalid transaction.") + return [] + } + func interaction(at index: UInt, transaction: YapDatabaseReadTransaction) -> TSInteraction? { guard let view = interactionExt(transaction) else { return nil @@ -929,6 +961,28 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { } } + static func interactions(withInteractionIds interactionIds: Set, transaction: GRDBReadTransaction) -> Set { + guard !interactionIds.isEmpty else { + return [] + } + + let sql = """ + SELECT * FROM \(InteractionRecord.databaseTableName) + WHERE \(interactionColumn: .uniqueId) IN (\(interactionIds.map { "\'\($0)'" }.joined(separator: ","))) + """ + let arguments: StatementArguments = [] + let cursor = TSInteraction.grdbFetchCursor(sql: sql, arguments: arguments, transaction: transaction) + var interactions = Set() + do { + while let interaction = try cursor.next() { + interactions.insert(interaction) + } + } catch { + owsFailDebug("unexpected error \(error)") + } + return interactions + } + // MARK: - instance methods func mostRecentInteractionForInbox(transaction: GRDBReadTransaction) -> TSInteraction? { @@ -1056,6 +1110,21 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter { } } + func interactionIds(inRange range: NSRange, transaction: GRDBReadTransaction) throws -> [String] { + let sql = """ + SELECT \(interactionColumn: .uniqueId) + FROM \(InteractionRecord.databaseTableName) + WHERE \(interactionColumn: .threadUniqueId) = ? + ORDER BY \(interactionColumn: .id) + LIMIT \(range.length) + OFFSET \(range.location) + """ + let arguments: StatementArguments = [threadUniqueId] + return try String.fetchAll(transaction.database, + sql: sql, + arguments: arguments) + } + @objc func enumerateMessagesWithAttachments(transaction: GRDBReadTransaction, block: @escaping (TSMessage, UnsafeMutablePointer) -> Void) throws { diff --git a/SignalServiceKit/src/Storage/Database/Records/ThreadFinder.swift b/SignalServiceKit/src/Storage/Database/Records/ThreadFinder.swift index d36d07d6de..b37a2ce78e 100644 --- a/SignalServiceKit/src/Storage/Database/Records/ThreadFinder.swift +++ b/SignalServiceKit/src/Storage/Database/Records/ThreadFinder.swift @@ -10,7 +10,9 @@ public protocol ThreadFinder { func visibleThreadCount(isArchived: Bool, transaction: ReadTransaction) throws -> UInt func enumerateVisibleThreads(isArchived: Bool, transaction: ReadTransaction, block: @escaping (TSThread) -> Void) throws + func visibleThreadIds(isArchived: Bool, transaction: ReadTransaction) throws -> [String] func sortIndex(thread: TSThread, transaction: ReadTransaction) throws -> UInt? + func threads(withThreadIds threadIds: Set, transaction: ReadTransaction) throws -> Set } @objc @@ -39,6 +41,16 @@ public class AnyThreadFinder: NSObject, ThreadFinder { } } + @objc + public func visibleThreadIds(isArchived: Bool, transaction: SDSAnyReadTransaction) throws -> [String] { + switch transaction.readTransaction { + case .grdbRead(let grdb): + return try grdbAdapter.visibleThreadIds(isArchived: isArchived, transaction: grdb) + case .yapRead(let yap): + return try yapAdapter.visibleThreadIds(isArchived: isArchived, transaction: yap) + } + } + @objc public func sortIndexObjc(thread: TSThread, transaction: ReadTransaction) -> NSNumber? { do { @@ -60,6 +72,15 @@ public class AnyThreadFinder: NSObject, ThreadFinder { return yapAdapter.sortIndex(thread: thread, transaction: yap) } } + + public func threads(withThreadIds threadIds: Set, transaction: SDSAnyReadTransaction) throws -> Set { + switch transaction.readTransaction { + case .grdbRead(let grdb): + return try grdbAdapter.threads(withThreadIds: threadIds, transaction: grdb) + case .yapRead(let yap): + return try yapAdapter.threads(withThreadIds: threadIds, transaction: yap) + } + } } struct YAPDBThreadFinder: ThreadFinder { @@ -87,6 +108,10 @@ struct YAPDBThreadFinder: ThreadFinder { } } + func visibleThreadIds(isArchived: Bool, transaction: YapDatabaseReadTransaction) throws -> [String] { + throw OWSAssertionError("Not implemented.") + } + func sortIndex(thread: TSThread, transaction: YapDatabaseReadTransaction) -> UInt? { guard let view = ext(transaction) else { owsFailDebug("view was unexpectedly nil") @@ -117,6 +142,10 @@ struct YAPDBThreadFinder: ThreadFinder { } } + func threads(withThreadIds threadIds: Set, transaction: YapDatabaseReadTransaction) throws -> Set { + throw OWSAssertionError("Not implemented.") + } + // MARK: - private static let extensionName: String = TSThreadDatabaseViewExtensionName @@ -170,6 +199,21 @@ public class GRDBThreadFinder: NSObject, ThreadFinder { } } + @objc + public func visibleThreadIds(isArchived: Bool, transaction: GRDBReadTransaction) throws -> [String] { + let sql = """ + SELECT \(threadColumn: .uniqueId) + FROM \(ThreadRecord.databaseTableName) + WHERE \(threadColumn: .shouldThreadBeVisible) = 1 + AND \(threadColumn: .isArchived) = ? + ORDER BY \(threadColumn: .lastInteractionRowId) DESC + """ + let arguments: StatementArguments = [isArchived] + return try String.fetchAll(transaction.database, + sql: sql, + arguments: arguments) + } + public func sortIndex(thread: TSThread, transaction: GRDBReadTransaction) throws -> UInt? { let sql = """ SELECT sortIndex @@ -279,4 +323,23 @@ public class GRDBThreadFinder: NSObject, ThreadFinder { return true } + + @objc + public func threads(withThreadIds threadIds: Set, transaction: GRDBReadTransaction) throws -> Set { + guard !threadIds.isEmpty else { + return [] + } + + let sql = """ + SELECT * FROM \(ThreadRecord.databaseTableName) + WHERE \(threadColumn: .uniqueId) IN (\(threadIds.map { "\'\($0)'" }.joined(separator: ","))) + """ + let arguments: StatementArguments = [] + let cursor = TSThread.grdbFetchCursor(sql: sql, arguments: arguments, transaction: transaction) + var threads = Set() + while let thread = try cursor.next() { + threads.insert(thread) + } + return threads + } } diff --git a/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m b/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m index 16683036ad..35eeb42eb5 100644 --- a/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m +++ b/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m @@ -32,7 +32,6 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSFakeProfileManager @synthesize localProfileKey = _localProfileKey; -@synthesize userProfileReadCache = _userProfileReadCache; - (instancetype)init { @@ -44,7 +43,6 @@ NS_ASSUME_NONNULL_BEGIN _profileKeys = [NSMutableDictionary new]; _recipientWhitelist = [NSMutableSet new]; _threadWhitelist = [NSMutableSet new]; - _userProfileReadCache = [UserProfileReadCache new]; _stubbedUuidCapabilitiesMap = [NSMutableDictionary new]; return self; diff --git a/SignalServiceKit/src/Util/ModelReadCache.swift b/SignalServiceKit/src/Util/ModelReadCache.swift index 37e7557dc4..16f4a7d9fc 100644 --- a/SignalServiceKit/src/Util/ModelReadCache.swift +++ b/SignalServiceKit/src/Util/ModelReadCache.swift @@ -736,6 +736,201 @@ public class SignalRecipientReadCache: NSObject { // MARK: - +@objc +public class ThreadReadCache: NSObject { + typealias KeyType = NSString + typealias ValueType = TSThread + + private class Adapter: ModelCacheAdapter { + override func read(key: KeyType, transaction: SDSAnyReadTransaction) -> ValueType? { + return TSThread.anyFetch(uniqueId: key as String, + transaction: transaction, + ignoreCache: true) + } + + override func deriveKey(fromValue value: ValueType) -> KeyType { + value.uniqueId as NSString + } + + override func copy(value: ValueType) throws -> ValueType { + return try DeepCopies.deepCopy(value) + } + + override func uiReadEvacuation(databaseChanges: UIDatabaseChanges, + nsCache: NSCache>) { + // Only evacuate modified models. + for uniqueId: String in databaseChanges.threadUniqueIds { + nsCache.removeObject(forKey: uniqueId as NSString) + } + } + } + + private let cache: ModelReadCacheWrapper + + @objc + public override init() { + cache = ModelReadCacheWrapper(cacheName: "TSThread", adapter: Adapter()) + } + + @objc(getThreadForUniqueId:transaction:) + public func getThread(uniqueId: String, transaction: SDSAnyReadTransaction) -> TSThread? { + return cache.getValue(for: uniqueId as NSString, transaction: transaction) + } + + @objc(getThreadsIfInCacheForUniqueIds:transaction:) + public func getThreadsIfInCache(forUniqueIds uniqueIds: [String], transaction: SDSAnyReadTransaction) -> [String: TSThread] { + let keys: [NSString] = uniqueIds.map { $0 as NSString } + let result: [NSString: TSThread] = cache.getValuesIfInCache(for: keys, transaction: transaction) + return Dictionary(uniqueKeysWithValues: result.map({ (key, value) in + return (key as String, value) + })) + } + + @objc(didRemoveThread:transaction:) + public func didRemove(thread: TSThread, transaction: SDSAnyWriteTransaction) { + cache.didRemove(value: thread, transaction: transaction) + } + + @objc(didInsertOrUpdateThread:transaction:) + public func didInsertOrUpdate(thread: TSThread, transaction: SDSAnyWriteTransaction) { + cache.didInsertOrUpdate(value: thread, transaction: transaction) + } + + @objc + public func didReadThread(_ thread: TSThread, transaction: SDSAnyReadTransaction) { + cache.didRead(value: thread, transaction: transaction) + } +} + +// MARK: - + +@objc +public class InteractionReadCache: NSObject { + typealias KeyType = NSString + typealias ValueType = TSInteraction + + private class Adapter: ModelCacheAdapter { + override func read(key: KeyType, transaction: SDSAnyReadTransaction) -> ValueType? { + return TSInteraction.anyFetch(uniqueId: key as String, + transaction: transaction, + ignoreCache: true) + } + + override func deriveKey(fromValue value: ValueType) -> KeyType { + value.uniqueId as NSString + } + + override func copy(value: ValueType) throws -> ValueType { + return try DeepCopies.deepCopy(value) + } + + override func uiReadEvacuation(databaseChanges: UIDatabaseChanges, + nsCache: NSCache>) { + // Only evacuate modified models. + for uniqueId: String in databaseChanges.interactionUniqueIds { + nsCache.removeObject(forKey: uniqueId as NSString) + } + } + } + + private let cache: ModelReadCacheWrapper + + @objc + public override init() { + cache = ModelReadCacheWrapper(cacheName: "TSInteraction", adapter: Adapter()) + } + + @objc(getInteractionForUniqueId:transaction:) + public func getInteraction(uniqueId: String, transaction: SDSAnyReadTransaction) -> TSInteraction? { + return cache.getValue(for: uniqueId as NSString, transaction: transaction) + } + + @objc(getInteractionsIfInCacheForUniqueIds:transaction:) + public func getInteractionsIfInCache(forUniqueIds uniqueIds: [String], transaction: SDSAnyReadTransaction) -> [String: TSInteraction] { + let keys: [NSString] = uniqueIds.map { $0 as NSString } + let result: [NSString: TSInteraction] = cache.getValuesIfInCache(for: keys, transaction: transaction) + return Dictionary(uniqueKeysWithValues: result.map({ (key, value) in + return (key as String, value) + })) + } + + @objc(didRemoveInteraction:transaction:) + public func didRemove(interaction: TSInteraction, transaction: SDSAnyWriteTransaction) { + cache.didRemove(value: interaction, transaction: transaction) + } + + @objc(didInsertOrUpdateInteraction:transaction:) + public func didInsertOrUpdate(interaction: TSInteraction, transaction: SDSAnyWriteTransaction) { + cache.didInsertOrUpdate(value: interaction, transaction: transaction) + } + + @objc + public func didReadInteraction(_ interaction: TSInteraction, transaction: SDSAnyReadTransaction) { + cache.didRead(value: interaction, transaction: transaction) + } +} + +// MARK: - + +@objc +public class AttachmentReadCache: NSObject { + typealias KeyType = NSString + typealias ValueType = TSAttachment + + private class Adapter: ModelCacheAdapter { + override func read(key: KeyType, transaction: SDSAnyReadTransaction) -> ValueType? { + return TSAttachment.anyFetch(uniqueId: key as String, + transaction: transaction, + ignoreCache: true) + } + + override func deriveKey(fromValue value: ValueType) -> KeyType { + value.uniqueId as NSString + } + + override func copy(value: ValueType) throws -> ValueType { + return try DeepCopies.deepCopy(value) + } + + override func uiReadEvacuation(databaseChanges: UIDatabaseChanges, + nsCache: NSCache>) { + // Only evacuate modified models. + for uniqueId: String in databaseChanges.attachmentUniqueIds { + nsCache.removeObject(forKey: uniqueId as NSString) + } + } + } + + private let cache: ModelReadCacheWrapper + + @objc + public override init() { + cache = ModelReadCacheWrapper(cacheName: "TSAttachment", adapter: Adapter()) + } + + @objc(getAttachmentForUniqueId:transaction:) + public func getAttachment(uniqueId: String, transaction: SDSAnyReadTransaction) -> TSAttachment? { + return cache.getValue(for: uniqueId as NSString, transaction: transaction) + } + + @objc(didRemoveAttachment:transaction:) + public func didRemove(attachment: TSAttachment, transaction: SDSAnyWriteTransaction) { + cache.didRemove(value: attachment, transaction: transaction) + } + + @objc(didInsertOrUpdateAttachment:transaction:) + public func didInsertOrUpdate(attachment: TSAttachment, transaction: SDSAnyWriteTransaction) { + cache.didInsertOrUpdate(value: attachment, transaction: transaction) + } + + @objc + public func didReadAttachment(_ attachment: TSAttachment, transaction: SDSAnyReadTransaction) { + cache.didRead(value: attachment, transaction: transaction) + } +} + +// MARK: - + @objc public class ModelReadCaches: NSObject { @objc @@ -744,4 +939,10 @@ public class ModelReadCaches: NSObject { public let signalAccountReadCache = SignalAccountReadCache() @objc public let signalRecipientReadCache = SignalRecipientReadCache() + @objc + public let threadReadCache = ThreadReadCache() + @objc + public let interactionReadCache = InteractionReadCache() + @objc + public let attachmentReadCache = AttachmentReadCache() } diff --git a/SignalServiceKit/src/Util/OWSUserProfile.m b/SignalServiceKit/src/Util/OWSUserProfile.m index 306153bb6c..39aac9fa83 100644 --- a/SignalServiceKit/src/Util/OWSUserProfile.m +++ b/SignalServiceKit/src/Util/OWSUserProfile.m @@ -69,6 +69,11 @@ NSUInteger const kUserProfileSchemaVersion = 1; return SSKEnvironment.shared.storageServiceManager; } +- (UserProfileReadCache *)userProfileReadCache +{ + return SSKEnvironment.shared.modelReadCaches.userProfileReadCache; +} + #pragma mark - @synthesize avatarUrlPath = _avatarUrlPath; @@ -691,21 +696,21 @@ NSUInteger const kUserProfileSchemaVersion = 1; { [super anyDidInsertWithTransaction:transaction]; - [self.profileManager.userProfileReadCache didInsertOrUpdateUserProfile:self transaction:transaction]; + [self.userProfileReadCache didInsertOrUpdateUserProfile:self transaction:transaction]; } - (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction { [super anyDidUpdateWithTransaction:transaction]; - [self.profileManager.userProfileReadCache didInsertOrUpdateUserProfile:self transaction:transaction]; + [self.userProfileReadCache didInsertOrUpdateUserProfile:self transaction:transaction]; } - (void)anyDidRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction { [super anyDidRemoveWithTransaction:transaction]; - [self.profileManager.userProfileReadCache didRemoveUserProfile:self transaction:transaction]; + [self.userProfileReadCache didRemoveUserProfile:self transaction:transaction]; } + (void)mergeUserProfilesIfNecessaryForAddress:(SignalServiceAddress *)address diff --git a/SignalServiceKit/src/groups/GroupAccess.swift b/SignalServiceKit/src/groups/GroupAccess.swift index 9c3c5aee8b..60a76266a6 100644 --- a/SignalServiceKit/src/groups/GroupAccess.swift +++ b/SignalServiceKit/src/groups/GroupAccess.swift @@ -29,6 +29,7 @@ public enum GroupV2Access: UInt, Codable { // MARK: - +// This class is immutable. @objc public class GroupAccess: MTLModel { @objc