diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58ec3dfb58..97c9b8c237 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -373,6 +373,8 @@ android { isDebuggable = false isMinifyEnabled = true matchingFallbacks += "debug" + applicationIdSuffix = ".benchmark" + buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Benchmark\"") buildConfigField("boolean", "TRACING_ENABLED", "true") buildConfigField("String[]", "UNIDENTIFIED_SENDER_TRUST_ROOTS", "new String[]{ \"BVT/2gHqbrG1xzuIypLIOjFgMtihrMld1/5TGADL6Dhv\"}") diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt index 68b7669adc..c1288d065e 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkCommandReceiver.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.signal.benchmark.network.BenchmarkWebSocketConnection +import org.signal.benchmark.setup.Generator import org.signal.benchmark.setup.Harness import org.signal.core.util.ThreadUtil import org.signal.core.util.logging.Log @@ -43,17 +44,20 @@ class BenchmarkCommandReceiver : BroadcastReceiver() { when (command) { "individual-send" -> handlePrepareIndividualSend() - "release-messages" -> BenchmarkWebSocketConnection.instance.releaseMessages() + "group-send" -> handlePrepareGroupSend() + "release-messages" -> { + BenchmarkWebSocketConnection.instance.startWholeBatchTrace = true + BenchmarkWebSocketConnection.instance.releaseMessages() + } else -> Log.w(TAG, "Unknown command: $command") } } private fun handlePrepareIndividualSend() { - val bobClient = Harness.bobClient + val client = Harness.otherClients[0] // Send message from Bob to Self - val firstPreKeyMessageTimestamp = System.currentTimeMillis() - val encryptedEnvelope = bobClient.encrypt(firstPreKeyMessageTimestamp) + val encryptedEnvelope = client.encrypt(Generator.encryptedTextMessage(System.currentTimeMillis())) runBlocking { launch(Dispatchers.IO) { @@ -70,11 +74,43 @@ class BenchmarkCommandReceiver : BroadcastReceiver() { // Have Bob generate N messages that will be received by Alice val messageCount = 100 - val envelopes = bobClient.generateInboundEnvelopes(messageCount) + val envelopes = client.generateInboundEnvelopes(messageCount) val messages = envelopes.map { e -> e.toWebSocketPayload() } BenchmarkWebSocketConnection.instance.addPendingMessages(messages) + BenchmarkWebSocketConnection.instance.addQueueEmptyMessage() + } + + private fun handlePrepareGroupSend() { + val clients = Harness.otherClients.take(5) + + // Send message from others to Self in the group + val encryptedEnvelopes = clients.map { it.encrypt(Generator.encryptedTextMessage(System.currentTimeMillis(), groupMasterKey = Harness.groupMasterKey)) } + + runBlocking { + launch(Dispatchers.IO) { + BenchmarkWebSocketConnection.instance.run { + Log.i(TAG, "Sending initial group messages from client to establish sessions.") + addPendingMessages(encryptedEnvelopes.map { it.toWebSocketPayload() }) + releaseMessages() + + // Sleep briefly to let the messages be processed. + ThreadUtil.sleep(1000) + } + } + } + + // Have clients generate N group messages that will be received by Alice + clients.forEach { client -> + val messageCount = 100 + val envelopes = client.generateInboundGroupEnvelopes(messageCount, Harness.groupMasterKey) + + val messages = envelopes.map { e -> e.toWebSocketPayload() } + + BenchmarkWebSocketConnection.instance.addPendingMessages(messages) + } + BenchmarkWebSocketConnection.instance.addQueueEmptyMessage() } private fun Envelope.toWebSocketPayload(): WebSocketRequestMessage { diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt index 5c263b2f22..3ecb7fbde2 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/BenchmarkSetupActivity.kt @@ -16,6 +16,7 @@ class BenchmarkSetupActivity : BaseActivity() { "cold-start" -> setupColdStart() "conversation-open" -> setupConversationOpen() "message-send" -> setupMessageSend() + "group-message-send" -> setupGroupMessageSend() } val textView: TextView = TextView(this).apply { @@ -60,6 +61,11 @@ class BenchmarkSetupActivity : BaseActivity() { private fun setupMessageSend() { TestUsers.setupSelf() - TestUsers.setupBob() + TestUsers.setupTestClients(1) + } + + private fun setupGroupMessageSend() { + TestUsers.setupSelf() + TestUsers.setupGroup() } } diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/network/BenchmarkWebSocketConnection.kt b/app/src/benchmarkShared/java/org/signal/benchmark/network/BenchmarkWebSocketConnection.kt index 50afcb3d6b..a812201e0b 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/network/BenchmarkWebSocketConnection.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/network/BenchmarkWebSocketConnection.kt @@ -8,6 +8,8 @@ package org.signal.benchmark.network import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.subjects.BehaviorSubject +import okio.IOException +import org.thoughtcrime.securesms.util.SignalTrace import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState import org.whispersystems.signalservice.internal.websocket.WebSocketConnection import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage @@ -47,6 +49,8 @@ class BenchmarkWebSocketConnection : WebSocketConnection { private val incomingRequests = LinkedList() private val incomingSemaphore = Semaphore(0) + var startWholeBatchTrace = false + @Volatile private var isShutdown = false @@ -77,7 +81,7 @@ class BenchmarkWebSocketConnection : WebSocketConnection { if (isShutdown) { throw SocketException("WebSocket connection closed") } - return incomingRequests.removeFirst() + return getNextRequest() } throw TimeoutException("Timeout exceeded") @@ -85,12 +89,21 @@ class BenchmarkWebSocketConnection : WebSocketConnection { override fun readRequestIfAvailable(): Optional { return if (incomingSemaphore.tryAcquire()) { - Optional.of(incomingRequests.removeFirst()) + Optional.of(getNextRequest()) } else { Optional.empty() } } + private fun getNextRequest(): WebSocketRequestMessage { + if (startWholeBatchTrace) { + startWholeBatchTrace = false + SignalTrace.beginSection("IncomingMessageObserver#totalProcessing") + } + + return incomingRequests.removeFirst() + } + override fun sendResponse(response: WebSocketResponseMessage) = Unit fun addPendingMessages(messages: List) { @@ -105,10 +118,21 @@ class BenchmarkWebSocketConnection : WebSocketConnection { request: WebSocketRequestMessage, timeoutSeconds: Long ): Single { - error("Not yet implemented") + return Single.error(IOException("fake timeout")) } override fun sendKeepAlive() { error("Not yet implemented") } + + fun addQueueEmptyMessage() { + addPendingMessages( + listOf( + WebSocketRequestMessage( + verb = "PUT", + path = "/api/v1/queue/empty" + ) + ) + ) + } } 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 747888969b..523dad2e2f 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/Generator.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/Generator.kt @@ -1,27 +1,44 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.signal.benchmark.setup import okio.ByteString.Companion.toByteString import org.signal.core.models.ServiceId import org.signal.core.util.Base64 import org.signal.core.util.toByteArray +import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith import org.whispersystems.signalservice.api.crypto.ContentHint import org.whispersystems.signalservice.api.crypto.EnvelopeContent import org.whispersystems.signalservice.internal.push.Content 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 java.util.Optional import java.util.UUID object Generator { - fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent { + fun encryptedTextMessage( + now: Long, + message: String = "Test message", + groupMasterKey: GroupMasterKey? = null + ): EnvelopeContent { val content = Content.Builder().apply { dataMessage( DataMessage.Builder().buildWith { body = message timestamp = now + if (groupMasterKey != null) { + groupV2 = GroupContextV2.Builder().buildWith { + masterKey = groupMasterKey.serialize().toByteString() + revision = 1 + } + } } ) } diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/setup/Harness.kt b/app/src/benchmarkShared/java/org/signal/benchmark/setup/Harness.kt index 0064189870..e91967a69a 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/Harness.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/Harness.kt @@ -7,6 +7,7 @@ package org.signal.benchmark.setup import org.signal.core.models.ServiceId.ACI import org.signal.core.util.Base64 +import org.signal.core.util.Hex import org.signal.core.util.UuidUtil import org.signal.libsignal.metadata.certificate.SenderCertificate import org.signal.libsignal.metadata.certificate.ServerCertificate @@ -14,32 +15,39 @@ import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.ecc.ECKeyPair import org.signal.libsignal.protocol.ecc.ECPrivateKey import org.signal.libsignal.protocol.ecc.ECPublicKey +import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.libsignal.zkgroup.profiles.ProfileKey import java.util.Optional import java.util.UUID +import kotlin.random.Random import kotlin.time.Duration object Harness { - val SELF_E164 = "+15555550101" + const val SELF_E164 = "+15555559999" val SELF_ACI = ACI.from(UuidUtil.parseOrThrow("d81b9a54-0ec9-43aa-a73f-7e99280ad53e")) - val BOB_E164 = "+15555551001" - val BOB_ACI = ACI.from(UuidUtil.parseOrThrow("752eb667-75f6-4ed6-ade5-ca4bfd050d3d")) - val BOB_IDENTITY_KEY = IdentityKeyPair(Base64.decode("CiEFbAw403SCGPB+tjqfk+jrH7r9ma1P2hcujqydHRYVzzISIGiWYdWYBBdBzDdF06wgEm+HKcc6ETuWB7Jnvk7Wjw1u")) - val BOB_PROFILE_KEY = ProfileKey(Base64.decode("aJJ/A7GBCSnU9HJ1DdMWcKMMeXQKRUguTlAbtlfo/ik")) + private val OTHERS_IDENTITY_KEY = IdentityKeyPair(Base64.decode("CiEFbAw403SCGPB+tjqfk+jrH7r9ma1P2hcujqydHRYVzzISIGiWYdWYBBdBzDdF06wgEm+HKcc6ETuWB7Jnvk7Wjw1u")) + private val OTHERS_PROFILE_KEY = ProfileKey(Base64.decode("aJJ/A7GBCSnU9HJ1DdMWcKMMeXQKRUguTlAbtlfo/ik")) + + val groupMasterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) val trustRoot = ECKeyPair( ECPublicKey(Base64.decode("BVT/2gHqbrG1xzuIypLIOjFgMtihrMld1/5TGADL6Dhv")), ECPrivateKey(Base64.decode("2B1zU7JQdPol/XWiom4pQXrSrHFeO8jzZ1u7wfrtY3o")) ) - val bobClient: BobClient by lazy { - BobClient( - serviceId = BOB_ACI, - e164 = BOB_E164, - identityKeyPair = BOB_IDENTITY_KEY, - profileKey = BOB_PROFILE_KEY - ) + val otherClients: List by lazy { + val random = Random(4242) + buildList { + (0 until 1000).forEach { i -> + val aci = ACI.from(UUID(random.nextLong(), random.nextLong())) + val e164 = "+1555555%04d".format(i) + val identityKey = OTHERS_IDENTITY_KEY + val profileKey = OTHERS_PROFILE_KEY + + add(OtherClient(aci, e164, identityKey, profileKey)) + } + } } fun createCertificateFor(uuid: UUID, e164: String?, deviceId: Int, identityKey: ECPublicKey, expires: Duration): SenderCertificate { diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/setup/BobClient.kt b/app/src/benchmarkShared/java/org/signal/benchmark/setup/OtherClient.kt similarity index 80% rename from app/src/benchmarkShared/java/org/signal/benchmark/setup/BobClient.kt rename to app/src/benchmarkShared/java/org/signal/benchmark/setup/OtherClient.kt index 8cffb4ea3d..497e2726f2 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/BobClient.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/OtherClient.kt @@ -7,8 +7,6 @@ package org.signal.benchmark.setup import org.signal.benchmark.setup.Generator.toEnvelope import org.signal.core.models.ServiceId -import org.signal.core.util.readToSingleInt -import org.signal.core.util.select import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.SessionBuilder @@ -23,16 +21,14 @@ import org.signal.libsignal.protocol.state.PreKeyRecord import org.signal.libsignal.protocol.state.SessionRecord import org.signal.libsignal.protocol.state.SignedPreKeyRecord import org.signal.libsignal.protocol.util.KeyHelper +import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.thoughtcrime.securesms.crypto.ProfileKeyUtil -import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil -import org.thoughtcrime.securesms.database.KyberPreKeyTable -import org.thoughtcrime.securesms.database.OneTimePreKeyTable import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.SignedPreKeyTable import org.thoughtcrime.securesms.keyvalue.SignalStore import org.whispersystems.signalservice.api.SignalServiceAccountDataStore import org.whispersystems.signalservice.api.SignalSessionLock +import org.whispersystems.signalservice.api.crypto.EnvelopeContent import org.whispersystems.signalservice.api.crypto.SealedSenderAccess import org.whispersystems.signalservice.api.crypto.SignalServiceCipher import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder @@ -46,12 +42,10 @@ import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds /** - * Welcome to Bob's Client. - * - * Bob is a "fake" client that can start a session with the running app's user, referred to as Alice in this + * This is a "fake" client that can start a session with the running app's user, referred to as Alice in this * code. */ -class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: IdentityKeyPair, val profileKey: ProfileKey) { +class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: IdentityKeyPair, val profileKey: ProfileKey) { private val serviceAddress = SignalServiceAddress(serviceId, e164) private val registrationId = KeyHelper.generateRegistrationId(false) @@ -67,9 +61,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: } /** Inspired by SignalServiceMessageSender#getEncryptedMessage */ - fun encrypt(now: Long): Envelope { - val envelopeContent = Generator.encryptedTextMessage(now) - + fun encrypt(envelopeContent: EnvelopeContent): Envelope { val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null) if (!aciStore.containsSession(getAliceProtocolAddress())) { @@ -81,16 +73,22 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: .toEnvelope(envelopeContent.content.get().dataMessage!!.timestamp!!, getAliceServiceId()) } - fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) { - val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, SealedSenderAccessUtil.getCertificateValidator()) - cipher.decrypt(envelope, serverDeliveredTimestamp) - } - fun generateInboundEnvelopes(count: Int): List { val envelopes = ArrayList(count) var now = System.currentTimeMillis() for (i in 0 until count) { - envelopes += encrypt(now) + envelopes += encrypt(Generator.encryptedTextMessage(now)) + now += 3 + } + + return envelopes + } + + fun generateInboundGroupEnvelopes(count: Int, groupMasterKey: GroupMasterKey): List { + val envelopes = ArrayList(count) + var now = System.currentTimeMillis() + for (i in 0 until count) { + envelopes += encrypt(Generator.encryptedTextMessage(now, groupMasterKey = groupMasterKey)) now += 3 } @@ -102,45 +100,22 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: } private fun getAlicePreKeyBundle(): PreKeyBundle { - val alicePreKeyId = SignalDatabase.rawDatabase - .select(OneTimePreKeyTable.KEY_ID) - .from(OneTimePreKeyTable.TABLE_NAME) - .where("${OneTimePreKeyTable.ACCOUNT_ID} = ?", getAliceServiceId().toString()) - .run() - .readToSingleInt(-1) + val aliceSignedPreKeyRecord = SignalDatabase.signedPreKeys.getAll(getAliceServiceId()).first() - val alicePreKeyRecord = SignalDatabase.oneTimePreKeys.get(getAliceServiceId(), alicePreKeyId)!! - - val aliceSignedPreKeyId = SignalDatabase.rawDatabase - .select(SignedPreKeyTable.KEY_ID) - .from(SignedPreKeyTable.TABLE_NAME) - .where("${SignedPreKeyTable.ACCOUNT_ID} = ?", getAliceServiceId().toString()) - .run() - .readToSingleInt(-1) - - val aliceSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), aliceSignedPreKeyId)!! - - val aliceSignedKyberPreKeyId = SignalDatabase.rawDatabase - .select(KyberPreKeyTable.KEY_ID) - .from(KyberPreKeyTable.TABLE_NAME) - .where("${KyberPreKeyTable.ACCOUNT_ID} = ?", getAliceServiceId().toString()) - .run() - .readToSingleInt(-1) - - val aliceSignedKyberPreKeyRecord = SignalDatabase.kyberPreKeys.get(getAliceServiceId(), aliceSignedKyberPreKeyId)!!.record + val aliceSignedKyberPreKeyRecord = SignalDatabase.kyberPreKeys.getAllLastResort(getAliceServiceId()).first().record return PreKeyBundle( - SignalStore.account.registrationId, - 1, - alicePreKeyId, - alicePreKeyRecord.keyPair.publicKey, - aliceSignedPreKeyId, - aliceSignedPreKeyRecord.keyPair.publicKey, - aliceSignedPreKeyRecord.signature, - getAlicePublicKey(), - aliceSignedKyberPreKeyId, - aliceSignedKyberPreKeyRecord.keyPair.publicKey, - aliceSignedKyberPreKeyRecord.signature + registrationId = SignalStore.account.registrationId, + deviceId = 1, + preKeyId = PreKeyBundle.NULL_PRE_KEY_ID, + preKeyPublic = null, + signedPreKeyId = aliceSignedPreKeyRecord.id, + signedPreKeyPublic = aliceSignedPreKeyRecord.keyPair.publicKey, + signedPreKeySignature = aliceSignedPreKeyRecord.signature, + identityKey = getAlicePublicKey(), + kyberPreKeyId = aliceSignedKyberPreKeyRecord.id, + kyberPreKeyPublic = aliceSignedKyberPreKeyRecord.keyPair.publicKey, + kyberPreKeySignature = aliceSignedKyberPreKeyRecord.signature ) } 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 e035f40f43..6abf86a667 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt @@ -4,10 +4,17 @@ import android.app.Application import android.content.SharedPreferences import android.preference.PreferenceManager import kotlinx.coroutines.runBlocking +import okio.ByteString import org.signal.core.models.ServiceId.ACI import org.signal.core.util.Util import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.SignalProtocolAddress +import org.signal.storageservice.storage.protos.groups.AccessControl +import org.signal.storageservice.storage.protos.groups.Member +import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup +import org.signal.storageservice.storage.protos.groups.local.DecryptedMember +import org.signal.storageservice.storage.protos.groups.local.DecryptedTimer +import org.signal.storageservice.storage.protos.groups.local.EnabledState import org.thoughtcrime.securesms.crypto.MasterSecretUtil import org.thoughtcrime.securesms.crypto.PreKeyUtil import org.thoughtcrime.securesms.crypto.ProfileKeyUtil @@ -140,17 +147,70 @@ object TestUsers { return others } - fun setupBob(): Recipient { - val recipientId = RecipientId.from(SignalServiceAddress(Harness.BOB_ACI, Harness.BOB_E164)) - SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Bob", "Boberson")) + fun setupTestClients(othersCount: Int): List { + val others = mutableListOf() + synchronized(this) { + for (i in 0 until othersCount) { + val otherClient = Harness.otherClients[i] - SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, Harness.BOB_PROFILE_KEY) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true)) - SignalDatabase.recipients.setProfileSharing(recipientId, true) - SignalDatabase.recipients.markRegistered(recipientId, Harness.BOB_ACI) + val recipientId = RecipientId.from(SignalServiceAddress(otherClient.serviceId, otherClient.e164)) + SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) + SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, otherClient.profileKey) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setProfileSharing(recipientId, true) + SignalDatabase.recipients.markRegistered(recipientId, otherClient.serviceId) + AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(otherClient.serviceId.toString(), 1), otherClient.identityKeyPair.publicKey) - AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(Harness.BOB_ACI.toString(), 1), Harness.BOB_IDENTITY_KEY.publicKey) + others += recipientId + } - return Recipient.resolved(recipientId) + generatedOthers += othersCount + } + + return others + } + + fun setupGroup() { + val members = setupTestClients(5) + val self = Recipient.self() + + val fullMembers = buildList { + add(member(aci = self.requireAci())) + addAll(members.map { member(aci = Recipient.resolved(it).requireAci()) }) + } + + val group = DecryptedGroup( + title = "Title", + avatar = "", + disappearingMessagesTimer = DecryptedTimer(), + accessControl = AccessControl(), + revision = 1, + members = fullMembers, + pendingMembers = emptyList(), + requestingMembers = emptyList(), + inviteLinkPassword = ByteString.EMPTY, + description = "Description", + isAnnouncementGroup = EnabledState.DISABLED, + bannedMembers = emptyList(), + isPlaceholderGroup = false + ) + + val groupId = SignalDatabase.groups.create( + groupMasterKey = Harness.groupMasterKey, + groupState = group, + groupSendEndorsements = null + ) + + SignalDatabase.recipients.setProfileSharing(Recipient.externalGroupExact(groupId!!).id, true) + } + + private fun member(aci: ACI, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0, labelEmoji: String = "", labelString: String = ""): DecryptedMember { + return DecryptedMember( + role = role, + aciBytes = aci.toByteString(), + joinedAtRevision = joinedAt, + labelEmoji = labelEmoji, + labelString = labelString + ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt index fb4be9ca00..2ce767800a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt @@ -457,6 +457,7 @@ class IncomingMessageObserver( SignalLocalMetrics.PushWebsocketFetch.onProcessedBatch() if (!hasMore && !decryptionDrained) { + SignalTrace.endSection() Log.i(TAG, "Decryptions newly-drained.") decryptionDrained = true diff --git a/benchmark/src/main/AndroidManifest.xml b/benchmark/src/main/AndroidManifest.xml index 42e60976f7..5e49a2e203 100644 --- a/benchmark/src/main/AndroidManifest.xml +++ b/benchmark/src/main/AndroidManifest.xml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt index 7673f42f99..e3c2a7fb27 100644 --- a/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/BenchmarkSetup.kt @@ -5,10 +5,11 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until object BenchmarkSetup { - private const val TARGET_PACKAGE = "org.thoughtcrime.securesms" + private const val TARGET_PACKAGE = "org.thoughtcrime.securesms.benchmark" private const val RECEIVER = "org.signal.benchmark.BenchmarkCommandReceiver" fun setup(type: String, device: UiDevice) { + device.executeShellCommand("pm clear $TARGET_PACKAGE") device.executeShellCommand("am start -W -n $TARGET_PACKAGE/org.signal.benchmark.BenchmarkSetupActivity --es setup-type $type") device.wait(Until.hasObject(By.textContains("done")), 25_000L) } @@ -17,6 +18,10 @@ object BenchmarkSetup { device.benchmarkCommandBroadcast("individual-send") } + fun setupGroupSend(device: UiDevice) { + device.benchmarkCommandBroadcast("group-send") + } + fun releaseMessages(device: UiDevice) { device.benchmarkCommandBroadcast("release-messages") } diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/ConversationBenchmarks.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/ConversationBenchmarks.kt index a06959dc33..64bd1a6d6f 100644 --- a/benchmark/src/main/java/org/thoughtcrime/benchmark/ConversationBenchmarks.kt +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/ConversationBenchmarks.kt @@ -25,7 +25,7 @@ class ConversationBenchmarks { fun simpleConversationOpen() { var setup = false benchmarkRule.measureRepeated( - packageName = "org.thoughtcrime.securesms", + packageName = "org.thoughtcrime.securesms.benchmark", metrics = listOf( TraceSectionMetric("6-ConversationOpen"), TraceSectionMetric("1-ConversationOpen-ViewModel-Init"), diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/GroupMessageProcessingBenchmarks.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/GroupMessageProcessingBenchmarks.kt new file mode 100644 index 0000000000..930d11f601 --- /dev/null +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/GroupMessageProcessingBenchmarks.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 +import androidx.test.uiautomator.Until +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Macrobenchmark benchmarks for message processing performance. + */ +@OptIn(ExperimentalMetricApi::class) +@RunWith(AndroidJUnit4::class) +@RequiresApi(31) +class GroupMessageProcessingBenchmarks { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun groupMessageReceiveOnConversationList() { + run(withConversationOpen = false) + } + + @Test + fun individualMessageReceiveOnConversation() { + run(withConversationOpen = true) + } + + 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 + ) + ), + 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() + } + } + ) { + + BenchmarkSetup.releaseMessages(device) + + device.wait(Until.hasObject(By.textContains("505")),10_000L) + } + } +} diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt index 9e08efa62e..c9dccfb7fe 100644 --- a/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/MessageProcessingBenchmarks.kt @@ -5,7 +5,6 @@ package org.thoughtcrime.benchmark -import android.os.SystemClock import androidx.annotation.RequiresApi import androidx.benchmark.macro.CompilationMode import androidx.benchmark.macro.ExperimentalMetricApi @@ -14,8 +13,6 @@ import androidx.benchmark.macro.TraceSectionMetric.Mode import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.uiautomator.By -import androidx.test.uiautomator.BySelector -import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import org.junit.Rule import org.junit.Test @@ -23,8 +20,6 @@ import org.junit.runner.RunWith /** * Macrobenchmark benchmarks for message processing performance. - * - * WARNING! THIS WILL WIPE YOUR SIGNAL INSTALL */ @OptIn(ExperimentalMetricApi::class) @RunWith(AndroidJUnit4::class) @@ -44,9 +39,8 @@ class MessageProcessingBenchmarks { } private fun run(withConversationOpen: Boolean) { - var setup = false benchmarkRule.measureRepeated( - packageName = "org.thoughtcrime.securesms", + packageName = "org.thoughtcrime.securesms.benchmark", metrics = listOf( TraceSectionMetric( sectionName = "IncomingMessageObserver#decryptMessage", @@ -59,14 +53,16 @@ class MessageProcessingBenchmarks { TraceSectionMetric( sectionName = "IncomingMessageObserver#processMessage", mode = Mode.Average + ), + TraceSectionMetric( + sectionName = "IncomingMessageObserver#totalProcessing", + mode = Mode.Sum ) ), iterations = 5, compilationMode = CompilationMode.Partial(), setupBlock = { - if (!setup) { - BenchmarkSetup.setup("message-send", device) - } + BenchmarkSetup.setup("message-send", device) killProcess() startActivityAndWait() @@ -74,7 +70,7 @@ class MessageProcessingBenchmarks { BenchmarkSetup.setupIndividualSend(device) - val uiObject = device.wait(Until.findObject(By.textContains("Bob")), 5_000) + val uiObject = device.wait(Until.findObject(By.textContains("Buddy")), 5_000) if (withConversationOpen) { uiObject.click() } @@ -83,31 +79,7 @@ class MessageProcessingBenchmarks { BenchmarkSetup.releaseMessages(device) - device.waitForAny( - 10_000L, - By.textContains("101"), - By.textContains("202"), - By.textContains("303"), - By.textContains("404"), - By.textContains("505"), - ) + device.wait(Until.hasObject(By.textContains("101")), 10_000L) } } } - -/** - * Inspired by [androidx.test.uiautomator.WaitMixin] - */ -fun UiDevice.waitForAny(timeout: Long, vararg conditions: BySelector): Boolean { - val startTime = SystemClock.uptimeMillis() - - var result = conditions.any { this.hasObject(it) } - var elapsedTime = 0L - while (!result && elapsedTime < timeout) { - SystemClock.sleep(100) - result = conditions.any { this.hasObject(it) } - elapsedTime = SystemClock.uptimeMillis() - startTime - } - - return result -} diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/StartupBenchmarks.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/StartupBenchmarks.kt index 802c8f9f25..31bd401b79 100644 --- a/benchmark/src/main/java/org/thoughtcrime/benchmark/StartupBenchmarks.kt +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/StartupBenchmarks.kt @@ -15,8 +15,6 @@ import org.junit.runner.RunWith /** * Macrobenchmark benchmarks for app startup performance. - * - * WARNING! THIS WILL WIPE YOUR SIGNAL INSTALL */ @RunWith(AndroidJUnit4::class) @RequiresApi(31) @@ -38,7 +36,7 @@ class StartupBenchmarks { private fun measureStartup(iterations: Int, compilationMode: CompilationMode) { var setup = false benchmarkRule.measureRepeated( - packageName = "org.thoughtcrime.securesms", + packageName = "org.thoughtcrime.securesms.benchmark", metrics = listOf(StartupTimingMetric(), TraceSectionMetric("ConversationListDataSource#load")), iterations = iterations, startupMode = StartupMode.COLD, diff --git a/benchmark/src/main/java/org/thoughtcrime/benchmark/UIDeviceExt.kt b/benchmark/src/main/java/org/thoughtcrime/benchmark/UIDeviceExt.kt new file mode 100644 index 0000000000..34fbf1209b --- /dev/null +++ b/benchmark/src/main/java/org/thoughtcrime/benchmark/UIDeviceExt.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.benchmark + +import android.os.SystemClock +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice + +/** + * Inspired by [androidx.test.uiautomator.WaitMixin] + */ +fun UiDevice.waitForAny(timeout: Long, vararg conditions: BySelector): Boolean { + val startTime = SystemClock.uptimeMillis() + + var result = conditions.any { this.hasObject(it) } + var elapsedTime = 0L + while (!result && elapsedTime < timeout) { + SystemClock.sleep(100) + result = conditions.any { this.hasObject(it) } + elapsedTime = SystemClock.uptimeMillis() - startTime + } + + return result +}