Fix all media storage overview performance.

This commit is contained in:
Greyson Parrelli
2026-03-21 23:38:33 -04:00
committed by Cody Henthorne
parent a8ba0dccca
commit 7a2eca3bd5
8 changed files with 139 additions and 35 deletions
@@ -297,7 +297,8 @@ class AttachmentTable(
"CREATE INDEX IF NOT EXISTS $DATA_FILE_INDEX ON $TABLE_NAME ($DATA_FILE);",
"CREATE INDEX IF NOT EXISTS attachment_archive_transfer_state ON $TABLE_NAME ($ARCHIVE_TRANSFER_STATE);",
"CREATE INDEX IF NOT EXISTS attachment_remote_digest_index ON $TABLE_NAME ($REMOTE_DIGEST);",
"CREATE INDEX IF NOT EXISTS attachment_metadata_id ON $TABLE_NAME ($METADATA_ID);"
"CREATE INDEX IF NOT EXISTS attachment_metadata_id ON $TABLE_NAME ($METADATA_ID);",
"CREATE INDEX IF NOT EXISTS attachment_media_overview_size ON $TABLE_NAME ($DATA_SIZE DESC, $DISPLAY_ORDER DESC) WHERE $QUOTE = 0 AND $STICKER_PACK_ID IS NULL AND $DATA_FILE IS NOT NULL"
)
private val DATA_FILE_INFO_PROJECTION = arrayOf(
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import androidx.compose.runtime.Immutable
import org.signal.core.util.logging.Log
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
@@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.util.MediaUtil.SlideType
class MediaTable internal constructor(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper) {
companion object {
private val TAG = Log.tag(MediaTable::class)
const val ALL_THREADS = -1
private const val THREAD_RECIPIENT_ID = "THREAD_RECIPIENT_ID"
private val BASE_MEDIA_QUERY = """
@@ -64,16 +66,12 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID},
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID,
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS}
FROM
${AttachmentTable.TABLE_NAME}
LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}
WHERE
${AttachmentTable.MESSAGE_ID} IN (
SELECT ${MessageTable.ID}
FROM ${MessageTable.TABLE_NAME}
WHERE ${MessageTable.THREAD_ID} __EQUALITY__ ?
) AND
FROM
${AttachmentTable.TABLE_NAME} __INDEX_HINT__
LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}
WHERE
__THREAD_FILTER__ AND
(%s) AND
${MessageTable.VIEW_ONCE} = 0 AND
${MessageTable.STORY_TYPE} = 0 AND
@@ -216,7 +214,25 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
)
private fun applyEqualityOperator(threadId: Long, query: String): String {
return query.replace("__EQUALITY__", if (threadId == ALL_THREADS.toLong()) "!=" else "=")
val isAllThreads = threadId == ALL_THREADS.toLong()
return query
.replace(
"__THREAD_FILTER__",
if (isAllThreads) {
"${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} != ?"
} else {
"${AttachmentTable.MESSAGE_ID} IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.THREAD_ID} = ?)"
}
)
.replace("__EQUALITY__", if (isAllThreads) "!=" else "=")
.replace("__INDEX_HINT__", "")
}
private fun applyIndexHint(query: String, threadId: Long, sorting: Sorting): String {
if (threadId == ALL_THREADS.toLong() && sorting == Sorting.Largest) {
return query.replace("__INDEX_HINT__", "INDEXED BY attachment_media_overview_size")
}
return query.replace("__INDEX_HINT__", "")
}
}
@@ -232,15 +248,27 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
return readableDatabase.rawQuery(query, args)
}
fun getDocumentMediaForThread(threadId: Long, sorting: Sorting): Cursor {
val query = sorting.applyToQuery(applyEqualityOperator(threadId, DOCUMENT_MEDIA_QUERY))
@JvmOverloads
fun getDocumentMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor {
var query = sorting.applyToQuery(applyEqualityOperator(threadId, DOCUMENT_MEDIA_QUERY))
val args = arrayOf(threadId.toString() + "")
if (limit > 0) {
query = "$query LIMIT $limit"
}
return readableDatabase.rawQuery(query, args)
}
fun getAudioMediaForThread(threadId: Long, sorting: Sorting): Cursor {
val query = sorting.applyToQuery(applyEqualityOperator(threadId, AUDIO_MEDIA_QUERY))
@JvmOverloads
fun getAudioMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor {
var query = sorting.applyToQuery(applyEqualityOperator(threadId, AUDIO_MEDIA_QUERY))
val args = arrayOf(threadId.toString() + "")
if (limit > 0) {
query = "$query LIMIT $limit"
}
return readableDatabase.rawQuery(query, args)
}
@@ -255,9 +283,15 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
return readableDatabase.rawQuery(query, args)
}
fun getAllMediaForThread(threadId: Long, sorting: Sorting): Cursor {
val query = sorting.applyToQuery(applyEqualityOperator(threadId, ALL_MEDIA_QUERY))
@JvmOverloads
fun getAllMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor {
var query = sorting.applyToQuery(applyEqualityOperator(threadId, applyIndexHint(ALL_MEDIA_QUERY, threadId, sorting)))
val args = arrayOf(threadId.toString() + "")
if (limit > 0) {
query = "$query LIMIT $limit"
}
return readableDatabase.rawQuery(query, args)
}
@@ -163,6 +163,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V306_AddRemoteDelet
import org.thoughtcrime.securesms.database.helpers.migration.V308_AddBackRemoteDeletedColumn
import org.thoughtcrime.securesms.database.helpers.migration.V309_GroupTerminatedColumnMigration
import org.thoughtcrime.securesms.database.helpers.migration.V310_AddStarredColumn
import org.thoughtcrime.securesms.database.helpers.migration.V311_AddAttachmentMediaOverviewSizeIndex
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -333,10 +334,11 @@ object SignalDatabaseMigrations {
// 307 to V307_RemoveRemoteDeletedColumn - Removed due to unsolvable OOM crashes. [TODO]: Attempt to fix in the future
308 to V308_AddBackRemoteDeletedColumn,
309 to V309_GroupTerminatedColumnMigration,
310 to V310_AddStarredColumn
310 to V310_AddStarredColumn,
311 to V311_AddAttachmentMediaOverviewSizeIndex
)
const val DATABASE_VERSION = 310
const val DATABASE_VERSION = 311
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* Adds a partial index on attachment (data_size, display_order) to speed up
* the "all threads" media overview sorted by largest.
*/
@Suppress("ClassName")
object V311_AddAttachmentMediaOverviewSizeIndex : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_media_overview_size ON attachment (data_size DESC, display_order DESC) WHERE quote = 0 AND sticker_pack_id IS NULL AND data_file IS NOT NULL")
}
}
@@ -30,16 +30,19 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThrea
private final MediaLoader.MediaType mediaType;
private final MediaTable.Sorting sorting;
private final long threadId;
private final int limit;
public GroupedThreadMediaLoader(@NonNull Context context,
long threadId,
@NonNull MediaLoader.MediaType mediaType,
@NonNull MediaTable.Sorting sorting)
@NonNull MediaTable.Sorting sorting,
int limit)
{
super(context);
this.threadId = threadId;
this.mediaType = mediaType;
this.sorting = sorting;
this.limit = limit;
this.observer = () -> ThreadUtil.runOnMain(this::onContentChanged);
onContentChanged();
@@ -73,7 +76,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThrea
AppDependencies.getDatabaseObserver().registerAttachmentUpdatedObserver(observer);
try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting)) {
try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting, limit)) {
while (cursor != null && cursor.moveToNext()) {
mediaGrouping.add(MediaTable.MediaRecord.from(cursor));
}
@@ -37,7 +37,7 @@ public final class RecipientMediaLoader extends MediaLoader {
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(recipientId));
return ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting);
return ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting, 0);
}
}
@@ -27,21 +27,22 @@ public final class ThreadMediaLoader extends MediaLoader {
@Override
public Cursor getCursor() {
return createThreadMediaCursor(context, threadId, mediaType, sorting);
return createThreadMediaCursor(context, threadId, mediaType, sorting, 0);
}
static Cursor createThreadMediaCursor(@NonNull Context context,
long threadId,
@NonNull MediaType mediaType,
@NonNull MediaTable.Sorting sorting) {
@NonNull MediaTable.Sorting sorting,
int limit) {
MediaTable mediaDatabase = SignalDatabase.media();
switch (mediaType) {
case GALLERY : return mediaDatabase.getGalleryMediaForThread(threadId, sorting);
case DOCUMENT: return mediaDatabase.getDocumentMediaForThread(threadId, sorting);
case AUDIO : return mediaDatabase.getAudioMediaForThread(threadId, sorting);
case GALLERY : return mediaDatabase.getGalleryMediaForThread(threadId, sorting, limit);
case DOCUMENT: return mediaDatabase.getDocumentMediaForThread(threadId, sorting, limit);
case AUDIO : return mediaDatabase.getAudioMediaForThread(threadId, sorting, limit);
case LINK : return mediaDatabase.getLinkMediaForThread(threadId, sorting);
case ALL : return mediaDatabase.getAllMediaForThread(threadId, sorting);
case ALL : return mediaDatabase.getAllMediaForThread(threadId, sorting, limit);
default : throw new AssertionError();
}
}