mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-20 17:57:29 +00:00
Add incoming group message benchmark tests.
This commit is contained in:
committed by
Alex Hart
parent
e67307a961
commit
08254edae6
@@ -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\"}")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WebSocketRequestMessage>()
|
||||
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<WebSocketRequestMessage> {
|
||||
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<WebSocketRequestMessage>) {
|
||||
@@ -105,10 +118,21 @@ class BenchmarkWebSocketConnection : WebSocketConnection {
|
||||
request: WebSocketRequestMessage,
|
||||
timeoutSeconds: Long
|
||||
): Single<WebsocketResponse> {
|
||||
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"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<OtherClient> 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 {
|
||||
|
||||
@@ -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<Envelope> {
|
||||
val envelopes = ArrayList<Envelope>(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<Envelope> {
|
||||
val envelopes = ArrayList<Envelope>(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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<RecipientId> {
|
||||
val others = mutableListOf<RecipientId>()
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +457,7 @@ class IncomingMessageObserver(
|
||||
SignalLocalMetrics.PushWebsocketFetch.onProcessedBatch()
|
||||
|
||||
if (!hasMore && !decryptionDrained) {
|
||||
SignalTrace.endSection()
|
||||
Log.i(TAG, "Decryptions newly-drained.")
|
||||
decryptionDrained = true
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<queries>
|
||||
<package android:name="org.thoughtcrime.securesms" />
|
||||
<package android:name="org.thoughtcrime.securesms.benchmark" />
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user