From 7a2eca3bd521919fe878902961ea1833f34fd486 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Sat, 21 Mar 2026 23:38:33 -0400 Subject: [PATCH] Fix all media storage overview performance. --- .../securesms/database/AttachmentTable.kt | 3 +- .../securesms/database/MediaTable.kt | 68 ++++++++++++++----- .../helpers/SignalDatabaseMigrations.kt | 6 +- ...311_AddAttachmentMediaOverviewSizeIndex.kt | 16 +++++ .../loaders/GroupedThreadMediaLoader.java | 7 +- .../loaders/RecipientMediaLoader.java | 2 +- .../database/loaders/ThreadMediaLoader.java | 13 ++-- .../MediaOverviewPageFragment.java | 59 ++++++++++++++-- 8 files changed, 139 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V311_AddAttachmentMediaOverviewSizeIndex.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt index c6fe7a2bb9..4a92d6596c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index aff3ff633c..543a58f571 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index ff5570e062..a2023ff372 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V311_AddAttachmentMediaOverviewSizeIndex.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V311_AddAttachmentMediaOverviewSizeIndex.kt new file mode 100644 index 0000000000..a0f8428c9c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V311_AddAttachmentMediaOverviewSizeIndex.kt @@ -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") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java index e86a159dd9..b8789c0ba3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java @@ -30,16 +30,19 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader ThreadUtil.runOnMain(this::onContentChanged); onContentChanged(); @@ -73,7 +76,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader 0 && scrollOffset + scrollExtent * 2 >= scrollRange) { + loadingMore = true; + loadLimit *= 2; + LoaderManager.getInstance(MediaOverviewPageFragment.this).restartLoader(0, null, MediaOverviewPageFragment.this); + } + } + }); MediaOverviewViewModel viewModel = MediaOverviewViewModel.getMediaOverviewViewModel(requireActivity()); @@ -158,8 +190,14 @@ public final class MediaOverviewPageFragment extends LoggingFragment .observe(getViewLifecycleOwner(), sorting -> { if (sorting != null) { this.sorting = sorting; + this.loadLimit = threadId == MediaTable.ALL_THREADS ? INITIAL_LOAD_LIMIT : 0; + this.allLoaded = loadLimit == 0; adapter.setShowFileSizes(sorting.isRelatedToFileSize()); - LoaderManager.getInstance(this).restartLoader(0, null, this); + if (isResumed()) { + LoaderManager.getInstance(this).restartLoader(0, null, this); + } else { + pendingLoad = true; + } updateMultiSelect(); } }); @@ -196,7 +234,7 @@ public final class MediaOverviewPageFragment extends LoggingFragment @Override public @NonNull Loader onCreateLoader(int i, Bundle bundle) { - return new GroupedThreadMediaLoader(requireContext(), threadId, mediaType, sorting); + return new GroupedThreadMediaLoader(requireContext(), threadId, mediaType, sorting, loadLimit); } @Override @@ -204,6 +242,15 @@ public final class MediaOverviewPageFragment extends LoggingFragment ((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(groupedThreadMedia); ((MediaGalleryAllAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged(); + if (loadLimit > 0) { + int totalMediaItems = 0; + for (int i = 0; i < groupedThreadMedia.getSectionCount(); i++) { + totalMediaItems += groupedThreadMedia.getSectionItemCount(i); + } + allLoaded = totalMediaItems < loadLimit; + } + + loadingMore = false; noMedia.setVisibility(recyclerView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE); getActivity().invalidateOptionsMenu(); }