Add a search benchmark.

This commit is contained in:
Greyson Parrelli
2026-06-18 09:21:08 -04:00
parent c4846d92da
commit fe0f7ee5e7
3 changed files with 138 additions and 11 deletions
@@ -32,6 +32,14 @@ class BenchmarkSetupActivity : BaseActivity() {
companion object {
private val TAG = Log.tag(BenchmarkSetupActivity::class)
const val SEARCH_KEYWORD = "lighthouse"
private val SEARCH_VOCABULARY = listOf(
"hello", "world", "signal", "android", "kotlin", "database", "benchmark", "conversation",
"morning", "evening", "weekend", "project", "meeting", "dinner", "coffee", "garden",
"mountain", "river", "forest", "harbor", "market", "library", "concert", "holiday"
)
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -51,6 +59,7 @@ class BenchmarkSetupActivity : BaseActivity() {
when (intent.extras!!.getString("setup-type")) {
"cold-start" -> setupColdStart()
"conversation-open" -> setupConversationOpen()
"conversation-list-search" -> setupConversationListSearch()
"message-send" -> setupMessageSend()
"group-message-send" -> setupGroupMessageSend()
"group-delivery-receipt" -> setupGroupReceipt(includeMsl = true)
@@ -97,6 +106,39 @@ class BenchmarkSetupActivity : BaseActivity() {
}
}
private fun setupConversationListSearch() {
TestUsers.setupSelf()
val recipientCount = 50
val messagesPerRecipient = 2000
val totalMessages = recipientCount * messagesPerRecipient
val generator = TestMessages.TimestampGenerator(System.currentTimeMillis() - (totalMessages * 2000L) - 60_000L)
TestUsers.setupTestRecipients(recipientCount).forEachIndexed { recipientIndex, recipientId ->
val recipient: Recipient = Recipient.resolved(recipientId)
for (i in 0 until messagesPerRecipient) {
val body = searchableMessageBody(recipientIndex, i)
if (i % 2 == 0) {
TestMessages.insertIncomingTextMessage(other = recipient, body = body, timestamp = generator.nextTimestamp())
} else {
TestMessages.insertOutgoingTextMessage(other = recipient, body = body, timestamp = generator.nextTimestamp())
}
}
SignalDatabase.messages.setAllMessagesRead()
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
}
}
private fun searchableMessageBody(recipientIndex: Int, messageIndex: Int): String {
val words = SEARCH_VOCABULARY
val w1 = words[(recipientIndex + messageIndex) % words.size]
val w2 = words[(recipientIndex * 7 + messageIndex * 3) % words.size]
val w3 = words[(recipientIndex * 13 + messageIndex * 5) % words.size]
return "$w1 $w2 $SEARCH_KEYWORD $w3 message $messageIndex"
}
private fun setupMessageSend() {
TestUsers.setupSelf()
TestUsers.setupTestClients(1)
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SignalTrace;
import org.signal.core.util.Util;
import org.signal.core.util.concurrent.SerialExecutor;
@@ -82,26 +83,36 @@ public class SearchRepository {
@WorkerThread
public @NonNull ThreadSearchResult queryThreadsSync(@NonNull String query, boolean unreadOnly) {
long start = System.currentTimeMillis();
List<ThreadWithRecipient> result = queryConversations(query, unreadOnly);
SignalTrace.beginSection("ConversationListSearch-Threads");
try {
long start = System.currentTimeMillis();
List<ThreadWithRecipient> result = queryConversations(query, unreadOnly);
Log.d(TAG, "[threads] Search took " + (System.currentTimeMillis() - start) + " ms");
Log.d(TAG, "[threads] Search took " + (System.currentTimeMillis() - start) + " ms");
return new ThreadSearchResult(result, query);
return new ThreadSearchResult(result, query);
} finally {
SignalTrace.endSection();
}
}
@WorkerThread
public @NonNull MessageSearchResult queryMessagesSync(@NonNull String query, @NonNull SearchFilter filter) {
long start = System.currentTimeMillis();
SignalTrace.beginSection("ConversationListSearch-Messages");
try {
long start = System.currentTimeMillis();
List<MessageResult> messages = queryMessages(query, filter);
List<MessageResult> mentionMessages = queryMentions(convertMentionsQueryToTokens(query));
List<MessageResult> filteredMentions = filterMentionResults(mentionMessages, filter);
List<MessageResult> combined = mergeMessagesAndMentions(messages, filteredMentions);
List<MessageResult> messages = queryMessages(query, filter);
List<MessageResult> mentionMessages = queryMentions(convertMentionsQueryToTokens(query));
List<MessageResult> filteredMentions = filterMentionResults(mentionMessages, filter);
List<MessageResult> combined = mergeMessagesAndMentions(messages, filteredMentions);
Log.d(TAG, "[messages] Search took " + (System.currentTimeMillis() - start) + " ms");
Log.d(TAG, "[messages] Search took " + (System.currentTimeMillis() - start) + " ms");
return new MessageSearchResult(combined, query);
return new MessageSearchResult(combined, query);
} finally {
SignalTrace.endSection();
}
}
public void query(@NonNull String query, long threadId, @NonNull Callback<List<MessageResult>> callback) {
@@ -0,0 +1,74 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.benchmark
import android.Manifest
import android.os.Build
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 for searching from the conversation list.
*
* Seeds 50 conversations with 2,000 messages each (100,000 messages total), then performs the same
* operations the app runs when a user searches from the conversation list: opening the search
* toolbar, typing a query, and waiting for results. Measures the full-text search against the
* search table via the [SearchRepository] trace sections.
*/
@RunWith(AndroidJUnit4::class)
@RequiresApi(31)
class SearchBenchmarks {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@OptIn(ExperimentalMetricApi::class)
@Test
fun conversationListSearch() {
var setup = false
benchmarkRule.measureRepeated(
packageName = "org.thoughtcrime.securesms.benchmark",
metrics = listOf(
TraceSectionMetric("ConversationListSearch-Messages", Mode.Sum),
TraceSectionMetric("ConversationListSearch-Threads", Mode.Sum)
),
iterations = 3,
compilationMode = CompilationMode.Partial(),
setupBlock = {
if (!setup) {
BenchmarkSetup.setup("conversation-list-search", device, timeout = 600_000L)
setup = true
}
killProcess()
if (Build.VERSION.SDK_INT >= 33) {
device.executeShellCommand("pm grant $packageName ${Manifest.permission.POST_NOTIFICATIONS}")
}
startActivityAndWait()
device.waitForIdle()
}
) {
device.findObject(By.desc("Search")).click()
val searchField = device.wait(Until.findObject(By.clazz("android.widget.EditText")), 10_000L)
searchField.text = SEARCH_QUERY
device.wait(Until.hasObject(By.textContains("Buddy")), 10_000L)
}
}
companion object {
private const val SEARCH_QUERY = "lighthouse"
}
}