From 0e549a1565fb727911177ad2a42d76f4196875a2 Mon Sep 17 00:00:00 2001 From: DarkKnight2122 <145188583+DarkKnight2122@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:02:26 +0530 Subject: [PATCH] Revert: Performance Mode PR #126 --- .../common/config/ConfigContainer.kt | 5 +- .../features/impl/messaging/SendOverride.kt | 223 +------ .../features/impl/tweaks/PerformanceMode.kt | 584 +++--------------- 3 files changed, 119 insertions(+), 693 deletions(-) diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt index aeb87e87..4848d287 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt @@ -1,7 +1,6 @@ package me.eternal.purrfectsnap.common.config import android.content.Context -import com.google.gson.JsonNull import com.google.gson.JsonObject import me.eternal.purrfectsnap.common.logger.AbstractLogger import kotlin.reflect.KProperty @@ -80,9 +79,7 @@ open class ConfigContainer( properties.forEach { (propertyKey, propertyValue) -> if (!exportSensitiveData && propertyKey.params.flags.contains(ConfigFlag.SENSITIVE)) return@forEach if (!includeSavedLocations && propertyKey.dataType.type == DataProcessors.Type.MAP_COORDINATES) return@forEach - val serializedValue = propertyValue.getRaw()?.let { - propertyKey.dataType.serializeAny(it, exportSensitiveData, includeSavedLocations) - } ?: JsonNull.INSTANCE + val serializedValue = propertyValue.getRaw()?.let { propertyKey.dataType.serializeAny(it, exportSensitiveData, includeSavedLocations) } json.add(propertyKey.name, serializedValue) } return json diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt index 3a092b38..68734871 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt @@ -1,6 +1,5 @@ package me.eternal.purrfectsnap.core.features.impl.messaging -import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.os.Build @@ -65,37 +64,15 @@ import kotlin.time.toDuration class SendOverride : Feature("Send Override") { companion object { private const val NOTIFICATION_CHANNEL_ID = "scheduled_send" - private const val CONTINUOUS_SEND_CHANNEL_ID = "continuous_send_status" - private const val STATUS_NOTIFICATION_ID = 54322 - private const val COMPLETION_NOTIFICATION_ID = 54323 - - const val ACTION_PAUSE_RESUME = "me.eternal.purrfectsnap.CONTINUOUS_SEND_PAUSE_RESUME" - const val ACTION_STOP = "me.eternal.purrfectsnap.CONTINUOUS_SEND_STOP" - private val internalMultipartSend = ThreadLocal.withInitial { false } private var queuedOriginalItemRepeatCount = 0 private var queuedOriginalItemRepeatOverrideType: String? = null private var queuedOriginalItemRepeatSnapDurationMs: Int? = null - - // Notification & Loop Tracking - private var totalRepeatCount = 0 - private var processedRepeatCount = 0 - private var currentRecipientName: String = "Unknown" - private val isPaused = java.util.concurrent.atomic.AtomicBoolean(false) - private val isStopped = java.util.concurrent.atomic.AtomicBoolean(false) - // State persistence for background operations - private var lastCapturedDestinationsObj: Any? = null - private var lastCapturedMessageContentJson: String? = null - private var lastCapturedOriginalCallback: Any? = null - - private fun queueOriginalItemRepeats(repeatCount: Int, overrideType: String, snapDurationMs: Int?, destinations: Any, contentJson: String, originalCallback: Any?) { + private fun queueOriginalItemRepeats(repeatCount: Int, overrideType: String, snapDurationMs: Int?) { queuedOriginalItemRepeatCount = repeatCount queuedOriginalItemRepeatOverrideType = overrideType queuedOriginalItemRepeatSnapDurationMs = snapDurationMs - lastCapturedDestinationsObj = destinations - lastCapturedMessageContentJson = contentJson - lastCapturedOriginalCallback = originalCallback MediaFilePicker.setQueuedOverrideType(overrideType, snapDurationMs) } @@ -103,68 +80,29 @@ class SendOverride : Feature("Send Override") { queuedOriginalItemRepeatCount = 0 queuedOriginalItemRepeatOverrideType = null queuedOriginalItemRepeatSnapDurationMs = null - totalRepeatCount = 0 - processedRepeatCount = 0 - isPaused.set(false) - isStopped.set(false) - lastCapturedDestinationsObj = null - lastCapturedMessageContentJson = null - lastCapturedOriginalCallback = null - } - } - - private val engineActive = java.util.concurrent.atomic.AtomicBoolean(true) - - private fun updateContinuousSendNotification() { - if (!engineActive.get() || isStopped.get()) return - - val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java) - val remaining = queuedOriginalItemRepeatCount - val processed = processedRepeatCount - val total = totalRepeatCount - val isWorking = (remaining > 0 || (total > 0 && processed < total)) && !isStopped.get() - - if (!isWorking) { - notificationManager.cancel(STATUS_NOTIFICATION_ID) - showCompletionNotification(processed, total) - return } - val progressPercent = if (total > 0) (processed * 100) / total else 0 - - val builder = Notification.Builder(context.androidContext, CONTINUOUS_SEND_CHANNEL_ID) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setSmallIcon(if (isPaused.get()) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play) - .setContentTitle("Sending Snaps to $currentRecipientName") - .setContentText("Progress: $processed / $total ($progressPercent%)") - .setSubText("$processed / $total") - .setProgress(total, processed, false) + private fun handleQueuedOriginalItemRepeatSuccess(): Boolean { + if (queuedOriginalItemRepeatCount <= 0) { + clearQueuedOriginalItemRepeats() + return false + } - val pauseResumeLabel = if (isPaused.get()) "Resume" else "Pause" - builder.addAction(Notification.Action.Builder(null, pauseResumeLabel, createPendingIntent(ACTION_PAUSE_RESUME)).build()) - builder.addAction(Notification.Action.Builder(null, "Stop", createPendingIntent(ACTION_STOP)).build()) + val overrideType = queuedOriginalItemRepeatOverrideType ?: run { + clearQueuedOriginalItemRepeats() + return false + } + val snapDurationMs = queuedOriginalItemRepeatSnapDurationMs - notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) - } - - private fun showCompletionNotification(sent: Int, total: Int) { - val title = if (isStopped.get()) "Continuous Send Stopped" else "Continuous Send Finished" - val content = "Successfully sent $sent / $total snaps to $currentRecipientName" - - val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java) - val builder = Notification.Builder(context.androidContext, CONTINUOUS_SEND_CHANNEL_ID) - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentTitle(title) - .setContentText(content) - .setAutoCancel(true) - - notificationManager.notify(COMPLETION_NOTIFICATION_ID, builder.build()) - } - - private fun createPendingIntent(action: String): android.app.PendingIntent { - val intent = android.content.Intent(action).setPackage(context.androidContext.packageName) - return android.app.PendingIntent.getBroadcast(context.androidContext, action.hashCode(), intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE) + queuedOriginalItemRepeatCount-- + MediaFilePicker.setQueuedOverrideType(overrideType, snapDurationMs) + val result = MediaFilePicker.sendReusableOriginalItem() + if (!result) { + queuedOriginalItemRepeatCount++ + clearQueuedOriginalItemRepeats() + } + return result + } } private var selectedType by mutableStateOf("SNAP") @@ -258,33 +196,6 @@ class SendOverride : Feature("Send Override") { @OptIn(ExperimentalLayoutApi::class) override fun init() { createNotificationChannel() - - val actionReceiver = object : android.content.BroadcastReceiver() { - override fun onReceive(ctx: android.content.Context?, intent: android.content.Intent?) { - when (intent?.action) { - ACTION_PAUSE_RESUME -> { - isPaused.set(!isPaused.get()) - updateContinuousSendNotification() - } - ACTION_STOP -> { - isStopped.set(true) - if (isPaused.get()) { - isPaused.set(false) - } - updateContinuousSendNotification() - } - } - } - } - val filter = android.content.IntentFilter().apply { - addAction(ACTION_PAUSE_RESUME) - addAction(ACTION_STOP) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.androidContext.registerReceiver(actionReceiver, filter, android.content.Context.RECEIVER_NOT_EXPORTED) - } else { - context.androidContext.registerReceiver(actionReceiver, filter) - } val stripMediaMetadata = context.config.messaging.stripMediaMetadata.get() var postSavePolicy: Int? = null @@ -798,13 +709,13 @@ class SendOverride : Feature("Send Override") { } } - fun invokeSendManually(destinations: MessageDestinations, messageContent: MessageContent, callback: Any?) { + fun invokeSendManually(messageContent: MessageContent, callback: Any?) { val conversationManager = conversationManagerInstance ?: error("ConversationManager is null") internalMultipartSend.set(true) try { sendMessageWithContentMethod.invoke( conversationManager, - cloneDestinations(destinations), + cloneDestinations(event.destinations), messageContent.instanceNonNull(), callback ) @@ -814,7 +725,6 @@ class SendOverride : Feature("Send Override") { } fun sendMediaManual( - destinations: MessageDestinations, sourceMessageContent: MessageContent, overrideType: String, snapDurationMs: Int?, @@ -874,7 +784,7 @@ class SendOverride : Feature("Send Override") { .build() } - invokeSendManually(destinations, partContent, callback) + invokeSendManually(partContent, callback) } sendPart(0) @@ -884,12 +794,11 @@ class SendOverride : Feature("Send Override") { postSavePolicy = null val targetReader = ProtoReader(sourceMessageContent.content ?: return false) if (!applyOverride(sourceMessageContent, targetReader, overrideType, snapDurationMs)) return false - invokeSendManually(destinations, sourceMessageContent, completionCallback) + invokeSendManually(sourceMessageContent, completionCallback) return true } fun sendRepeatedMediaManual( - destinations: MessageDestinations, repeatCount: Int, overrideType: String, snapDurationMs: Int? @@ -911,7 +820,7 @@ class SendOverride : Feature("Send Override") { } val preparedContent = createMessageContentFromOriginal() - if (!sendMediaManual(destinations, preparedContent, overrideType, snapDurationMs, callback)) { + if (!sendMediaManual(preparedContent, overrideType, snapDurationMs, callback)) { invokeCallbackError(originalCallback, "Failed to send") } } @@ -934,76 +843,12 @@ class SendOverride : Feature("Send Override") { 10000 } - fun handleQueuedOriginalItemRepeatSuccess(convId: String): Boolean { - if (isStopped.get() || queuedOriginalItemRepeatCount <= 0) { - val processed = processedRepeatCount - val total = totalRepeatCount - clearQueuedOriginalItemRepeats() - context.runOnUiThread { - val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java) - notificationManager.cancel(STATUS_NOTIFICATION_ID) - showCompletionNotification(processed, total) - } - return false - } - - val destinations = lastCapturedDestinationsObj as? MessageDestinations ?: return false - val contentJson = lastCapturedMessageContentJson ?: return false - val originalCb = lastCapturedOriginalCallback - val overrideType = queuedOriginalItemRepeatOverrideType ?: "SNAP" - val snapDurationMs = queuedOriginalItemRepeatSnapDurationMs - - context.coroutineScope.launch { - while (isPaused.get() && !isStopped.get()) { - delay(500) - } - if (isStopped.get()) { - context.runOnUiThread { handleQueuedOriginalItemRepeatSuccess(convId) } - return@launch - } - - delay(1000) - - context.runOnUiThread { - queuedOriginalItemRepeatCount-- - processedRepeatCount++ - updateContinuousSendNotification() - - val repeatedContent = createMessageContentFromOriginal() - - val callback = CallbackBuilder(sendMessageCallbackClass) - .override("onSuccess") { - context.runOnUiThread { - if (!handleQueuedOriginalItemRepeatSuccess(convId)) { - runCatching { - originalCb?.javaClass?.methods?.firstOrNull { it.name == "onSuccess" }?.invoke(originalCb) - } - } - } - } - .override("onError", shouldUnhook = false) { - val error = it.argNullable(0) - runCatching { - originalCb?.javaClass?.methods?.firstOrNull { it.name == "onError" && it.parameterCount == 1 }?.invoke(originalCb, error) - } - clearQueuedOriginalItemRepeats() - val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java) - notificationManager.cancel(STATUS_NOTIFICATION_ID) - } - .build() - - sendMediaManual(destinations, repeatedContent, overrideType, snapDurationMs, callback) - } - } - return true - } - fun attachQueuedRepeatCallbacks(sendEvent: SendMessageWithContentEvent) { sendEvent.addCallbackResult("onSuccess") { context.runOnUiThread { val handledSplit = MediaFilePicker.handleCurrentQueuedItemSuccess() val handledRepeat = if (!handledSplit) { - handleQueuedOriginalItemRepeatSuccess(conversationIds.first()) + handleQueuedOriginalItemRepeatSuccess() } else { false } @@ -1529,7 +1374,6 @@ class SendOverride : Feature("Send Override") { context.bridgeClient.getTaskInterface().updateTaskProgress(taskHash, "Sending...", 100) if (sendRepeatedMediaManual( - MessageDestinations(cloneDestinations(event.destinations)), repeatCount, finalSelectedType, selectedSnapDurationMs @@ -1585,19 +1429,7 @@ class SendOverride : Feature("Send Override") { invokeOriginalAndRestoreResult(event) } } else if (MediaFilePicker.hasReusableOriginalItem()) { - totalRepeatCount = repeatCount - processedRepeatCount = 1 - currentRecipientName = recipientNameForTask - updateContinuousSendNotification() - - queueOriginalItemRepeats( - repeatCount - 1, - finalSelectedType, - selectedSnapDurationMs, - MessageDestinations(cloneDestinations(event.destinations)), - originalMessageJson, - originalCallback - ) + queueOriginalItemRepeats(repeatCount - 1, finalSelectedType, selectedSnapDurationMs) attachQueuedRepeatCallbacks(event) if (sendMedia(finalSelectedType, selectedSnapDurationMs)) { invokeOriginalAndRestoreResult(event) @@ -1606,7 +1438,6 @@ class SendOverride : Feature("Send Override") { } } else { sendRepeatedMediaManual( - MessageDestinations(cloneDestinations(event.destinations)), repeatCount, finalSelectedType, selectedSnapDurationMs diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt index 71b43ff2..5f798a23 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt @@ -2,85 +2,31 @@ package me.eternal.purrfectsnap.core.features.impl.tweaks import android.animation.ValueAnimator import android.app.Activity -import android.app.Dialog -import android.content.Context -import android.database.Cursor -import android.database.MatrixCursor import android.database.sqlite.SQLiteDatabase +import android.hardware.camera2.CaptureRequest import android.media.MediaRecorder +import android.os.Build +import android.transition.Transition import android.os.HandlerThread import android.os.Process -import android.util.Base64 +import android.util.Range import android.view.View +import android.view.ViewPropertyAnimator +import android.view.animation.Animation import android.widget.OverScroller import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager -import java.io.File import java.lang.Thread -import java.lang.reflect.Method -import java.util.LinkedHashMap import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.ThreadPoolExecutor -import com.google.gson.reflect.TypeToken import me.eternal.purrfectsnap.core.features.Feature -import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging -import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent -import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID -import me.eternal.purrfectsnap.mapper.impl.CallbackMapper import me.eternal.purrfectsnap.core.util.hook.HookStage -import me.eternal.purrfectsnap.core.util.hook.findRestrictedMethod import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hookConstructor -import me.eternal.purrfectsnap.core.util.ktx.getObjectField -import me.eternal.purrfectsnap.core.util.ktx.setObjectField import okhttp3.Dispatcher class PerformanceMode : Feature("Performance Mode") { - companion object { - private const val CHAT_FEED_CACHE_MAX_ROWS = 400 - private const val CHAT_FEED_CACHE_MAX_BLOB_BYTES = 512 - private const val CHAT_FEED_CACHE_MIN_REFRESH_INTERVAL_MS = 15_000L - private const val CHAT_FEED_CACHE_MAX_AGE_MS = 5L * 60L * 1000L - private const val CHAT_FEED_CACHE_SCHEMA_VERSION = 2 - private const val MESSAGE_WINDOW_STATE_MAX_CONVERSATIONS = 64 - private const val MESSAGE_WINDOW_STATE_MAX_AGE_MS = 7L * 24L * 60L * 60L * 1000L - private const val SNAP_PREFETCH_GROUP_MESSAGES = 48 - private const val SNAP_PREFETCH_DM_MESSAGES = 24 - private const val REOPEN_WARMUP_GROUP_MESSAGES = 160 - private const val REOPEN_WARMUP_DM_MESSAGES = 96 - } - - private data class SnapshotCell( - val type: Int, - val stringValue: String? = null, - val longValue: Long? = null, - val doubleValue: Double? = null, - val blobValue: String? = null, - ) - - private data class CursorSnapshot( - val columns: List, - val rows: List>, - ) - - private data class ChatFeedSnapshotCache( - val schemaVersion: Int, - val queryKey: String, - val createdAt: Long, - val snapshot: CursorSnapshot, - ) - - private data class MessageWindowState( - val conversationId: String, - val currentSize: Int, - val oldestOrderKey: Long?, - val newestOrderKey: Long?, - val updatedAt: Long, - val isGroup: Boolean, - ) - override fun init() { val profile = context.config.global.performanceMode.profile.getNullable() ?: return val isMaxProfile = profile == "max" @@ -90,7 +36,6 @@ class PerformanceMode : Feature("Performance Mode") { Process.THREAD_PRIORITY_MORE_FAVORABLE } val minimumFrameRate = if (isMaxProfile) 60 else 45 - val minimumRecordingFrameRate = if (isMaxProfile) 30 else 24 val durationScale = if (isMaxProfile) 0.35f else 0.55f val recyclerViewCacheSize = if (isMaxProfile) 64 else 32 val maxRequests = if (isMaxProfile) 192 else 96 @@ -98,16 +43,11 @@ class PerformanceMode : Feature("Performance Mode") { val minimumCoreThreads = if (isMaxProfile) 16 else 8 val prefetchItemCount = if (isMaxProfile) 24 else 12 val maxAnimationDurationMs = if (isMaxProfile) 90L else 140L - val maxScrollDurationMs = if (isMaxProfile) 72 else 180 + val maxScrollDurationMs = if (isMaxProfile) 120 else 180 val preferredRefreshRate = if (isMaxProfile) 120f else 90f - val snapMapTransitionDurationMs = if (isMaxProfile) 0L else 24L - val snapMapCameraDurationMs = if (isMaxProfile) 16L else 64L - val snapMapMoveDurationMs = if (isMaxProfile) 8L else 40L - val snapMapPrefetchZoomDelta = if (isMaxProfile) 6 else 3 - val preferredJavaThreadPriority = if (isMaxProfile) Thread.NORM_PRIORITY + 2 else Thread.NORM_PRIORITY + 1 context.log.info( - "Performance mode enabled: profile=$profile, threadPriority=$threadPriority, minFps=$minimumFrameRate, minRecordingFps=$minimumRecordingFrameRate, durationScale=$durationScale, rvCache=$recyclerViewCacheSize, maxRequests=$maxRequests/$maxRequestsPerHost, minCoreThreads=$minimumCoreThreads, prefetch=$prefetchItemCount, maxAnimMs=$maxAnimationDurationMs, maxScrollMs=$maxScrollDurationMs, preferredRefreshRate=$preferredRefreshRate, snapMapTransitionMs=$snapMapTransitionDurationMs, snapMapCameraMs=$snapMapCameraDurationMs, snapMapMoveMs=$snapMapMoveDurationMs, snapMapPrefetchZoomDelta=$snapMapPrefetchZoomDelta, javaThreadPriority=$preferredJavaThreadPriority", + "Performance mode enabled: profile=$profile, threadPriority=$threadPriority, minFps=$minimumFrameRate, durationScale=$durationScale, rvCache=$recyclerViewCacheSize, maxRequests=$maxRequests/$maxRequestsPerHost, minCoreThreads=$minimumCoreThreads, prefetch=$prefetchItemCount, maxAnimMs=$maxAnimationDurationMs, maxScrollMs=$maxScrollDurationMs, preferredRefreshRate=$preferredRefreshRate", "PerformanceMode" ) @@ -131,217 +71,28 @@ class PerformanceMode : Feature("Performance Mode") { val executorLog = firstHitLogger("ThreadPoolExecutor.constructor") val dispatcherLog = firstHitLogger("OkHttp.Dispatcher.constructor") val animatorLog = firstHitLogger("ValueAnimator.getDurationScale") + val animatorDurationLog = firstHitLogger("ValueAnimator.setDuration") + val viewAnimatorDurationLog = firstHitLogger("ViewPropertyAnimator.setDuration") + val transitionDurationLog = firstHitLogger("Transition.setDuration") + val animationDurationLog = firstHitLogger("Animation.setDuration") val recyclerCtorLog = firstHitLogger("RecyclerView.constructor") val recyclerAdapterLog = firstHitLogger("RecyclerView.setAdapter") val recyclerLayoutManagerLog = firstHitLogger("RecyclerView.setLayoutManager") val sqliteOpenLog = firstHitLogger("SQLiteDatabase.openDatabase") val sqliteCreateLog = firstHitLogger("SQLiteDatabase.openOrCreateDatabase") val mediaRecorderLog = firstHitLogger("MediaRecorder.setVideoFrameRate") + val captureRequestLog = firstHitLogger("CaptureRequest.Builder.set") + val sustainedModeLog = firstHitLogger("Window.setSustainedPerformanceMode") + val refreshRateLog = firstHitLogger("Activity.preferredRefreshRate") val overScrollerLog = firstHitLogger("OverScroller.startScroll") - val mapDialogLog = firstHitLogger("Dialog.show") - val mapViewLog = firstHitLogger("MapView.constructor") - val mapboxNetworkBlockLog = firstHitLogger("SnapMap.telemetryBlock") - val mapCameraAnimLog = firstHitLogger("SnapMap.mapAnimatorDuration") - val mapThreadLog = firstHitLogger("SnapMap.mapThread") - val mapRendererFpsLog = firstHitLogger("SnapMap.mapRendererFps") - val mapTransitionLog = firstHitLogger("SnapMap.transitionOptions") - val mapMoveLog = firstHitLogger("SnapMap.moveDuration") + fun isPerformanceSensitiveThread(name: String?): Boolean { val normalizedName = name?.lowercase() ?: return false - return listOf("codec", "transcod", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any { + return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk").any { normalizedName.contains(it) } } - fun clampPositiveDuration(durationMs: Long, maxDurationMs: Long): Long { - if (durationMs <= 0L) return durationMs - return durationMs.coerceAtMost(maxDurationMs) - } - - val performanceCacheDir = File(context.androidContext.filesDir, "performance_mode_cache").apply { mkdirs() } - val chatFeedSnapshotFile = File(performanceCacheDir, "chat_feed_snapshot.json") - val lastChatFeedSnapshotWrite = AtomicLong(0L) - val chatFeedSnapshotServedThisProcess = AtomicBoolean(false) - - fun invalidateChatFeedSnapshot(reason: String) { - val deleted = runCatching { - if (!chatFeedSnapshotFile.exists()) return@runCatching false - chatFeedSnapshotFile.delete() - }.getOrDefault(false) - chatFeedSnapshotServedThisProcess.set(false) - if (deleted) { - context.log.info("Invalidated chat feed snapshot ($reason)", "PerformanceMode") - } - } - - Activity::class.java.hook("onResume", HookStage.AFTER) { - if (!isMaxProfile) return@hook - chatFeedSnapshotServedThisProcess.set(false) - } - - val windowStatePrefs = context.androidContext.getSharedPreferences("purrfectsnap_perf_message_windows", Context.MODE_PRIVATE) - val messageWindowStates = runCatching { - val raw = windowStatePrefs.getString("states", null).orEmpty() - if (raw.isBlank()) { - LinkedHashMap() - } else { - context.gson.fromJson>( - raw, - object : TypeToken>() {}.type - ) ?: LinkedHashMap() - } - }.getOrElse { LinkedHashMap() } - - fun persistMessageWindowStates() { - runCatching { - windowStatePrefs.edit().putString("states", context.gson.toJson(messageWindowStates)).apply() - }.onFailure { - context.log.error("Failed to persist message window states", it, "PerformanceMode") - } - } - - val snapshotQueryWhitespaceRegex = Regex("\\s+") - fun buildChatFeedSnapshotQueryKey(sql: String): String { - return sql.lowercase() - .replace(snapshotQueryWhitespaceRegex, " ") - .trim() - } - - fun isChatFeedQuery(sql: String): Boolean { - val normalized = buildChatFeedSnapshotQueryKey(sql) - if (!normalized.startsWith("select ")) return false - - val isFriendsFeedViewQuery = - normalized.startsWith("select * from friendsfeedview ") && - normalized.contains(" order by _id ") && - normalized.contains(" limit ") - - val isFeedEntryQuery = - normalized.startsWith("select * from feed_entry ") && - normalized.contains(" order by last_updated_timestamp desc ") && - normalized.contains(" limit ") - - return (isFriendsFeedViewQuery || isFeedEntryQuery) && - !normalized.contains("count(") && - !normalized.contains("select 0") && - !normalized.contains("where key = ?") && - !normalized.contains("where client_conversation_id = ?") - } - - fun cursorCell(cursor: Cursor, index: Int): SnapshotCell { - return when (cursor.getType(index)) { - Cursor.FIELD_TYPE_NULL -> SnapshotCell(Cursor.FIELD_TYPE_NULL) - Cursor.FIELD_TYPE_INTEGER -> SnapshotCell(Cursor.FIELD_TYPE_INTEGER, longValue = cursor.getLong(index)) - Cursor.FIELD_TYPE_FLOAT -> SnapshotCell(Cursor.FIELD_TYPE_FLOAT, doubleValue = cursor.getDouble(index)) - Cursor.FIELD_TYPE_STRING -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index)) - Cursor.FIELD_TYPE_BLOB -> SnapshotCell( - Cursor.FIELD_TYPE_BLOB, - blobValue = cursor.getBlob(index) - ?.takeIf { it.size <= CHAT_FEED_CACHE_MAX_BLOB_BYTES } - ?.let { Base64.encodeToString(it, Base64.NO_WRAP) } - ) - else -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index)) - } - } - - fun snapshotFromCursor(cursor: Cursor): CursorSnapshot? { - val originalPosition = cursor.position - val snapshot = runCatching { - val columns = cursor.columnNames.toList() - val rows = mutableListOf>() - if (cursor.moveToFirst()) { - var rowCount = 0 - do { - rows += columns.indices.map { index -> cursorCell(cursor, index) } - rowCount++ - } while (rowCount < CHAT_FEED_CACHE_MAX_ROWS && cursor.moveToNext()) - } - CursorSnapshot(columns, rows) - }.onFailure { - context.log.error("Failed to snapshot chat feed cursor", it, "PerformanceMode") - }.getOrNull() - - runCatching { cursor.moveToPosition(originalPosition) } - val restoredPosition = runCatching { cursor.position }.getOrNull() - if (restoredPosition != originalPosition) { - context.log.warn( - "Skipping chat feed snapshot write due non-restorable cursor position (from=$originalPosition to=${restoredPosition ?: "unknown"})", - "PerformanceMode" - ) - return null - } - return snapshot - } - - fun snapshotToMatrixCursor(snapshot: CursorSnapshot): MatrixCursor { - return MatrixCursor(snapshot.columns.toTypedArray(), snapshot.rows.size).also { matrixCursor -> - snapshot.rows.forEach { row -> - matrixCursor.addRow(row.map { cell -> - when (cell.type) { - Cursor.FIELD_TYPE_NULL -> null - Cursor.FIELD_TYPE_INTEGER -> cell.longValue - Cursor.FIELD_TYPE_FLOAT -> cell.doubleValue - Cursor.FIELD_TYPE_BLOB -> cell.blobValue?.let { Base64.decode(it, Base64.NO_WRAP) } - else -> cell.stringValue - } - }) - } - } - } - - fun readSnapshot(file: File, expectedQueryKey: String): CursorSnapshot? { - return runCatching { - if (!file.exists()) return null - val cache = context.gson.fromJson(file.readText(Charsets.UTF_8), ChatFeedSnapshotCache::class.java) ?: return null - if (cache.schemaVersion != CHAT_FEED_CACHE_SCHEMA_VERSION) { - runCatching { file.delete() } - return null - } - if (cache.queryKey != expectedQueryKey) { - runCatching { file.delete() } - return null - } - if (System.currentTimeMillis() - cache.createdAt > CHAT_FEED_CACHE_MAX_AGE_MS) { - runCatching { file.delete() } - return null - } - cache.snapshot - }.getOrElse { - runCatching { file.delete() } - null - } - } - - fun writeSnapshot(file: File, queryKey: String, snapshot: CursorSnapshot) { - runCatching { - file.writeText( - context.gson.toJson( - ChatFeedSnapshotCache( - schemaVersion = CHAT_FEED_CACHE_SCHEMA_VERSION, - queryKey = queryKey, - createdAt = System.currentTimeMillis(), - snapshot = snapshot, - ) - ), - Charsets.UTF_8 - ) - }.onFailure { - context.log.error("Failed to persist friend list snapshot", it, "PerformanceMode") - } - } - - context.event.subscribe(NetworkApiRequestEvent::class) { event -> - if (!isMaxProfile) return@subscribe - val url = event.url - if (url.contains("ami/friends")) { - invalidateChatFeedSnapshot("friends-mutation-sync") - } - if (url.contains("mapbox") && (url.contains("events.") || url.contains("telemetry"))) { - event.canceled = true - mapboxNetworkBlockLog("url=$url") - } - } - HandlerThread::class.java.hookConstructor(HookStage.BEFORE) { param -> if (param.args().size < 2) return@hookConstructor val threadName = param.argNullable(0) @@ -366,12 +117,9 @@ class PerformanceMode : Feature("Performance Mode") { val thread = param.thisObject() if (!isPerformanceSensitiveThread(thread.name)) return@hook runCatching { - thread.priority = preferredJavaThreadPriority + thread.priority = Thread.MAX_PRIORITY } threadStartLog("name=${thread.name} priority=${thread.priority}") - if ((thread.name ?: "").contains("map", ignoreCase = true) || (thread.name ?: "").contains("mapbox", ignoreCase = true)) { - mapThreadLog("name=${thread.name} priority=${thread.priority}") - } } ThreadPoolExecutor::class.java.hookConstructor(HookStage.AFTER) { param -> @@ -401,10 +149,47 @@ class PerformanceMode : Feature("Performance Mode") { animatorLog("durationScale=$durationScale") } + ValueAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + animatorDurationLog("requested=$original applied=${param.arg(0)}") + } + + ViewPropertyAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + viewAnimatorDurationLog("requested=$original applied=${param.arg(0)}") + } + + Transition::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + transitionDurationLog("requested=$original applied=${param.arg(0)}") + } + + Animation::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + animationDurationLog("requested=$original applied=${param.arg(0)}") + } + RecyclerView::class.java.hookConstructor(HookStage.AFTER) { param -> val recyclerView = param.thisObject() recyclerView.setItemViewCacheSize(recyclerViewCacheSize) recyclerView.overScrollMode = View.OVER_SCROLL_NEVER + recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null) if (isMaxProfile) { recyclerView.itemAnimator = null } @@ -414,6 +199,7 @@ class PerformanceMode : Feature("Performance Mode") { RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param -> val recyclerView = param.thisObject() recyclerView.setItemViewCacheSize(recyclerViewCacheSize) + recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null) if (isMaxProfile) { recyclerView.itemAnimator = null } @@ -433,6 +219,7 @@ class PerformanceMode : Feature("Performance Mode") { layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS } } + recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null) recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount") } @@ -461,11 +248,8 @@ class PerformanceMode : Feature("Performance Mode") { MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param -> val currentRate = param.arg(0) - val applied = currentRate - .coerceAtLeast(minimumRecordingFrameRate) - .coerceAtMost(if (isMaxProfile) 60 else 45) - if (applied != currentRate) { - param.setArg(0, applied) + if (currentRate < minimumFrameRate) { + param.setArg(0, minimumFrameRate) } mediaRecorderLog("requested=$currentRate applied=${param.arg(0)}") } @@ -481,234 +265,48 @@ class PerformanceMode : Feature("Performance Mode") { } } - OverScroller::class.java.hook("fling", HookStage.BEFORE) { param -> - if (param.args().size >= 10) { - val overX = param.arg(8) - val overY = param.arg(9) - if (overX != 0) param.setArg(8, 0) - if (overY != 0) param.setArg(9, 0) + CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param -> + val key = param.arg>(0) + when (key) { + CaptureRequest.EDGE_MODE -> param.setArg(1, CaptureRequest.EDGE_MODE_FAST) + CaptureRequest.NOISE_REDUCTION_MODE -> param.setArg(1, CaptureRequest.NOISE_REDUCTION_MODE_FAST) + CaptureRequest.HOT_PIXEL_MODE -> param.setArg(1, CaptureRequest.HOT_PIXEL_MODE_FAST) + CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE -> param.setArg(1, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST) + CaptureRequest.CONTROL_AF_MODE -> param.setArg(1, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) + CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE -> param.setArg(1, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF) + CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE -> { + val currentRange = param.argNullable(1) as? Range<*> + val lower = (currentRange?.lower as? Int) ?: minimumFrameRate + val upper = (currentRange?.upper as? Int) ?: minimumFrameRate + if (upper < minimumFrameRate) { + param.setArg(1, Range(lower.coerceAtMost(minimumFrameRate), minimumFrameRate)) + } + } } + captureRequestLog("key=${key.name} value=${param.argNullable(1)}") } fun applyActivityPerformanceTuning(activity: Activity) { runCatching { - activity.window.setWindowAnimations(0) + activity.window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + val display = activity.display + val targetRefreshRate = display?.supportedModes?.maxByOrNull { it.refreshRate }?.refreshRate + ?.coerceAtLeast(preferredRefreshRate) ?: preferredRefreshRate + activity.window.attributes = activity.window.attributes.apply { + this.preferredRefreshRate = targetRefreshRate + } + refreshRateLog("activity=${activity::class.java.name} refreshRate=$targetRefreshRate") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isMaxProfile) { + runCatching { + activity.window.setSustainedPerformanceMode(true) + sustainedModeLog("activity=${activity::class.java.name}") + } } } onNextActivityCreate { applyActivityPerformanceTuning(it) } - - Dialog::class.java.hook("show", HookStage.AFTER) { param -> - val dialog = param.nullableThisObject() as? Dialog ?: return@hook - val window = dialog.window ?: return@hook - runCatching { - window.setWindowAnimations(0) - if (dialog::class.java.name.contains("map", ignoreCase = true) || dialog::class.java.name.contains("snap", ignoreCase = true)) { - mapDialogLog("class=${dialog::class.java.name}") - } - } - } - - runCatching { - findClass("com.mapbox.mapboxsdk.maps.MapView").hookConstructor(HookStage.AFTER) { param -> - val mapView = param.nullableThisObject() as? View ?: return@hookConstructor - mapView.overScrollMode = View.OVER_SCROLL_NEVER - mapViewLog("class=${mapView::class.java.name}") - } - } - - runCatching { - findClass("com.mapbox.mapboxsdk.maps.renderer.MapRenderer").hook("setMaximumFps", HookStage.BEFORE) { param -> - val requested = param.arg(0) - val applied = requested.coerceAtLeast(120) - if (applied != requested) { - param.setArg(0, applied) - } - mapRendererFpsLog("requested=$requested applied=${param.arg(0)}") - } - } - - runCatching { - val nativeMapViewClass = findClass("com.mapbox.mapboxsdk.maps.NativeMapView") - val transitionOptionsClass = findClass("com.mapbox.mapboxsdk.style.layers.TransitionOptions") - val transitionOptionsCtor = transitionOptionsClass.getDeclaredConstructor(Long::class.javaPrimitiveType, Long::class.javaPrimitiveType, Boolean::class.javaPrimitiveType).apply { - isAccessible = true - } - - fun findNativeMapMethod(name: String, predicate: (Method) -> Boolean): Method? { - return nativeMapViewClass.findRestrictedMethod { method -> - method.name == name && predicate(method) - }?.apply { - isAccessible = true - } - } - - val nativeCancelTransitions = findNativeMapMethod("nativeCancelTransitions") { it.parameterCount == 0 } - val nativeSetPrefetchTiles = findNativeMapMethod("nativeSetPrefetchTiles") { it.parameterCount == 1 && it.parameterTypes[0] == Boolean::class.javaPrimitiveType } - val nativeSetPrefetchZoomDelta = findNativeMapMethod("nativeSetPrefetchZoomDelta") { it.parameterCount == 1 && it.parameterTypes[0] == Int::class.javaPrimitiveType } - val nativeSetTransitionDelay = findNativeMapMethod("nativeSetTransitionDelay") { it.parameterCount == 1 && it.parameterTypes[0] == Long::class.javaPrimitiveType } - val nativeSetTransitionDuration = findNativeMapMethod("nativeSetTransitionDuration") { it.parameterCount == 1 && it.parameterTypes[0] == Long::class.javaPrimitiveType } - val nativeSetTransitionOptions = findNativeMapMethod("nativeSetTransitionOptions") { it.parameterCount == 1 && it.parameterTypes[0].name == transitionOptionsClass.name } - - nativeMapViewClass.hookConstructor(HookStage.AFTER) { param -> - val nativeMapView = param.thisObject() - runCatching { - nativeSetPrefetchTiles?.invoke(nativeMapView, true) - nativeSetPrefetchZoomDelta?.invoke(nativeMapView, snapMapPrefetchZoomDelta) - nativeSetTransitionDelay?.invoke(nativeMapView, 0L) - nativeSetTransitionDuration?.invoke(nativeMapView, snapMapTransitionDurationMs) - nativeSetTransitionOptions?.invoke( - nativeMapView, - transitionOptionsCtor.newInstance(snapMapTransitionDurationMs, 0L, false) - ) - nativeCancelTransitions?.invoke(nativeMapView) - mapTransitionLog("transitionMs=$snapMapTransitionDurationMs prefetchZoomDelta=$snapMapPrefetchZoomDelta placementTransitions=false") - } - } - - nativeMapViewClass.findRestrictedMethod { method -> - method.name == "g" && - method.parameterCount == 6 && - method.parameterTypes.last() == Long::class.javaPrimitiveType - }?.hook(HookStage.BEFORE) { param -> - val original = param.arg(5) - val applied = clampPositiveDuration(original, snapMapCameraDurationMs) - if (applied != original) { - param.setArg(5, applied) - } - runCatching { nativeCancelTransitions?.invoke(param.thisObject()) } - mapCameraAnimLog("requested=$original applied=${param.arg(5)}") - } - - nativeMapViewClass.findRestrictedMethod { method -> - method.name == "v" && - method.parameterCount == 3 && - method.parameterTypes[0] == Double::class.javaPrimitiveType && - method.parameterTypes[1] == Double::class.javaPrimitiveType && - method.parameterTypes[2] == Long::class.javaPrimitiveType - }?.hook(HookStage.BEFORE) { param -> - val original = param.arg(2) - val applied = clampPositiveDuration(original, snapMapMoveDurationMs) - if (applied != original) { - param.setArg(2, applied) - } - runCatching { nativeCancelTransitions?.invoke(param.thisObject()) } - mapMoveLog("requested=$original applied=${param.arg(2)}") - } - }.onFailure { - context.log.error("Failed to install Snap Map transition hooks", it, "PerformanceMode") - } - - runCatching { - findClass("com.snapchat.client.messaging.MessageWindowManager\$CppProxy").hook("initWindow", HookStage.BEFORE) { param -> - if (!isMaxProfile) return@hook - - val conversationId = runCatching { - SnapUUID(param.arg(0)).toString() - }.getOrNull()?.takeIf { it.isNotBlank() } ?: return@hook - - val initParams = param.arg(1) - val conversationType = context.database.getConversationType(conversationId) ?: return@hook - val isGroup = conversationType == 1 - val savedState = synchronized(messageWindowStates) { - messageWindowStates[conversationId] - ?.takeIf { System.currentTimeMillis() - it.updatedAt <= MESSAGE_WINDOW_STATE_MAX_AGE_MS } - } - - val enumConstants = initParams.getObjectField("mStartingType")?.javaClass?.enumConstants ?: return@hook - if (savedState != null) { - val restoredMaxSize = if (savedState.isGroup) { - savedState.currentSize.coerceAtLeast(220).coerceAtMost(520) - } else { - savedState.currentSize.coerceAtLeast(140).coerceAtMost(320) - } - val restoredForward = (savedState.currentSize + if (savedState.isGroup) 24 else 16).coerceAtMost(restoredMaxSize) - val restoredBack = if (savedState.isGroup) 180 else 120 - initParams.setObjectField("mStartingType", enumConstants.firstOrNull { it.toString() == "MESSAGE" } ?: return@hook) - initParams.setObjectField("mStartingOrderKey", savedState.oldestOrderKey ?: savedState.newestOrderKey) - initParams.setObjectField("mMaxSize", restoredMaxSize) - initParams.setObjectField("mNumMessagesForward", restoredForward) - initParams.setObjectField("mNumMessagesBack", restoredBack) - - val warmupAmount = if (savedState.isGroup) REOPEN_WARMUP_GROUP_MESSAGES else REOPEN_WARMUP_DM_MESSAGES - val oldestKey = savedState.oldestOrderKey - if (oldestKey != null) { - context.feature(Messaging::class).conversationManager?.fetchConversationWithMessagesPaginated( - conversationId = conversationId, - lastMessageId = oldestKey, - amount = warmupAmount, - onSuccess = {}, - onError = {} - ) - } - } - } - }.onFailure { - context.log.error("Failed to install saved message window restore hooks", it, "PerformanceMode") - } - - context.mappings.useMapper(CallbackMapper::class) { - callbacks.getClass("MessageWindowManagerDelegate")?.hook("onWindowUpdated", HookStage.AFTER) { param -> - if (!isMaxProfile) return@hook - - val conversationId = runCatching { SnapUUID(param.arg(0)).toString() }.getOrNull() ?: return@hook - val update = param.arg(2) - val pagination = update.getObjectField("mPagination") ?: return@hook - val currentSize = pagination.getObjectField("mCurrentSize") as? Int ?: return@hook - val oldestOrderKey = pagination.getObjectField("mOldestOrderKey") as? Long - val newestOrderKey = pagination.getObjectField("mNewestOrderKey") as? Long - val conversationType = context.database.getConversationType(conversationId) ?: 0 - val isGroup = conversationType == 1 - - synchronized(messageWindowStates) { - messageWindowStates[conversationId] = MessageWindowState( - conversationId = conversationId, - currentSize = currentSize.coerceAtMost(if (isGroup) 420 else 260), - oldestOrderKey = oldestOrderKey, - newestOrderKey = newestOrderKey, - updatedAt = System.currentTimeMillis(), - isGroup = isGroup - ) - while (messageWindowStates.size > MESSAGE_WINDOW_STATE_MAX_CONVERSATIONS) { - val eldestKey = messageWindowStates.entries.minByOrNull { it.value.updatedAt }?.key ?: break - messageWindowStates.remove(eldestKey) - } - persistMessageWindowStates() - } - } - } - - runCatching { - findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.BEFORE) { param -> - if (!isMaxProfile) return@hook - val sql = param.argNullable(1) ?: return@hook - if (!isChatFeedQuery(sql)) return@hook - if (chatFeedSnapshotServedThisProcess.get()) return@hook - val queryKey = buildChatFeedSnapshotQueryKey(sql) - readSnapshot(chatFeedSnapshotFile, queryKey)?.let { snapshot -> - param.setResult(snapshotToMatrixCursor(snapshot)) - chatFeedSnapshotServedThisProcess.set(true) - } - } - - findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.AFTER) { param -> - if (!isMaxProfile) return@hook - val sql = param.argNullable(1) ?: return@hook - if (!isChatFeedQuery(sql)) return@hook - val cursor = param.getResult() as? Cursor ?: return@hook - val now = System.currentTimeMillis() - if (now - lastChatFeedSnapshotWrite.get() < CHAT_FEED_CACHE_MIN_REFRESH_INTERVAL_MS) return@hook - val queryKey = buildChatFeedSnapshotQueryKey(sql) - val snapshot = snapshotFromCursor(cursor) ?: return@hook - if (snapshot.rows.isEmpty()) return@hook - writeSnapshot(chatFeedSnapshotFile, queryKey, snapshot) - lastChatFeedSnapshotWrite.set(now) - } - }.onFailure { - context.log.error("Failed to install chat feed cache hooks", it, "PerformanceMode") - } - } }