Performance improvements to import performance for messages
This commit is contained in:
parent
ebabb78339
commit
96a76065df
@ -1760,8 +1760,8 @@ extension %sRecord {
|
||||
|
||||
// This defines all of the columns used in the table
|
||||
// where this model (and any subclasses) are persisted.
|
||||
internal func asArguments() -> StatementArguments {
|
||||
let databaseValues: [DatabaseValueConvertible?] = [
|
||||
internal func asValues() -> [DatabaseValueConvertible?] {
|
||||
return [
|
||||
""" % str(
|
||||
remove_prefix_from_class_name(clazz.name)
|
||||
)
|
||||
@ -1784,7 +1784,10 @@ extension %sRecord {
|
||||
|
||||
swift_body += """
|
||||
]
|
||||
return StatementArguments(databaseValues)
|
||||
}
|
||||
|
||||
internal func asArguments() -> StatementArguments {
|
||||
return StatementArguments(asValues())
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@ -813,7 +813,6 @@
|
||||
66232AD92CB9D00400AE6A76 /* MessageBackupThreadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66232AD82CB9D00000AE6A76 /* MessageBackupThreadStore.swift */; };
|
||||
66232ADB2CB9E33600AE6A76 /* MessageBackupStoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66232ADA2CB9E32F00AE6A76 /* MessageBackupStoryStore.swift */; };
|
||||
66232AE12CC0272900AE6A76 /* MessageBackupFullTextSearchIndexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66232AE02CC0271F00AE6A76 /* MessageBackupFullTextSearchIndexer.swift */; };
|
||||
662590BB2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590BA2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift */; };
|
||||
662590BF2B56ECA8001FDCDD /* MessageBackupGroupUpdateMessageArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590BE2B56ECA8001FDCDD /* MessageBackupGroupUpdateMessageArchiver.swift */; };
|
||||
662590CB2B5994D2001FDCDD /* MessageBackupGroupUpdateProtoToSwiftConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590CA2B5994D2001FDCDD /* MessageBackupGroupUpdateProtoToSwiftConverter.swift */; };
|
||||
662590CD2B5994DC001FDCDD /* MessageBackupGroupUpdateSwiftToProtoConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662590CC2B5994DC001FDCDD /* MessageBackupGroupUpdateSwiftToProtoConverter.swift */; };
|
||||
@ -2452,6 +2451,8 @@
|
||||
D93EDC042AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */; };
|
||||
D941863C2ACE252D002FE2D3 /* CallRecordLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941863B2ACE252D002FE2D3 /* CallRecordLogger.swift */; };
|
||||
D943F3EF2892F89B008C0C8B /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D943F3EE2892F89B008C0C8B /* NSELogger.swift */; };
|
||||
D94441312D55956B005B2A54 /* UUIDv7.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441302D559567005B2A54 /* UUIDv7.swift */; };
|
||||
D94441332D559C6F005B2A54 /* UUIDv7Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441322D559C6B005B2A54 /* UUIDv7Test.swift */; };
|
||||
D945319E2CE53CEB004DAB30 /* SubscriptionRedemptionNecessityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */; };
|
||||
D9495A6D2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */; };
|
||||
D9495A702C76965600843BC1 /* TSOutgoingMessageRecipientStateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6E2C76963F00843BC1 /* TSOutgoingMessageRecipientStateTest.swift */; };
|
||||
@ -4634,7 +4635,6 @@
|
||||
66232AD82CB9D00000AE6A76 /* MessageBackupThreadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupThreadStore.swift; sourceTree = "<group>"; };
|
||||
66232ADA2CB9E32F00AE6A76 /* MessageBackupStoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupStoryStore.swift; sourceTree = "<group>"; };
|
||||
66232AE02CC0271F00AE6A76 /* MessageBackupFullTextSearchIndexer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupFullTextSearchIndexer.swift; sourceTree = "<group>"; };
|
||||
662590BA2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInfoMessageInserterBackupHelper.swift; sourceTree = "<group>"; };
|
||||
662590BE2B56ECA8001FDCDD /* MessageBackupGroupUpdateMessageArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupGroupUpdateMessageArchiver.swift; sourceTree = "<group>"; };
|
||||
662590CA2B5994D2001FDCDD /* MessageBackupGroupUpdateProtoToSwiftConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupGroupUpdateProtoToSwiftConverter.swift; sourceTree = "<group>"; };
|
||||
662590CC2B5994DC001FDCDD /* MessageBackupGroupUpdateSwiftToProtoConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupGroupUpdateSwiftToProtoConverter.swift; sourceTree = "<group>"; };
|
||||
@ -6281,6 +6281,8 @@
|
||||
D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DonationSettingsViewController+MySupport.swift"; sourceTree = "<group>"; };
|
||||
D941863B2ACE252D002FE2D3 /* CallRecordLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRecordLogger.swift; sourceTree = "<group>"; };
|
||||
D943F3EE2892F89B008C0C8B /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
|
||||
D94441302D559567005B2A54 /* UUIDv7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7.swift; sourceTree = "<group>"; };
|
||||
D94441322D559C6B005B2A54 /* UUIDv7Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7Test.swift; sourceTree = "<group>"; };
|
||||
D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedemptionNecessityChecker.swift; sourceTree = "<group>"; };
|
||||
D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientState.swift; sourceTree = "<group>"; };
|
||||
D9495A6E2C76963F00843BC1 /* TSOutgoingMessageRecipientStateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientStateTest.swift; sourceTree = "<group>"; };
|
||||
@ -12801,6 +12803,7 @@
|
||||
F94261E8289B1B5400460798 /* UnfairLockTest.swift */,
|
||||
6600F34D298C81E300B1EDB7 /* UnknownEnumCodableTest.swift */,
|
||||
F9D5BFD02979B027001737E5 /* URLPathComponentsTest.swift */,
|
||||
D94441322D559C6B005B2A54 /* UUIDv7Test.swift */,
|
||||
F94261FD289B1B5400460798 /* ViewOnceMessagesTest.swift */,
|
||||
);
|
||||
name = Util;
|
||||
@ -14067,6 +14070,7 @@
|
||||
6600F34B298C81CD00B1EDB7 /* UnknownEnumCodable.swift */,
|
||||
F9D5BFCE2979AFF4001737E5 /* URLPathComponents.swift */,
|
||||
F9C5CB54289453B200548EEE /* UserProfileFinder.swift */,
|
||||
D94441302D559567005B2A54 /* UUIDv7.swift */,
|
||||
F9C5CB03289453B200548EEE /* ViewOnceMessages.swift */,
|
||||
F9C5CB7F289453B200548EEE /* Weak.swift */,
|
||||
F9C5CB29289453B200548EEE /* WeakTimer.swift */,
|
||||
@ -14132,7 +14136,6 @@
|
||||
34BB3C5823C6644B001651FC /* GroupsV2Utils.swift */,
|
||||
D99A0F6129F35CE1002E02E3 /* GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift */,
|
||||
D99A0F5F29F34FDA002E02E3 /* GroupUpdateInfoMessageInserter.swift */,
|
||||
662590BA2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift */,
|
||||
34BB3C5B23C6644B001651FC /* GroupV2Params.swift */,
|
||||
34BB3C5A23C6644B001651FC /* GroupV2Snapshot.swift */,
|
||||
340B870D23DF3E3A00BE0AFC /* GroupV2UpdatesImpl.swift */,
|
||||
@ -17358,7 +17361,6 @@
|
||||
724D47BD2B97C5B9001BE973 /* GroupsV2Utils.swift in Sources */,
|
||||
D99A0F6229F35CE1002E02E3 /* GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift in Sources */,
|
||||
D99A0F6029F34FDA002E02E3 /* GroupUpdateInfoMessageInserter.swift in Sources */,
|
||||
662590BB2B50922D001FDCDD /* GroupUpdateInfoMessageInserterBackupHelper.swift in Sources */,
|
||||
667AF9E22B4DC5EE008AEE5D /* GroupUpdateSource.swift in Sources */,
|
||||
724D47BB2B97C558001BE973 /* GroupV2Params.swift in Sources */,
|
||||
724D47BC2B97C57C001BE973 /* GroupV2Snapshot.swift in Sources */,
|
||||
@ -18186,6 +18188,7 @@
|
||||
5037F1942A43A6A300C372AD /* UserProfileMerger.swift in Sources */,
|
||||
506ABE6B2A43AECA008844D1 /* UserProfileStore.swift in Sources */,
|
||||
72901D2B2C9B11A3000406DC /* UserProfileWriter.swift in Sources */,
|
||||
D94441312D55956B005B2A54 /* UUIDv7.swift in Sources */,
|
||||
504F397C29D23B1700E849A6 /* ValidatedIncomingEnvelope.swift in Sources */,
|
||||
66F6D69C2C73F01900EFAF75 /* VersionedDisappearingMessageToken.swift in Sources */,
|
||||
F9C5CE3B289453B400548EEE /* VersionedProfiles.swift in Sources */,
|
||||
@ -18427,6 +18430,7 @@
|
||||
D9A3E2322A0DBDFC00E2A8B5 /* Usernames+UsernameLinkTests.swift in Sources */,
|
||||
D93830812A7065C7006CDCDE /* UsernameValidationManagerTests.swift in Sources */,
|
||||
506ABE6E2A43B2FE008844D1 /* UserProfileMergerTest.swift in Sources */,
|
||||
D94441332D559C6F005B2A54 /* UUIDv7Test.swift in Sources */,
|
||||
D9B95A9D29E894A600D7CB95 /* ValidatableModel.swift in Sources */,
|
||||
F942626A289B1B5500460798 /* ViewOnceMessagesTest.swift in Sources */,
|
||||
D9C964092BE44D700058F143 /* XCTest+Thenable.swift in Sources */,
|
||||
|
||||
@ -1104,7 +1104,6 @@ public class AppSetup {
|
||||
dateProvider: dateProvider,
|
||||
editMessageStore: editMessageStore,
|
||||
groupCallRecordManager: groupCallRecordManager,
|
||||
groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelperImpl(),
|
||||
groupUpdateItemBuilder: groupUpdateItemBuilder,
|
||||
individualCallRecordManager: individualCallRecordManager,
|
||||
interactionStore: backupInteractionStore,
|
||||
|
||||
@ -211,79 +211,6 @@ extension GroupUpdateInfoMessageInserterImpl {
|
||||
}
|
||||
}
|
||||
|
||||
/// See ``GroupUpdateInfoMessageInserterBackupHelper``.
|
||||
public static func collapseFromBackupIfNeeded(
|
||||
updates: inout [TSInfoMessage.PersistableGroupUpdateItem],
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
groupThread: TSGroupThread,
|
||||
transaction: SDSAnyWriteTransaction
|
||||
) {
|
||||
if
|
||||
updates.count == 2,
|
||||
let (sequenceRequestor, count) = updates[0]
|
||||
.representsSequenceOfRequestsAndCancels(),
|
||||
let singleRequestor = updates[1]
|
||||
.representsCollapsibleSingleRequestToJoin(),
|
||||
sequenceRequestor == singleRequestor
|
||||
{
|
||||
// These updates are collapsible in isolation.
|
||||
// Mark the first one as not the tail; thats the only change
|
||||
// we need to make to "collapse" this one.
|
||||
updates[0] =
|
||||
.sequenceOfInviteLinkRequestAndCancels(
|
||||
requester: sequenceRequestor.codableUuid,
|
||||
count: count,
|
||||
isTail: false
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
updates.count == 1,
|
||||
let requestingAci = updates[0].representsCollapsibleSingleRequestToJoin()
|
||||
else {
|
||||
// No change needed.
|
||||
return
|
||||
}
|
||||
|
||||
// This latest message is collapsible; try and collapse it.
|
||||
|
||||
guard let (mostRecentInfoMsg, _) =
|
||||
mostRecentVisibleInteractionsAsInfoMessages(
|
||||
forGroupThread: groupThread,
|
||||
withTransaction: transaction
|
||||
)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let (mostRecentMsgAci, count) = mostRecentInfoMsg
|
||||
.representsSingleSequenceOfRequestsAndCancels(
|
||||
localIdentifiers: localIdentifiers
|
||||
),
|
||||
requestingAci == mostRecentMsgAci
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
mostRecentInfoMsg.anyUpdateInfoMessage(
|
||||
transaction: transaction,
|
||||
block: {
|
||||
$0.setSingleUpdateItem(
|
||||
singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels(
|
||||
requester: mostRecentMsgAci.codableUuid,
|
||||
// Count stays the same.
|
||||
count: count,
|
||||
// Its not the tail because of the subsequent request.
|
||||
isTail: false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
// No need to change the updates in the new info message.
|
||||
return
|
||||
}
|
||||
|
||||
private static func mostRecentVisibleInteractionsAsInfoMessages(
|
||||
forGroupThread groupThread: TSGroupThread,
|
||||
withTransaction transaction: SDSAnyReadTransaction
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import LibSignalClient
|
||||
|
||||
public protocol GroupUpdateInfoMessageInserterBackupHelper {
|
||||
|
||||
/**
|
||||
* When processing a backup, we may encounter a single group update item
|
||||
* representing a collapsed sequence of requests and cancels from the same user
|
||||
* (``GroupSequenceOfRequestsAndCancelsUpdate`` proto, which becomes
|
||||
* ``TSInfoMessage.PersistableGroupUpdateItem.sequenceOfInviteLinkRequestAndCancels``).
|
||||
*
|
||||
* If we do, and it is immediately followed by a request to join by the same user in the same group,
|
||||
* we have to update the prior collapsed sequence message to mark it as _not_ the tail (isTail = false).
|
||||
* This method is a helper for that, which co-locates the code with the same code for processing
|
||||
* incoming group updates during normal app functioning.
|
||||
*
|
||||
* This method handles two cases:
|
||||
* 1. The sequence and subsequent request occur in the same TSInfoMessage, meaning they are both
|
||||
* in the passed-in updates. In this case it updates the sequence's isTail to false, and returns the
|
||||
* modified updates without touching the database.
|
||||
* 2. The sequence is in the most recently inserted TSInfoMessage, and the passed-in updates have just
|
||||
* the new request from the same user. In this case it updates the most recent TSInfoMessage in the db
|
||||
* and returns the same update(s).
|
||||
*
|
||||
* MUST BE CALLED BEFORE INSERTING THE NEW TSINFOMESSAGE.
|
||||
*
|
||||
* - parameter updates: the updates pulled from the backup that we are about to generate
|
||||
* a new TSInfoMessage for. This method may modify the updates.
|
||||
*/
|
||||
func collapseIfNeeded(
|
||||
updates: inout [TSInfoMessage.PersistableGroupUpdateItem],
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
groupThread: TSGroupThread,
|
||||
tx: DBWriteTransaction
|
||||
)
|
||||
}
|
||||
|
||||
public class GroupUpdateInfoMessageInserterBackupHelperImpl: GroupUpdateInfoMessageInserterBackupHelper {
|
||||
|
||||
public init() {}
|
||||
|
||||
public func collapseIfNeeded(
|
||||
updates: inout [TSInfoMessage.PersistableGroupUpdateItem],
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
groupThread: TSGroupThread,
|
||||
tx: DBWriteTransaction
|
||||
) {
|
||||
return GroupUpdateInfoMessageInserterImpl
|
||||
.collapseFromBackupIfNeeded(
|
||||
updates: &updates,
|
||||
localIdentifiers: localIdentifiers,
|
||||
groupThread: groupThread,
|
||||
transaction: SDSDB.shimOnlyBridge(tx)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -15,16 +15,13 @@ final class MessageBackupGroupUpdateMessageArchiver {
|
||||
private typealias PersistableGroupUpdateItem = TSInfoMessage.PersistableGroupUpdateItem
|
||||
|
||||
private let groupUpdateBuilder: GroupUpdateItemBuilder
|
||||
private let groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper
|
||||
private let interactionStore: MessageBackupInteractionStore
|
||||
|
||||
public init(
|
||||
groupUpdateBuilder: GroupUpdateItemBuilder,
|
||||
groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper,
|
||||
interactionStore: MessageBackupInteractionStore
|
||||
) {
|
||||
self.groupUpdateBuilder = groupUpdateBuilder
|
||||
self.groupUpdateHelper = groupUpdateHelper
|
||||
self.interactionStore = interactionStore
|
||||
}
|
||||
|
||||
@ -224,16 +221,6 @@ final class MessageBackupGroupUpdateMessageArchiver {
|
||||
)])
|
||||
}
|
||||
|
||||
// FIRST, try and do any collapsing. This might collapse
|
||||
// the passed in array of updates (modifying it), or
|
||||
// may update the most recent TSInfoMessage on disk, or both.
|
||||
groupUpdateHelper.collapseIfNeeded(
|
||||
updates: &persistableUpdates,
|
||||
localIdentifiers: context.recipientContext.localIdentifiers,
|
||||
groupThread: groupThread,
|
||||
tx: context.tx
|
||||
)
|
||||
|
||||
guard persistableUpdates.isEmpty.negated else {
|
||||
// If we got an empty array, that means it got collapsed!
|
||||
// Ok to skip, as any updates should be applied to the
|
||||
|
||||
@ -22,14 +22,12 @@ final class MessageBackupChatUpdateMessageArchiver: MessageBackupProtoArchiver {
|
||||
callRecordStore: any CallRecordStore,
|
||||
contactManager: any MessageBackup.Shims.ContactManager,
|
||||
groupCallRecordManager: any GroupCallRecordManager,
|
||||
groupUpdateHelper: any GroupUpdateInfoMessageInserterBackupHelper,
|
||||
groupUpdateItemBuilder: any GroupUpdateItemBuilder,
|
||||
individualCallRecordManager: any IndividualCallRecordManager,
|
||||
interactionStore: MessageBackupInteractionStore
|
||||
) {
|
||||
groupUpdateMessageArchiver = MessageBackupGroupUpdateMessageArchiver(
|
||||
groupUpdateBuilder: groupUpdateItemBuilder,
|
||||
groupUpdateHelper: groupUpdateHelper,
|
||||
interactionStore: interactionStore
|
||||
)
|
||||
expirationTimerChatUpdateArchiver = MessageBackupExpirationTimerChatUpdateArchiver(
|
||||
|
||||
@ -17,7 +17,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver {
|
||||
private let dateProvider: DateProvider
|
||||
private let editMessageStore: EditMessageStore
|
||||
private let groupCallRecordManager: GroupCallRecordManager
|
||||
private let groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper
|
||||
private let groupUpdateItemBuilder: GroupUpdateItemBuilder
|
||||
private let individualCallRecordManager: IndividualCallRecordManager
|
||||
private let interactionStore: MessageBackupInteractionStore
|
||||
@ -34,7 +33,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver {
|
||||
dateProvider: @escaping DateProvider,
|
||||
editMessageStore: EditMessageStore,
|
||||
groupCallRecordManager: GroupCallRecordManager,
|
||||
groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelper,
|
||||
groupUpdateItemBuilder: GroupUpdateItemBuilder,
|
||||
individualCallRecordManager: IndividualCallRecordManager,
|
||||
interactionStore: MessageBackupInteractionStore,
|
||||
@ -50,7 +48,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver {
|
||||
self.dateProvider = dateProvider
|
||||
self.editMessageStore = editMessageStore
|
||||
self.groupCallRecordManager = groupCallRecordManager
|
||||
self.groupUpdateHelper = groupUpdateHelper
|
||||
self.groupUpdateItemBuilder = groupUpdateItemBuilder
|
||||
self.individualCallRecordManager = individualCallRecordManager
|
||||
self.interactionStore = interactionStore
|
||||
@ -92,7 +89,6 @@ public class MessageBackupChatItemArchiverImpl: MessageBackupChatItemArchiver {
|
||||
callRecordStore: callRecordStore,
|
||||
contactManager: contactManager,
|
||||
groupCallRecordManager: groupCallRecordManager,
|
||||
groupUpdateHelper: groupUpdateHelper,
|
||||
groupUpdateItemBuilder: groupUpdateItemBuilder,
|
||||
individualCallRecordManager: individualCallRecordManager,
|
||||
interactionStore: interactionStore
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
//
|
||||
|
||||
import LibSignalClient
|
||||
import SQLite3
|
||||
import GRDB
|
||||
|
||||
public final class MessageBackupInteractionStore {
|
||||
|
||||
@ -156,11 +158,8 @@ public final class MessageBackupInteractionStore {
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Insert
|
||||
// MARK: - Insert
|
||||
|
||||
// Even generating the sql string itself is expensive when multiplied by 200k messages.
|
||||
// So we generate the string once and cache it (on top of caching the Statement)
|
||||
private var cachedSQL: String?
|
||||
private func insert(
|
||||
interaction: TSInteraction,
|
||||
in thread: MessageBackup.ChatThread,
|
||||
@ -200,22 +199,7 @@ public final class MessageBackupInteractionStore {
|
||||
// and restore, we'll only send back a Null message. (Until such a day
|
||||
// when resends use the interactions table and not MSL at all).
|
||||
|
||||
let sql: String
|
||||
if let cachedSQL {
|
||||
sql = cachedSQL
|
||||
} else {
|
||||
let columnsSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map(\.name).joined(separator: ", ")
|
||||
let valuesSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map({ _ in "?" }).joined(separator: ", ")
|
||||
sql = """
|
||||
INSERT INTO \(InteractionRecord.databaseTableName) (\(columnsSQL)) \
|
||||
VALUES (\(valuesSQL))
|
||||
"""
|
||||
cachedSQL = sql
|
||||
}
|
||||
|
||||
let statement = try context.tx.databaseConnection.cachedStatement(sql: sql)
|
||||
statement.setUncheckedArguments((interaction.asRecord() as! InteractionRecord).asArguments())
|
||||
try statement.execute()
|
||||
try insertInteractionWithDirectSQLiteCalls(interaction, databaseConnection: context.tx.databaseConnection)
|
||||
interaction.updateRowId(context.tx.databaseConnection.lastInsertedRowID)
|
||||
|
||||
guard let interactionRowId = interaction.sqliteRowId else {
|
||||
@ -249,8 +233,110 @@ public final class MessageBackupInteractionStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reuse the same SQL string, since generating the same string for each
|
||||
/// message gets expensive.
|
||||
private lazy var insertInteractionSQL: String = {
|
||||
let columnsSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map(\.name).joined(separator: ", ")
|
||||
let valuesSQL = InteractionRecord.CodingKeys.allCases.filter({ $0 != .id }).map({ _ in "?" }).joined(separator: ", ")
|
||||
return """
|
||||
INSERT INTO \(InteractionRecord.databaseTableName) (\(columnsSQL)) \
|
||||
VALUES (\(valuesSQL))
|
||||
"""
|
||||
}()
|
||||
|
||||
/// Inserts the given interaction using direct `sqlite3_*` function calls,
|
||||
/// sidestepping GRDB abstractions.
|
||||
///
|
||||
/// Profiling showed that the GRDB methods for creating `Statement` and
|
||||
/// `StatementArgument` structs for insertion were taking a shocking amount
|
||||
/// of time, primarily due to their use of Swift methods like `zip` that
|
||||
/// instantiate opaque iterator structs. To avoid that cost, this method
|
||||
/// sidesteps the GRDB abstractions and makes direct `sqlite3_*` method
|
||||
/// calls instead.
|
||||
///
|
||||
/// In normal app functioning, the cost of those abstractions probably isn't
|
||||
/// worth managing these `sqlite3_*` methods ourselves. However, the savings
|
||||
/// over hundreds of thousands of interaction inserts during a restore are.
|
||||
private func insertInteractionWithDirectSQLiteCalls(
|
||||
_ interaction: TSInteraction,
|
||||
databaseConnection: GRDB.Database
|
||||
) throws {
|
||||
guard let sqliteConnection = databaseConnection.sqliteConnection else {
|
||||
throw OWSAssertionError("Missing SQLite connection!")
|
||||
}
|
||||
|
||||
/// SQLite compiles SQL strings into its internal bytecode. The bytecode
|
||||
/// statements can be cached by SQLite, to avoid re-compiling an
|
||||
/// identical SQL string repeatedly.
|
||||
///
|
||||
/// This line uses GRDB to make the necessary SQLite calls to compile
|
||||
/// and cache our "insert interaction" statement, which is returned as a
|
||||
/// pointer we can pass back into SQLite,, since those calls involve
|
||||
/// tricky pointer math. GRDB then holds a reference to that compiled
|
||||
/// statement pointer in a package-level cache, from which we can
|
||||
/// retrieve it.
|
||||
let cachedSqliteStatement: GRDB.SQLiteStatement = try databaseConnection.cachedStatement(
|
||||
sql: insertInteractionSQL
|
||||
).sqliteStatement
|
||||
|
||||
/// The compiled "insert interaction" SQLite statement contains `?`
|
||||
/// placeholders, which must have real values "bound" to them before the
|
||||
/// statement can be used to actually insert a database row. Those bound
|
||||
/// values are specific to each interaction being inserted; so, before
|
||||
/// we can use the cached statement we must reset any values from
|
||||
/// previous interaction inserts and bind new values.
|
||||
var sqliteReturnCode: Int32
|
||||
|
||||
// Reset the cached statement.
|
||||
sqliteReturnCode = sqlite3_reset(cachedSqliteStatement)
|
||||
guard sqliteReturnCode == SQLITE_OK else {
|
||||
let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!)
|
||||
throw OWSAssertionError("Failed to reset interaction insert statement! \(errmsg)")
|
||||
}
|
||||
|
||||
// Clear any previously bound arguments.
|
||||
sqliteReturnCode = sqlite3_clear_bindings(cachedSqliteStatement)
|
||||
guard sqliteReturnCode == SQLITE_OK else {
|
||||
let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!)
|
||||
throw OWSAssertionError("Failed to clear argument bindings from interaction insert statement! \(errmsg)")
|
||||
}
|
||||
|
||||
// Bind new values from the current interaction.
|
||||
let args = (interaction.asRecord() as! InteractionRecord).asValues()
|
||||
var count: Int32 = 1
|
||||
for arg in args {
|
||||
defer { count += 1 }
|
||||
|
||||
guard let arg else {
|
||||
continue
|
||||
}
|
||||
|
||||
let code = arg.databaseValue.bind(to: cachedSqliteStatement, at: count)
|
||||
guard code == SQLITE_OK else {
|
||||
let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!)
|
||||
throw OWSAssertionError("Failed to bind argument to interaction insert statement! \(errmsg)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Now that we've bound values for the current interaction, we can
|
||||
/// execute the statement.
|
||||
insertLoop: while true {
|
||||
switch sqlite3_step(cachedSqliteStatement) {
|
||||
case SQLITE_DONE:
|
||||
break insertLoop
|
||||
case SQLITE_ROW, SQLITE_OK:
|
||||
break
|
||||
case let code:
|
||||
let errmsg = String(cString: sqlite3_errmsg(sqliteConnection)!)
|
||||
throw OWSAssertionError("Unexpected SQLite return code \(code) while executing interaction insert statement! \(errmsg)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension BackupProto_ChatItem.OneOf_DirectionalDetails {
|
||||
|
||||
var wasRead: Bool {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
//
|
||||
|
||||
public import LibSignalClient
|
||||
import GRDB
|
||||
|
||||
public enum BackupValidationError: Error {
|
||||
case unknownFields([String])
|
||||
@ -674,6 +675,14 @@ public class MessageBackupManagerImpl: MessageBackupManager {
|
||||
) async throws {
|
||||
let result: Result<BackupProto_BackupInfo, Error> = await db.awaitableWriteWithTxCompletion { tx in
|
||||
do {
|
||||
/// Drops all indexes on the `TSInteraction` table before doing
|
||||
/// the import, which dramatically speeds up the import. We'll
|
||||
/// then recreate all these indexes in bulk afterwards.
|
||||
let interactionIndexes = try dropAllIndexes(
|
||||
forTable: InteractionRecord.databaseTableName,
|
||||
tx: tx
|
||||
)
|
||||
|
||||
let backupInfo = try Bench(
|
||||
title: benchTitle,
|
||||
memorySamplerRatio: FeatureFlags.messageBackupMemorySamplerRatio,
|
||||
@ -700,6 +709,19 @@ public class MessageBackupManagerImpl: MessageBackupManager {
|
||||
}
|
||||
}
|
||||
|
||||
let timeBeforeCreatingIndexes = dateProviderMonotonic()
|
||||
|
||||
/// Now that we've imported successfully, we want to recreate
|
||||
/// the indexes we temporarily dropped.
|
||||
try createIndexes(
|
||||
interactionIndexes,
|
||||
onTable: InteractionRecord.databaseTableName,
|
||||
tx: tx
|
||||
)
|
||||
|
||||
let timeAfterCreatingIndexes = dateProviderMonotonic()
|
||||
Logger.info("Created indexes in \(timeAfterCreatingIndexes.millisSince(timeBeforeCreatingIndexes))ms")
|
||||
|
||||
return .commit(.success(backupInfo))
|
||||
} catch let error {
|
||||
return .rollback(.failure(error))
|
||||
@ -1059,6 +1081,62 @@ public class MessageBackupManagerImpl: MessageBackupManager {
|
||||
|
||||
// MARK: -
|
||||
|
||||
private struct SQLiteIndexInfo {
|
||||
let tableName: String
|
||||
let sqlThatCreatedIndex: String
|
||||
}
|
||||
|
||||
private func dropAllIndexes(
|
||||
forTable tableName: String,
|
||||
tx: DBWriteTransaction
|
||||
) throws -> [SQLiteIndexInfo] {
|
||||
let allIndexesOnTable: [GRDB.IndexInfo] = try tx.databaseConnection.indexes(on: tableName)
|
||||
|
||||
var sqliteIndexInfos = [SQLiteIndexInfo]()
|
||||
|
||||
for index in allIndexesOnTable {
|
||||
if index.name.contains("autoindex") {
|
||||
// Skip indexes automatically created by SQLite, such as on
|
||||
// primary keys.
|
||||
continue
|
||||
}
|
||||
|
||||
guard let sqlThatCreatedIndex = try String.fetchOne(
|
||||
tx.databaseConnection,
|
||||
sql: """
|
||||
SELECT sql FROM sqlite_master
|
||||
WHERE type = 'index'
|
||||
AND name = '\(index.name)'
|
||||
"""
|
||||
) else {
|
||||
throw OWSAssertionError("Failed to get SQL for creating index \(index.name)!")
|
||||
}
|
||||
|
||||
sqliteIndexInfos.append(SQLiteIndexInfo(
|
||||
tableName: tableName,
|
||||
sqlThatCreatedIndex: sqlThatCreatedIndex
|
||||
))
|
||||
|
||||
try tx.databaseConnection.drop(index: index.name)
|
||||
}
|
||||
|
||||
return sqliteIndexInfos
|
||||
}
|
||||
|
||||
private func createIndexes(
|
||||
_ indexInfos: [SQLiteIndexInfo],
|
||||
onTable tableName: String,
|
||||
tx: DBWriteTransaction
|
||||
) throws {
|
||||
owsPrecondition(indexInfos.allSatisfy { $0.tableName == tableName })
|
||||
|
||||
for indexInfo in indexInfos {
|
||||
try tx.databaseConnection.execute(sql: indexInfo.sqlThatCreatedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func processErrors(
|
||||
errors: [LoggableErrorAndProto],
|
||||
didFail: Bool,
|
||||
|
||||
@ -4950,8 +4950,8 @@ extension InteractionRecord {
|
||||
|
||||
// This defines all of the columns used in the table
|
||||
// where this model (and any subclasses) are persisted.
|
||||
internal func asArguments() -> StatementArguments {
|
||||
let databaseValues: [DatabaseValueConvertible?] = [
|
||||
internal func asValues() -> [DatabaseValueConvertible?] {
|
||||
return [
|
||||
recordType,
|
||||
uniqueId,
|
||||
receivedAtTimestamp,
|
||||
@ -5029,7 +5029,10 @@ extension InteractionRecord {
|
||||
isSmsMessageRestoredFromBackup,
|
||||
|
||||
]
|
||||
return StatementArguments(databaseValues)
|
||||
}
|
||||
|
||||
internal func asArguments() -> StatementArguments {
|
||||
return StatementArguments(asValues())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,10 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value)
|
||||
receivedAtTimestamp:(uint64_t)receivedAtTimestamp
|
||||
thread:(TSThread *)thread
|
||||
{
|
||||
NSString *uniqueId = [[self class] generateUniqueId];
|
||||
// Use a sequential UUID for interaction inserts, as an optimization for the
|
||||
// corresponding insert into the index on `uniqueId`. See comments about
|
||||
// UUIDv7 for more.
|
||||
NSString *uniqueId = [[NSUUID sequential] UUIDString];
|
||||
self = [super initWithUniqueId:uniqueId];
|
||||
|
||||
if (!self) {
|
||||
|
||||
73
SignalServiceKit/Util/UUIDv7.swift
Normal file
73
SignalServiceKit/Util/UUIDv7.swift
Normal file
@ -0,0 +1,73 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
extension UUID {
|
||||
/// Returns a UUIDv7, as defined in [the RFC][RFC]. Notably, v7 UUIDs embed
|
||||
/// a timestamp in the first 6 most-significant bytes, thereby allowing
|
||||
/// callers who pass sequential timestamps to construct UUIDs that are
|
||||
/// lexicographically sequential.
|
||||
///
|
||||
/// This sequential property can be a useful optimization when the UUIDs are
|
||||
/// part of a database row and that row is indexed by the UUID. When the
|
||||
/// UUIDs are ordered in the same sequence as the row insertions the
|
||||
/// corresponding insertion into the index is always at the end of the
|
||||
/// index (`O(1)`), whereas a random UUID might be inserted anywhere in the
|
||||
/// index (`O(log n)`).
|
||||
///
|
||||
/// [RFC]: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7
|
||||
static func v7(timestamp: UInt64) -> UUID {
|
||||
var uuidBytes = Data(repeating: 0, count: 16)
|
||||
|
||||
// Assign the lower six bytes of the timestamp (the bytes that change
|
||||
// the most) to the first six bytes of the UUID, in big-endian order
|
||||
// (most-significant byte first).
|
||||
|
||||
// Assign timestamp to the first 6 bytes (big-endian)
|
||||
// For each of the first six bytes of the
|
||||
uuidBytes[0] = UInt8((timestamp >> 40) & 0xFF)
|
||||
uuidBytes[1] = UInt8((timestamp >> 32) & 0xFF)
|
||||
uuidBytes[2] = UInt8((timestamp >> 24) & 0xFF)
|
||||
uuidBytes[3] = UInt8((timestamp >> 16) & 0xFF)
|
||||
uuidBytes[4] = UInt8((timestamp >> 8) & 0xFF)
|
||||
uuidBytes[5] = UInt8(timestamp & 0xFF)
|
||||
|
||||
// Set the next four bits to 0b0111, the required "version" for UUIDv7.
|
||||
// Set the rest of that byte to random.
|
||||
let versionBits = UInt8(0b0111)
|
||||
let fourRandomBits = UInt8.random(in: 0...0xFF) | 0x0F
|
||||
uuidBytes[6] = UInt8((versionBits << 4) | fourRandomBits)
|
||||
|
||||
// Add a full byte of random.
|
||||
uuidBytes[7] = UInt8.random(in: 0...0xFF)
|
||||
|
||||
// Set the next two bits to 0b10, the required "variant" for UUIDv7. Set
|
||||
// the rest of that byte to random.
|
||||
let variantBits = UInt8(0b10)
|
||||
let sixRandomBits = UInt8.random(in: 0...0xFF) | 0x3F
|
||||
uuidBytes[8] = UInt8((variantBits << 6) | sixRandomBits)
|
||||
|
||||
// Set the remaining bytes to random.
|
||||
for i in 9..<16 {
|
||||
uuidBytes[i] = UInt8.random(in: 0...0xFF)
|
||||
}
|
||||
|
||||
// Finally, construct a UUID from the bytes.
|
||||
return uuidBytes.withUnsafeBytes { bytes in
|
||||
UUID(uuid: bytes.load(as: uuid_t.self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSUUID {
|
||||
private static var sequentialCounter: UInt64 = MonotonicDate().millisSince(.distantPast)
|
||||
|
||||
@objc
|
||||
static func sequential() -> NSUUID {
|
||||
let uuid: UUID = .v7(timestamp: sequentialCounter)
|
||||
sequentialCounter += 1
|
||||
|
||||
return uuid as NSUUID
|
||||
}
|
||||
}
|
||||
23
SignalServiceKit/tests/Util/UUIDv7Test.swift
Normal file
23
SignalServiceKit/tests/Util/UUIDv7Test.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
@testable import SignalServiceKit
|
||||
import Testing
|
||||
|
||||
struct UUIDv7Test {
|
||||
@Test
|
||||
@available(iOS 17, *)
|
||||
func testSequential() {
|
||||
let timestamps = [
|
||||
MessageTimestampGenerator.sharedInstance.generateTimestamp(),
|
||||
MessageTimestampGenerator.sharedInstance.generateTimestamp(),
|
||||
MessageTimestampGenerator.sharedInstance.generateTimestamp()
|
||||
]
|
||||
|
||||
let uuids: [UUID] = timestamps.map { .v7(timestamp: $0) }
|
||||
|
||||
#expect(uuids == uuids.sorted())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user