Cache threads, interactions, attachments. Improve model cache.

This commit is contained in:
Matthew Chen 2020-06-17 16:10:31 -03:00
parent edb703a550
commit 179ecaf258
14 changed files with 838 additions and 14 deletions

View File

@ -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.")
}

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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 -

View File

@ -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;

View File

@ -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

View File

@ -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<T: DeepCopyable>(_ 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<T: DeepCopyable>(_ arrayToCopy: [T]) throws -> [T] {
return try arrayToCopy.deepCopy()
}
static func deepCopy<K: DeepCopyable, V: DeepCopyable>(_ 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<K: DeepCopyable, V: DeepCopyable>(_ 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<T: NSObject>(_ 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)
}
}

View File

@ -27,6 +27,8 @@ protocol InteractionFinderAdapter {
static func enumerateMessagesWhichFailedToStartExpiring(transaction: ReadTransaction, block: @escaping (TSMessage, UnsafeMutablePointer<ObjCBool>) -> Void)
static func interactions(withInteractionIds interactionIds: Set<String>, transaction: ReadTransaction) -> Set<TSInteraction>
// MARK: - instance methods
func mostRecentInteractionForInbox(transaction: ReadTransaction) -> TSInteraction?
@ -38,6 +40,7 @@ protocol InteractionFinderAdapter {
func enumerateInteractionIds(transaction: ReadTransaction, block: @escaping (String, UnsafeMutablePointer<ObjCBool>) throws -> Void) throws
func enumerateRecentInteractions(transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer<ObjCBool>) -> Void) throws
func enumerateInteractions(range: NSRange, transaction: ReadTransaction, block: @escaping (TSInteraction, UnsafeMutablePointer<ObjCBool>) -> 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<String>, transaction: SDSAnyReadTransaction) -> Set<TSInteraction> {
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<String>, transaction: YapDatabaseReadTransaction) -> Set<TSInteraction> {
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<String>, transaction: GRDBReadTransaction) -> Set<TSInteraction> {
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<TSInteraction>()
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<ObjCBool>) -> Void) throws {

View File

@ -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<String>, transaction: ReadTransaction) throws -> Set<TSThread>
}
@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<String>, transaction: SDSAnyReadTransaction) throws -> Set<TSThread> {
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<String>, transaction: YapDatabaseReadTransaction) throws -> Set<TSThread> {
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<String>, transaction: GRDBReadTransaction) throws -> Set<TSThread> {
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<TSThread>()
while let thread = try cursor.next() {
threads.insert(thread)
}
return threads
}
}

View File

@ -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;

View File

@ -736,6 +736,201 @@ public class SignalRecipientReadCache: NSObject {
// MARK: -
@objc
public class ThreadReadCache: NSObject {
typealias KeyType = NSString
typealias ValueType = TSThread
private class Adapter: ModelCacheAdapter<KeyType, ValueType> {
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<KeyType, ModelCacheValueBox<ValueType>>) {
// Only evacuate modified models.
for uniqueId: String in databaseChanges.threadUniqueIds {
nsCache.removeObject(forKey: uniqueId as NSString)
}
}
}
private let cache: ModelReadCacheWrapper<KeyType, ValueType>
@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<KeyType, ValueType> {
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<KeyType, ModelCacheValueBox<ValueType>>) {
// Only evacuate modified models.
for uniqueId: String in databaseChanges.interactionUniqueIds {
nsCache.removeObject(forKey: uniqueId as NSString)
}
}
}
private let cache: ModelReadCacheWrapper<KeyType, ValueType>
@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<KeyType, ValueType> {
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<KeyType, ModelCacheValueBox<ValueType>>) {
// Only evacuate modified models.
for uniqueId: String in databaseChanges.attachmentUniqueIds {
nsCache.removeObject(forKey: uniqueId as NSString)
}
}
}
private let cache: ModelReadCacheWrapper<KeyType, ValueType>
@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()
}

View File

@ -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

View File

@ -29,6 +29,7 @@ public enum GroupV2Access: UInt, Codable {
// MARK: -
// This class is immutable.
@objc
public class GroupAccess: MTLModel {
@objc