diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt index 92b15e0622..9fa33fa4ba 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt @@ -13,8 +13,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.signal.benchmark.setup.Generator import org.signal.benchmark.setup.Harness +import org.signal.benchmark.setup.OtherClient import org.signal.core.util.ThreadUtil import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.TestDbUtils +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.recipients.Recipient import org.whispersystems.signalservice.internal.push.Envelope import org.whispersystems.signalservice.internal.websocket.BenchmarkWebSocketConnection import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage @@ -45,6 +50,8 @@ class BenchmarkCommandReceiver : BroadcastReceiver() { when (command) { "individual-send" -> handlePrepareIndividualSend() "group-send" -> handlePrepareGroupSend() + "group-delivery-receipt" -> handlePrepareGroupReceipts { client, timestamps -> client.generateInboundDeliveryReceipts(timestamps) } + "group-read-receipt" -> handlePrepareGroupReceipts { client, timestamps -> client.generateInboundReadReceipts(timestamps) } "release-messages" -> { BenchmarkWebSocketConnection.authInstance.startWholeBatchTrace = true BenchmarkWebSocketConnection.authInstance.releaseMessages() @@ -113,6 +120,62 @@ class BenchmarkCommandReceiver : BroadcastReceiver() { BenchmarkWebSocketConnection.authInstance.addQueueEmptyMessage() } + private fun handlePrepareGroupReceipts(generateReceipts: (OtherClient, List) -> List) { + val clients = Harness.otherClients.take(5) + + establishGroupSessions(clients) + + val timestamps = getOutgoingGroupMessageTimestamps() + Log.i(TAG, "Found ${timestamps.size} outgoing message timestamps for receipts") + + val allClientEnvelopes = clients.map { client -> + generateReceipts(client, timestamps).map { it.toWebSocketPayload() } + } + + BenchmarkWebSocketConnection.authInstance.addPendingMessages(interleave(allClientEnvelopes)) + BenchmarkWebSocketConnection.authInstance.addQueueEmptyMessage() + } + + private fun establishGroupSessions(clients: List) { + val encryptedEnvelopes = clients.map { it.encrypt(Generator.encryptedTextMessage(System.currentTimeMillis(), groupMasterKey = Harness.groupMasterKey)) } + + runBlocking { + launch(Dispatchers.IO) { + BenchmarkWebSocketConnection.authInstance.run { + Log.i(TAG, "Sending initial group messages from clients to establish sessions.") + addPendingMessages(encryptedEnvelopes.map { it.toWebSocketPayload() }) + releaseMessages() + ThreadUtil.sleep(1000) + } + } + } + } + + private fun getOutgoingGroupMessageTimestamps(): List { + val groupId = GroupId.v2(Harness.groupMasterKey) + val groupRecipient = Recipient.externalGroupExact(groupId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient) + val selfId = Recipient.self().id.toLong() + return TestDbUtils.getOutgoingMessageTimestamps(threadId, selfId) + } + + /** + * Interleaves lists so that items from different lists alternate: + * [[a1, a2], [b1, b2], [c1, c2]] -> [a1, b1, c1, a2, b2, c2] + */ + private fun interleave(lists: List>): List { + val result = mutableListOf() + val maxSize = lists.maxOf { it.size } + for (i in 0 until maxSize) { + for (list in lists) { + if (i < list.size) { + result += list[i] + } + } + } + return result + } + private fun Envelope.toWebSocketPayload(): WebSocketRequestMessage { return WebSocketRequestMessage( verb = "PUT", diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt index 3ecb7fbde2..92211e93f0 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt @@ -6,7 +6,10 @@ import org.signal.benchmark.setup.TestMessages import org.signal.benchmark.setup.TestUsers import org.thoughtcrime.securesms.BaseActivity import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.TestDbUtils +import org.thoughtcrime.securesms.mms.OutgoingMessage import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.TextSecurePreferences class BenchmarkSetupActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -17,6 +20,8 @@ class BenchmarkSetupActivity : BaseActivity() { "conversation-open" -> setupConversationOpen() "message-send" -> setupMessageSend() "group-message-send" -> setupGroupMessageSend() + "group-delivery-receipt" -> setupGroupReceipt(includeMsl = true) + "group-read-receipt" -> setupGroupReceipt(enableReadReceipts = true) } val textView: TextView = TextView(this).apply { @@ -68,4 +73,40 @@ class BenchmarkSetupActivity : BaseActivity() { TestUsers.setupSelf() TestUsers.setupGroup() } + + private fun setupGroupReceipt(includeMsl: Boolean = false, enableReadReceipts: Boolean = false) { + TestUsers.setupSelf() + val groupId = TestUsers.setupGroup() + + val groupRecipient = Recipient.externalGroupExact(groupId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient) + + val messageIds = mutableListOf() + val timestamps = mutableListOf() + val baseTimestamp = 2_000_000L + + for (i in 0 until 100) { + val timestamp = baseTimestamp + i + val message = OutgoingMessage( + recipient = groupRecipient, + body = "Outgoing message $i", + timestamp = timestamp, + isSecure = true + ) + val insert = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null) + SignalDatabase.messages.markAsSent(insert.messageId, true) + messageIds += insert.messageId + timestamps += timestamp + } + + if (includeMsl) { + val selfId = Recipient.self().id + val memberRecipientIds = SignalDatabase.groups.getGroup(groupId).get().members.filter { it != selfId } + TestDbUtils.insertMessageSendLogEntries(messageIds, timestamps, memberRecipientIds) + } + + if (enableReadReceipts) { + TextSecurePreferences.setReadReceiptsEnabled(this, true) + } + } } diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/setup/Generator.kt b/app/src/benchmarkShared/java/org/signal/benchmark/setup/Generator.kt index 523dad2e2f..9de7c9c9aa 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/Generator.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/Generator.kt @@ -18,6 +18,7 @@ import org.whispersystems.signalservice.internal.push.DataMessage import org.whispersystems.signalservice.internal.push.Envelope import org.whispersystems.signalservice.internal.push.GroupContextV2 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage +import org.whispersystems.signalservice.internal.push.ReceiptMessage import java.util.Optional import java.util.UUID @@ -45,6 +46,26 @@ object Generator { return EnvelopeContent.encrypted(content.build(), ContentHint.RESENDABLE, Optional.empty()) } + fun encryptedDeliveryReceipt(now: Long, timestamps: List): EnvelopeContent { + return encryptedReceipt(ReceiptMessage.Type.DELIVERY, timestamps) + } + + fun encryptedReadReceipt(now: Long, timestamps: List): EnvelopeContent { + return encryptedReceipt(ReceiptMessage.Type.READ, timestamps) + } + + private fun encryptedReceipt(type: ReceiptMessage.Type, timestamps: List): EnvelopeContent { + val content = Content.Builder().apply { + receiptMessage( + ReceiptMessage.Builder().buildWith { + this.type = type + timestamp = timestamps + } + ) + } + return EnvelopeContent.encrypted(content.build(), ContentHint.IMPLICIT, Optional.empty()) + } + fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope { val serverGuid = UUID.randomUUID() return Envelope.Builder() diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/setup/OtherClient.kt b/app/src/benchmarkShared/java/org/signal/benchmark/setup/OtherClient.kt index 497e2726f2..a581181291 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/OtherClient.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/OtherClient.kt @@ -62,6 +62,10 @@ class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPai /** Inspired by SignalServiceMessageSender#getEncryptedMessage */ fun encrypt(envelopeContent: EnvelopeContent): Envelope { + return encrypt(envelopeContent, envelopeContent.content.get().dataMessage!!.timestamp!!) + } + + fun encrypt(envelopeContent: EnvelopeContent, timestamp: Long): Envelope { val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null) if (!aciStore.containsSession(getAliceProtocolAddress())) { @@ -70,7 +74,7 @@ class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPai } return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent) - .toEnvelope(envelopeContent.content.get().dataMessage!!.timestamp!!, getAliceServiceId()) + .toEnvelope(timestamp, getAliceServiceId()) } fun generateInboundEnvelopes(count: Int): List { @@ -84,6 +88,24 @@ class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPai return envelopes } + fun generateInboundDeliveryReceipts(messageTimestamps: List): List { + return generateInboundReceipts(messageTimestamps, Generator::encryptedDeliveryReceipt) + } + + fun generateInboundReadReceipts(messageTimestamps: List): List { + return generateInboundReceipts(messageTimestamps, Generator::encryptedReadReceipt) + } + + private fun generateInboundReceipts(messageTimestamps: List, receiptFactory: (Long, List) -> EnvelopeContent): List { + val envelopes = ArrayList(messageTimestamps.size) + var now = System.currentTimeMillis() + for (messageTimestamp in messageTimestamps) { + envelopes += encrypt(receiptFactory(now, listOf(messageTimestamp)), now) + now += 3 + } + return envelopes + } + fun generateInboundGroupEnvelopes(count: Int, groupMasterKey: GroupMasterKey): List { val envelopes = ArrayList(count) var now = System.currentTimeMillis() diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt b/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt index 6abf86a667..4b0fb5fecf 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.keyvalue.CertificateType import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.Skipped @@ -170,7 +171,7 @@ object TestUsers { return others } - fun setupGroup() { + fun setupGroup(): GroupId.V2 { val members = setupTestClients(5) val self = Recipient.self() @@ -202,6 +203,8 @@ object TestUsers { ) SignalDatabase.recipients.setProfileSharing(Recipient.externalGroupExact(groupId!!).id, true) + + return groupId } private fun member(aci: ACI, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0, labelEmoji: String = "", labelString: String = ""): DecryptedMember { diff --git a/app/src/benchmarkShared/java/org/thoughtcrime/securesms/database/TestDbUtils.kt b/app/src/benchmarkShared/java/org/thoughtcrime/securesms/database/TestDbUtils.kt index 6f09ba76be..b627aced9d 100644 --- a/app/src/benchmarkShared/java/org/thoughtcrime/securesms/database/TestDbUtils.kt +++ b/app/src/benchmarkShared/java/org/thoughtcrime/securesms/database/TestDbUtils.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import org.signal.core.util.SqlUtil.buildArgs +import org.thoughtcrime.securesms.recipients.RecipientId +import org.whispersystems.signalservice.internal.push.Content object TestDbUtils { @@ -11,4 +13,58 @@ object TestDbUtils { contentValues.put(MessageTable.DATE_RECEIVED, timestamp) val rowsUpdated = database.update(MessageTable.TABLE_NAME, contentValues, DatabaseTable.ID_WHERE, buildArgs(messageId)) } + + fun getOutgoingMessageTimestamps(threadId: Long, selfRecipientId: Long): List { + val timestamps = mutableListOf() + SignalDatabase.messages.databaseHelper.signalReadableDatabase.query( + MessageTable.TABLE_NAME, + arrayOf(MessageTable.DATE_SENT), + "${MessageTable.THREAD_ID} = ? AND ${MessageTable.FROM_RECIPIENT_ID} = ?", + arrayOf(threadId.toString(), selfRecipientId.toString()), + null, + null, + "${MessageTable.DATE_SENT} ASC" + ).use { cursor -> + while (cursor.moveToNext()) { + timestamps += cursor.getLong(0) + } + } + return timestamps + } + + fun insertMessageSendLogEntries(messageIds: List, timestamps: List, recipientIds: List) { + val db = SignalDatabase.messages.databaseHelper.signalWritableDatabase + val dummyContent = Content.Builder().build().encode() + + db.beginTransaction() + try { + for (i in messageIds.indices) { + val payloadValues = ContentValues().apply { + put("date_sent", timestamps[i]) + put("content", dummyContent) + put("content_hint", 0) + put("urgent", 1) + } + val payloadId = db.insert("msl_payload", null, payloadValues) + + val messageValues = ContentValues().apply { + put("payload_id", payloadId) + put("message_id", messageIds[i]) + } + db.insert("msl_message", null, messageValues) + + for (recipientId in recipientIds) { + val recipientValues = ContentValues().apply { + put("payload_id", payloadId) + put("recipient_id", recipientId.toLong()) + put("device", 1) + } + db.insert("msl_recipient", null, recipientValues) + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/BatchCache.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/BatchCache.kt index 26b6a996d1..12046d5285 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/BatchCache.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/BatchCache.kt @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.groupMasterKey import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.hasGroupContext +import org.thoughtcrime.securesms.recipients.RecipientId import org.whispersystems.signalservice.internal.push.DataMessage import java.util.Optional @@ -65,8 +66,13 @@ abstract class BatchCache { SignalDatabase.threads.updateForMessageInsert(threadId, unarchive = true) } + protected fun flushMslDelete(recipientId: RecipientId, device: Int, timestamps: List) { + SignalDatabase.messageLog.deleteEntriesForRecipient(timestamps, recipientId, device) + } + abstract fun addJob(job: Job) abstract fun addIncomingMessageInsertThreadUpdate(threadId: Long) + abstract fun addMslDelete(recipientId: RecipientId, device: Int, timestamps: List) } /** @@ -83,6 +89,10 @@ class OneTimeBatchCache : BatchCache() { override fun addIncomingMessageInsertThreadUpdate(threadId: Long) { flushIncomingMessageInsertThreadUpdate(threadId) } + + override fun addMslDelete(recipientId: RecipientId, device: Int, timestamps: List) { + flushMslDelete(recipientId, device, timestamps) + } } /** @@ -100,6 +110,7 @@ class ReusedBatchCache : BatchCache() { private val batchedJobs = ArrayList(BATCH_SIZE) private val threadUpdates = HashSet(BATCH_SIZE) + private val mslDeletes = HashMap, MutableList>(BATCH_SIZE) override fun addJob(job: Job) { batchedJobs += job @@ -109,6 +120,10 @@ class ReusedBatchCache : BatchCache() { threadUpdates += threadId } + override fun addMslDelete(recipientId: RecipientId, device: Int, timestamps: List) { + mslDeletes.getOrPut(recipientId to device) { mutableListOf() } += timestamps + } + override fun flushAndClear() { super.flushAndClear() @@ -123,5 +138,12 @@ class ReusedBatchCache : BatchCache() { } } threadUpdates.clear() + + if (mslDeletes.isNotEmpty()) { + SignalDatabase.runInTransaction { + mslDeletes.forEach { (key, timestamps) -> flushMslDelete(key.first, key.second, timestamps) } + } + } + mslDeletes.clear() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt index 04822f46a2..e0c46b8506 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt @@ -496,7 +496,8 @@ open class MessageContentProcessor(private val context: Context) { envelope, content, metadata, - if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp) + if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp), + batchCache ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/ReceiptMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/ReceiptMessageProcessor.kt index e136bb3bec..ce78b72eb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/ReceiptMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/ReceiptMessageProcessor.kt @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.messages.MessageContentProcessor.Companion.war import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry +import org.thoughtcrime.securesms.util.SignalTrace import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata import org.whispersystems.signalservice.internal.push.Content @@ -23,11 +24,11 @@ object ReceiptMessageProcessor { private const val VERBOSE = false - fun process(context: Context, senderRecipient: Recipient, envelope: Envelope, content: Content, metadata: EnvelopeMetadata, earlyMessageCacheEntry: EarlyMessageCacheEntry?) { + fun process(context: Context, senderRecipient: Recipient, envelope: Envelope, content: Content, metadata: EnvelopeMetadata, earlyMessageCacheEntry: EarlyMessageCacheEntry?, batchCache: BatchCache) { val receiptMessage = content.receiptMessage!! when (receiptMessage.type) { - ReceiptMessage.Type.DELIVERY -> handleDeliveryReceipt(envelope, metadata, receiptMessage, senderRecipient.id) + ReceiptMessage.Type.DELIVERY -> handleDeliveryReceipt(envelope, metadata, receiptMessage, senderRecipient.id, batchCache) ReceiptMessage.Type.READ -> handleReadReceipt(context, senderRecipient.id, envelope, metadata, receiptMessage, earlyMessageCacheEntry) ReceiptMessage.Type.VIEWED -> handleViewedReceipt(context, envelope, metadata, receiptMessage, senderRecipient.id, earlyMessageCacheEntry) else -> warn(envelope.timestamp!!, "Unknown recipient message type ${receiptMessage.type}") @@ -39,12 +40,15 @@ object ReceiptMessageProcessor { envelope: Envelope, metadata: EnvelopeMetadata, deliveryReceipt: ReceiptMessage, - senderRecipientId: RecipientId + senderRecipientId: RecipientId, + batchCache: BatchCache ) { log(envelope.timestamp!!, "Processing delivery receipts. Sender: $senderRecipientId, Device: ${metadata.sourceDeviceId}, Timestamps: ${deliveryReceipt.timestamp.joinToString(", ")}") val stopwatch: Stopwatch? = if (VERBOSE) Stopwatch("delivery-receipt", decimalPlaces = 2) else null + SignalTrace.beginSection("ReceiptMessageProcessor#incrementDeliveryReceiptCounts") val missingTargetTimestamps: Set = SignalDatabase.messages.incrementDeliveryReceiptCounts(deliveryReceipt.timestamp, senderRecipientId, envelope.timestamp!!, stopwatch) + SignalTrace.endSection() for (targetTimestamp in missingTargetTimestamps) { warn(envelope.timestamp!!, "[handleDeliveryReceipt] Could not find matching message! targetTimestamp: $targetTimestamp, receiptAuthor: $senderRecipientId") @@ -58,7 +62,7 @@ object ReceiptMessageProcessor { SignalDatabase.pendingPniSignatureMessages.acknowledgeReceipts(senderRecipientId, deliveryReceipt.timestamp, metadata.sourceDeviceId) stopwatch?.split("pni-signatures") - SignalDatabase.messageLog.deleteEntriesForRecipient(deliveryReceipt.timestamp, senderRecipientId, metadata.sourceDeviceId) + batchCache.addMslDelete(senderRecipientId, metadata.sourceDeviceId, deliveryReceipt.timestamp) stopwatch?.split("msl") stopwatch?.stop(TAG) @@ -80,7 +84,9 @@ object ReceiptMessageProcessor { log(envelope.timestamp!!, "Processing read receipts. Sender: $senderRecipientId, Device: ${metadata.sourceDeviceId}, Timestamps: ${readReceipt.timestamp.joinToString(", ")}") + SignalTrace.beginSection("ReceiptMessageProcessor#incrementReadReceiptCounts") val missingTargetTimestamps: Set = SignalDatabase.messages.incrementReadReceiptCounts(readReceipt.timestamp, senderRecipientId, envelope.timestamp!!) + SignalTrace.endSection() if (missingTargetTimestamps.isNotEmpty()) { val selfId = Recipient.self().id diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkMetrics.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkMetrics.kt new file mode 100644 index 0000000000..f22cc8a696 --- /dev/null +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkMetrics.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.benchmark + +import androidx.benchmark.macro.ExperimentalMetricApi +import androidx.benchmark.macro.TraceSectionMetric +import androidx.benchmark.macro.TraceSectionMetric.Mode + +@OptIn(ExperimentalMetricApi::class) +object BenchmarkMetrics { + + val incomingMessageObserver: List + get() = listOf( + TraceSectionMetric("IncomingMessageObserver#decryptMessage", Mode.Average), + TraceSectionMetric("IncomingMessageObserver#perMessageTransaction", Mode.Average), + TraceSectionMetric("IncomingMessageObserver#processMessage", Mode.Average), + TraceSectionMetric("IncomingMessageObserver#totalProcessing", Mode.Sum) + ) + + val dataMessageProcessor: List + get() = listOf( + TraceSectionMetric("DataMessageProcessor#gv2PreProcessing", Mode.Average), + TraceSectionMetric("DataMessageProcessor#messageInsert", Mode.Average), + TraceSectionMetric("DataMessageProcessor#postProcess", Mode.Average) + ) + + val messageContentProcessor: List + get() = listOf( + TraceSectionMetric("MessageContentProcessor#handleMessage", Mode.Average) + ) + + val deliveryReceipt: List + get() = listOf( + TraceSectionMetric("ReceiptMessageProcessor#incrementDeliveryReceiptCounts", Mode.Average) + ) + + val readReceipt: List + get() = listOf( + TraceSectionMetric("ReceiptMessageProcessor#incrementReadReceiptCounts", Mode.Average) + ) +} diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt index e3c2a7fb27..0f63a4441b 100644 --- a/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt @@ -22,6 +22,14 @@ object BenchmarkSetup { device.benchmarkCommandBroadcast("group-send") } + fun setupGroupDeliveryReceipt(device: UiDevice) { + device.benchmarkCommandBroadcast("group-delivery-receipt") + } + + fun setupGroupReadReceipt(device: UiDevice) { + device.benchmarkCommandBroadcast("group-read-receipt") + } + fun releaseMessages(device: UiDevice) { device.benchmarkCommandBroadcast("release-messages") } diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/GroupMessageProcessingBenchmarks.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/GroupMessageProcessingBenchmarks.kt index 7554f760c7..4f637c7196 100644 --- a/benchmark/src/main/java/org/thoughtcrime/benchmark/GroupMessageProcessingBenchmarks.kt +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/GroupMessageProcessingBenchmarks.kt @@ -8,11 +8,11 @@ package org.thoughtcrime.benchmark import androidx.annotation.RequiresApi import androidx.benchmark.macro.CompilationMode import androidx.benchmark.macro.ExperimentalMetricApi -import androidx.benchmark.macro.TraceSectionMetric -import androidx.benchmark.macro.TraceSectionMetric.Mode +import androidx.benchmark.macro.MacrobenchmarkScope import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import org.junit.Rule import org.junit.Test @@ -30,62 +30,22 @@ class GroupMessageProcessingBenchmarks { @Test fun groupMessageReceiveOnConversationList() { - run(withConversationOpen = false) + runGroupMessageReceive(withConversationOpen = false) } @Test - fun individualMessageReceiveOnConversation() { - run(withConversationOpen = true) + fun groupMessageReceiveOnConversation() { + runGroupMessageReceive(withConversationOpen = true) } - private fun run(withConversationOpen: Boolean) { + private fun runGroupMessageReceive(withConversationOpen: Boolean) { benchmarkRule.measureRepeated( packageName = "org.thoughtcrime.securesms.benchmark", - metrics = listOf( - TraceSectionMetric( - sectionName = "IncomingMessageObserver#decryptMessage", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "IncomingMessageObserver#perMessageTransaction", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "DataMessageProcessor#gv2PreProcessing", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "DataMessageProcessor#messageInsert", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "DataMessageProcessor#postProcess", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "IncomingMessageObserver#processMessage", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "IncomingMessageObserver#totalProcessing", - mode = Mode.Sum - ) - ), + metrics = BenchmarkMetrics.incomingMessageObserver + BenchmarkMetrics.messageContentProcessor + BenchmarkMetrics.dataMessageProcessor, iterations = 5, compilationMode = CompilationMode.Partial(), setupBlock = { - BenchmarkSetup.setup("group-message-send", device) - - killProcess() - startActivityAndWait() - device.waitForIdle() - - BenchmarkSetup.setupGroupSend(device) - - val uiObject = device.wait(Until.findObject(By.textContains("Title")), 5_000) - if (withConversationOpen) { - uiObject.click() - } + setupGroup("group-message-send", BenchmarkSetup::setupGroupSend, withConversationOpen) } ) { @@ -94,4 +54,76 @@ class GroupMessageProcessingBenchmarks { device.wait(Until.hasObject(By.textContains("505")),10_000L) } } + + @Test + fun groupDeliveryReceiptOnConversationList() { + runGroupDeliveryReceipt(withConversationOpen = false) + } + + @Test + fun groupDeliveryReceiptOnConversation() { + runGroupDeliveryReceipt(withConversationOpen = true) + } + + private fun runGroupDeliveryReceipt(withConversationOpen: Boolean) { + benchmarkRule.measureRepeated( + packageName = "org.thoughtcrime.securesms.benchmark", + metrics = BenchmarkMetrics.incomingMessageObserver + BenchmarkMetrics.messageContentProcessor + BenchmarkMetrics.deliveryReceipt, + iterations = 5, + compilationMode = CompilationMode.Partial(), + setupBlock = { + setupGroup("group-delivery-receipt", BenchmarkSetup::setupGroupDeliveryReceipt, withConversationOpen) + } + ) { + BenchmarkSetup.releaseMessages(device) + + Thread.sleep(10_000) + } + } + + @Test + fun groupReadReceiptOnConversationList() { + runGroupReadReceipt(withConversationOpen = false) + } + + @Test + fun groupReadReceiptOnConversation() { + runGroupReadReceipt(withConversationOpen = true) + } + + private fun runGroupReadReceipt(withConversationOpen: Boolean) { + benchmarkRule.measureRepeated( + packageName = "org.thoughtcrime.securesms.benchmark", + metrics = BenchmarkMetrics.incomingMessageObserver + BenchmarkMetrics.messageContentProcessor + BenchmarkMetrics.readReceipt, + iterations = 5, + compilationMode = CompilationMode.Partial(), + setupBlock = { + setupGroup("group-read-receipt", BenchmarkSetup::setupGroupReadReceipt, withConversationOpen) + } + ) { + BenchmarkSetup.releaseMessages(device) + + Thread.sleep(10_000) + } + } + + private fun MacrobenchmarkScope.setupGroup( + setupType: String, + prepareCommand: (UiDevice) -> Unit, + withConversationOpen: Boolean + ) { + BenchmarkSetup.setup(setupType, device) + + killProcess() + startActivityAndWait() + device.waitForIdle() + + prepareCommand(device) + + device.wait(Until.findObject(By.textContains("Title")), 5_000) + if (withConversationOpen) { + device.waitForIdle() + device.findObject(By.textContains("Title")).click() + } + } } diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt index c9dccfb7fe..be576d7d95 100644 --- a/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt @@ -8,8 +8,6 @@ package org.thoughtcrime.benchmark import androidx.annotation.RequiresApi import androidx.benchmark.macro.CompilationMode import androidx.benchmark.macro.ExperimentalMetricApi -import androidx.benchmark.macro.TraceSectionMetric -import androidx.benchmark.macro.TraceSectionMetric.Mode import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.uiautomator.By @@ -41,24 +39,7 @@ class MessageProcessingBenchmarks { private fun run(withConversationOpen: Boolean) { benchmarkRule.measureRepeated( packageName = "org.thoughtcrime.securesms.benchmark", - metrics = listOf( - TraceSectionMetric( - sectionName = "IncomingMessageObserver#decryptMessage", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "MessageContentProcessor#handleMessage", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "IncomingMessageObserver#processMessage", - mode = Mode.Average - ), - TraceSectionMetric( - sectionName = "IncomingMessageObserver#totalProcessing", - mode = Mode.Sum - ) - ), + metrics = BenchmarkMetrics.incomingMessageObserver + BenchmarkMetrics.messageContentProcessor, iterations = 5, compilationMode = CompilationMode.Partial(), setupBlock = {