Regularly analyze database tables to improve index usage.

This commit is contained in:
Greyson Parrelli 2024-04-09 16:55:25 -04:00 committed by GitHub
parent 713298109a
commit 982f602178
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 169 additions and 0 deletions

View file

@ -1288,6 +1288,12 @@
</intent-filter>
</receiver>
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">

View file

@ -83,6 +83,7 @@ import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
@ -419,6 +420,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
LocalBackupListener.schedule(this);
RotateSenderCertificateListener.schedule(this);
RoutineMessageFetchReceiver.startOrUpdateAlarm(this);
AnalyzeDatabaseAlarmListener.schedule(this);
if (BuildConfig.MANAGES_APP_UPDATES) {
ApkUpdateRefreshListener.schedule(this);

View file

@ -0,0 +1,100 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.getAllTables
import org.signal.core.util.logTime
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JsonJobData
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
/**
* Analyzes the database, updating statistics to ensure that sqlite is using the best indices possible for different queries.
*
* Given that analysis can be slow, this job will only analyze one table at a time, retrying itself so long as there are more tables to analyze.
* This should help protect it against getting canceled by the system for running for too long, while also giving it the ability to save it's place.
*/
class AnalyzeDatabaseJob private constructor(
parameters: Parameters,
private var lastCompletedTable: String?
) : Job(parameters) {
companion object {
val TAG = Log.tag(AnalyzeDatabaseJob::class.java)
const val KEY = "AnalyzeDatabaseJob"
private const val KEY_LAST_COMPLETED_TABLE = "last_completed_table"
}
constructor() : this(
Parameters.Builder()
.setMaxInstancesForFactory(1)
.setLifespan(1.days.inWholeMilliseconds)
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
null
)
override fun serialize(): ByteArray? {
return JsonJobData.Builder()
.putString(KEY_LAST_COMPLETED_TABLE, lastCompletedTable)
.build()
.serialize()
}
override fun getFactoryKey(): String = KEY
override fun run(): Result {
val tables = SignalDatabase.rawDatabase.getAllTables()
.sorted()
.filterNot { it.startsWith("sqlite_") || it.contains("fts_") }
if (tables.isEmpty()) {
Log.w(TAG, "Table list is empty!")
return Result.success()
}
val startingIndex = if (lastCompletedTable != null) {
tables.indexOf(lastCompletedTable) + 1
} else {
0
}
if (startingIndex >= tables.size) {
Log.i(TAG, "Already finished all of the tables!")
return Result.success()
}
val table = tables[startingIndex]
logTime(TAG, "analyze-$table", decimalPlaces = 2) {
SignalDatabase.rawDatabase.execSQL("PRAGMA analysis_limit=1000")
SignalDatabase.rawDatabase.execSQL("ANALYZE $table")
}
if (startingIndex >= tables.size - 1) {
Log.i(TAG, "Finished all of the tables!")
return Result.success()
}
lastCompletedTable = table
return Result.retry(1.seconds.inWholeMilliseconds)
}
override fun onFailure() = Unit
class Factory : Job.Factory<AnalyzeDatabaseJob> {
override fun create(parameters: Parameters, data: ByteArray?): AnalyzeDatabaseJob {
val builder = JsonJobData.deserialize(data)
return AnalyzeDatabaseJob(parameters, builder.getStringOrDefault(KEY_LAST_COMPLETED_TABLE, null))
}
}
}

View file

@ -99,6 +99,7 @@ public final class JobManagerFactories {
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
return new HashMap<String, Job.Factory>() {{
put(AccountConsistencyWorkerJob.KEY, new AccountConsistencyWorkerJob.Factory());
put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory());
put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory());
put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory());
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());

View file

@ -35,6 +35,7 @@ internal class MiscellaneousValues internal constructor(store: KeyValueStore) :
private const val LAST_CDS_FOREGROUND_SYNC = "misc.last_cds_foreground_sync"
private const val LINKED_DEVICE_LAST_ACTIVE_CHECK_TIME = "misc.linked_device.last_active_check_time"
private const val LEAST_ACTIVE_LINKED_DEVICE = "misc.linked_device.least_active"
private const val NEXT_DATABASE_ANALYSIS_TIME = "misc.next_database_analysis_time"
}
public override fun onFirstEverAppLaunch() {
@ -236,4 +237,9 @@ internal class MiscellaneousValues internal constructor(store: KeyValueStore) :
* Details about the least-active linked device.
*/
var leastActiveLinkedDevice: LeastActiveLinkedDevice? by protoValue(LEAST_ACTIVE_LINKED_DEVICE, LeastActiveLinkedDevice.ADAPTER)
/**
* When the next scheduled database analysis is.
*/
var nextDatabaseAnalysisTime: Long by longValue(NEXT_DATABASE_ANALYSIS_TIME, 0)
}

View file

@ -0,0 +1,54 @@
package org.thoughtcrime.securesms.service
import android.content.Context
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.AnalyzeDatabaseJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.toMillis
import java.time.LocalDateTime
/**
* Schedules database analysis to happen everyday at 3am.
*/
class AnalyzeDatabaseAlarmListener : PersistentAlarmManagerListener() {
companion object {
@JvmStatic
fun schedule(context: Context?) {
AnalyzeDatabaseAlarmListener().onReceive(context, getScheduleIntent())
}
}
override fun shouldScheduleExact(): Boolean {
return true
}
override fun getNextScheduledExecutionTime(context: Context): Long {
var nextTime = SignalStore.misc().nextDatabaseAnalysisTime
if (nextTime == 0L) {
nextTime = getNextTime()
SignalStore.misc().nextDatabaseAnalysisTime = nextTime
}
return nextTime
}
override fun onAlarm(context: Context, scheduledTime: Long): Long {
ApplicationDependencies.getJobManager().add(AnalyzeDatabaseJob())
val nextTime = getNextTime()
SignalStore.misc().nextDatabaseAnalysisTime = nextTime
return nextTime
}
private fun getNextTime(): Long {
return LocalDateTime
.now()
.plusDays(1)
.withHour(3)
.withMinute(0)
.withSecond(0)
.toMillis()
}
}