Performance improvements to import performance for messages

This commit is contained in:
Sasha Weiss 2025-02-19 11:48:25 -08:00 committed by GitHub
parent ebabb78339
commit 96a76065df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 304 additions and 185 deletions

View File

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

View File

@ -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 */,

View File

@ -1104,7 +1104,6 @@ public class AppSetup {
dateProvider: dateProvider,
editMessageStore: editMessageStore,
groupCallRecordManager: groupCallRecordManager,
groupUpdateHelper: GroupUpdateInfoMessageInserterBackupHelperImpl(),
groupUpdateItemBuilder: groupUpdateItemBuilder,
individualCallRecordManager: individualCallRecordManager,
interactionStore: backupInteractionStore,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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())
}
}

View File

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

View 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
}
}

View 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())
}
}