Performance Mode bug fixes
This commit is contained in:
@@ -55,9 +55,7 @@ class Global : ConfigContainer() {
|
||||
}
|
||||
}
|
||||
val mediaUploadQualityConfig = container("media_upload_quality", MediaUploadQualityConfig())
|
||||
val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() }.apply {
|
||||
profile.set("max")
|
||||
}
|
||||
val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() }
|
||||
val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "erase_message", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() }
|
||||
val disableMetrics = boolean("disable_metrics") { requireRestart() }
|
||||
val disableStorySections = multiple("disable_story_sections", "friends", "suggested_stories", "following", "discover") { requireRestart(); requireCleanCache() }
|
||||
|
||||
@@ -128,6 +128,16 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
|
||||
}
|
||||
}
|
||||
|
||||
// Background Watchdog: Periodically refreshes UI and verifies engine health
|
||||
this@AutoOpenSnaps.context.coroutineScope.launch(Dispatchers.Default) {
|
||||
while (isActive && engineActive.get()) {
|
||||
if (autoOpenConfig.globalState == true) {
|
||||
updateStatusNotification()
|
||||
}
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
setupReceivers()
|
||||
startEngineWorker()
|
||||
setupDetector()
|
||||
@@ -158,12 +168,12 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
|
||||
processSnapItem(item)
|
||||
lastSnapProcessedAt.set(System.currentTimeMillis())
|
||||
|
||||
// HIGH SPEED: 10ms floor for 20+ snaps/s
|
||||
// Process at natural network speed when safety is disabled
|
||||
val baseDelay = if (currentSpeedText == "Throttled") 3000L else (autoOpenConfig.delayBetweenSnaps as PropertyValue<Int>).get().toLong()
|
||||
if (isSafe) {
|
||||
delay(Random.nextLong(baseDelay, baseDelay + 200))
|
||||
} else {
|
||||
delay(baseDelay.coerceAtMost(10))
|
||||
if (baseDelay > 0) delay(baseDelay)
|
||||
}
|
||||
|
||||
if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
|
||||
@@ -175,6 +185,13 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
|
||||
}
|
||||
|
||||
private suspend fun processSnapItem(item: SnapQueueItem) {
|
||||
// Verify database state on background thread before processing
|
||||
val dbMessage = withContext(Dispatchers.IO) { this@AutoOpenSnaps.context.database.getConversationMessageFromId(item.messageId) }
|
||||
if (dbMessage?.isViewedByUser == 1) {
|
||||
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
|
||||
return
|
||||
}
|
||||
|
||||
currentStatusText = "Active"; updateStatusNotification()
|
||||
var success = false
|
||||
val startTime = System.currentTimeMillis()
|
||||
@@ -186,7 +203,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
success = withContext(Dispatchers.IO) { performOpen(item) }
|
||||
success = performOpen(item)
|
||||
if (success) {
|
||||
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
|
||||
sessionProcessed.incrementAndGet(); totalProcessed.incrementAndGet(); recordSpeedTimestamp()
|
||||
@@ -200,23 +217,24 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
|
||||
logError("Engine failed to open Snap: ${item.messageId}")
|
||||
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
|
||||
currentStatusText = "Failed: ${item.senderName}"; updateStatusNotification()
|
||||
openedSnapsIds.remove(item.messageId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performOpen(item: SnapQueueItem): Boolean {
|
||||
val manager = messaging.conversationManager ?: return false
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
runCatching {
|
||||
manager.updateMessage(item.conversationId, item.messageId, MessageUpdate.READ) { result ->
|
||||
if (result == null || result == "DUPLICATEREQUEST") { cont.resume(true) }
|
||||
else if (item.serverMessageId != 0L) {
|
||||
manager.updateMessage(item.conversationId, item.serverMessageId, MessageUpdate.READ) { serverResult ->
|
||||
cont.resume(serverResult == null || serverResult == "DUPLICATEREQUEST")
|
||||
}
|
||||
} else { cont.resume(false) }
|
||||
}
|
||||
}.onFailure { logError("Bridge Error", it); cont.resume(false) }
|
||||
return withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
runCatching {
|
||||
manager.updateMessage(item.conversationId, item.messageId, MessageUpdate.READ) { result ->
|
||||
if (result == null || result == "DUPLICATEREQUEST") { cont.resume(true) }
|
||||
else if (item.serverMessageId != 0L) {
|
||||
manager.updateMessage(item.conversationId, item.serverMessageId, MessageUpdate.READ) { serverResult ->
|
||||
cont.resume(serverResult == null || serverResult == "DUPLICATEREQUEST")
|
||||
}
|
||||
} else { cont.resume(false) }
|
||||
}
|
||||
}.onFailure { logError("Bridge Error", it); cont.resume(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,13 +268,17 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
|
||||
if (autoOpenConfig.globalState == false || !engineActive.get()) return@subscribe
|
||||
val message = event.message
|
||||
if (message.messageState != MessageState.COMMITTED || message.senderId?.toString() == this@AutoOpenSnaps.context.database.myUserId) return@subscribe
|
||||
val conversationId = message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
|
||||
|
||||
val clientMessageId = message.messageDescriptor?.messageId ?: return@subscribe
|
||||
|
||||
val conversationId = message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
|
||||
val serverMessageId = message.orderKey ?: 0L
|
||||
|
||||
val contentType = message.messageContent?.contentType
|
||||
if (contentType != ContentType.SNAP && contentType != ContentType.EXTERNAL_MEDIA) return@subscribe
|
||||
if (!canUseRule(conversationId)) return@subscribe
|
||||
|
||||
// Prevent re-queueing the same message while it is currently being processed
|
||||
if (openedSnapsIds.contains(clientMessageId)) return@subscribe
|
||||
openedSnapsIds.add(clientMessageId)
|
||||
|
||||
@@ -295,7 +317,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
|
||||
private fun isWifiConnected(): Boolean {
|
||||
val cm = this@AutoOpenSnaps.context.androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
// RESILIENT WIFI CHECK: Iterates through all networks to find ANY WiFi transport (VPN aware)
|
||||
// Check for any available network with a WiFi or Ethernet transport
|
||||
return cm.allNetworks.any { network ->
|
||||
cm.getNetworkCapabilities(network)?.let { caps ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.messaging
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
@@ -64,11 +70,25 @@ 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)
|
||||
|
||||
private fun queueOriginalItemRepeats(repeatCount: Int, overrideType: String, snapDurationMs: Int?) {
|
||||
queuedOriginalItemRepeatCount = repeatCount
|
||||
queuedOriginalItemRepeatOverrideType = overrideType
|
||||
@@ -116,6 +136,80 @@ class SendOverride : Feature("Send Override") {
|
||||
private val backgroundHookLock = Any()
|
||||
private var backgroundHookRefs = 0
|
||||
private var backgroundHooks: List<Hooker.HookHandle>? = null
|
||||
private val engineActive = java.util.concurrent.atomic.AtomicBoolean(true)
|
||||
|
||||
private fun createContinuousNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java)
|
||||
val channel = NotificationChannel(
|
||||
CONTINUOUS_SEND_CHANNEL_ID,
|
||||
"Continuous Send",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
channel.description = "Progress status for continuous snap sending"
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
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(android.R.drawable.ic_popup_sync) // The Industrial Loop icon
|
||||
.setColor(0xFF3498DB.toInt()) // Industrial Purple/Blue tint
|
||||
.setContentTitle("Sending Snaps to $currentRecipientName")
|
||||
.setContentText("Progress: $processed / $total ($progressPercent%)")
|
||||
.setSubText("$processed / $total")
|
||||
.setProgress(total, processed, 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())
|
||||
|
||||
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.checkbox_on_background) // Emerald Green Checkmark
|
||||
.setColor(0xFF2ECC71.toInt()) // Emerald Green tint
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setAutoCancel(true)
|
||||
|
||||
notificationManager.notify(COMPLETION_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
private fun createPendingIntent(action: String): PendingIntent {
|
||||
val intent = Intent(action).setPackage(context.androidContext.packageName)
|
||||
return PendingIntent.getBroadcast(
|
||||
context.androidContext,
|
||||
action.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun acquireScheduledSendBackground(): () -> Unit {
|
||||
if (!context.config.messaging.scheduledSendAllowRunningInBackground.get()) return {}
|
||||
var enableFailed = false
|
||||
@@ -196,7 +290,35 @@ class SendOverride : Feature("Send Override") {
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
override fun init() {
|
||||
createNotificationChannel()
|
||||
|
||||
createContinuousNotificationChannel()
|
||||
|
||||
val actionReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context?, intent: 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 = IntentFilter().apply {
|
||||
addAction(ACTION_PAUSE_RESUME)
|
||||
addAction(ACTION_STOP)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
context.androidContext.registerReceiver(actionReceiver, filter)
|
||||
}
|
||||
|
||||
val stripMediaMetadata = context.config.messaging.stripMediaMetadata.get()
|
||||
var postSavePolicy: Int? = null
|
||||
|
||||
@@ -1429,6 +1551,11 @@ class SendOverride : Feature("Send Override") {
|
||||
invokeOriginalAndRestoreResult(event)
|
||||
}
|
||||
} else if (MediaFilePicker.hasReusableOriginalItem()) {
|
||||
totalRepeatCount = repeatCount
|
||||
processedRepeatCount = 1
|
||||
currentRecipientName = recipientNameForTask
|
||||
updateContinuousSendNotification()
|
||||
|
||||
queueOriginalItemRepeats(repeatCount - 1, finalSelectedType, selectedSnapDurationMs)
|
||||
attachQueuedRepeatCallbacks(event)
|
||||
if (sendMedia(finalSelectedType, selectedSnapDurationMs)) {
|
||||
@@ -1437,6 +1564,11 @@ class SendOverride : Feature("Send Override") {
|
||||
clearQueuedOriginalItemRepeats()
|
||||
}
|
||||
} else {
|
||||
totalRepeatCount = repeatCount
|
||||
processedRepeatCount = 0
|
||||
currentRecipientName = recipientNameForTask
|
||||
updateContinuousSendNotification()
|
||||
|
||||
sendRepeatedMediaManual(
|
||||
repeatCount,
|
||||
finalSelectedType,
|
||||
|
||||
@@ -2,29 +2,35 @@ package me.eternal.purrfectsnap.core.features.impl.tweaks
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
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.transition.Transition
|
||||
import android.util.Range
|
||||
import android.view.View
|
||||
import android.view.TextureView
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.WindowManager
|
||||
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.lang.Thread
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent
|
||||
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 okhttp3.Dispatcher
|
||||
import java.lang.Thread
|
||||
import java.lang.reflect.Method
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class PerformanceMode : Feature("Performance Mode") {
|
||||
override fun init() {
|
||||
@@ -43,7 +49,7 @@ 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) 120 else 180
|
||||
val maxScrollDurationMs = if (isMaxProfile) 72 else 180
|
||||
val preferredRefreshRate = if (isMaxProfile) 120f else 90f
|
||||
|
||||
context.log.info(
|
||||
@@ -53,52 +59,33 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
|
||||
runCatching {
|
||||
ValueAnimator.setFrameDelay(0L)
|
||||
context.log.info("Applied ValueAnimator frame delay override: 0ms", "PerformanceMode")
|
||||
}
|
||||
|
||||
fun firstHitLogger(name: String): (String) -> Unit {
|
||||
val didLog = AtomicBoolean(false)
|
||||
return { details ->
|
||||
if (didLog.compareAndSet(false, true)) {
|
||||
context.log.info("First hit: $name | $details", "PerformanceMode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val handlerThreadConstructorLog = firstHitLogger("HandlerThread.constructor")
|
||||
val handlerThreadStartLog = firstHitLogger("HandlerThread.start")
|
||||
val threadStartLog = firstHitLogger("Thread.start")
|
||||
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")
|
||||
|
||||
fun isPerformanceSensitiveThread(name: String?): Boolean {
|
||||
val normalizedName = name?.lowercase() ?: return false
|
||||
return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk").any {
|
||||
return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any {
|
||||
normalizedName.contains(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun clampPositiveDuration(durationMs: Long, maxDurationMs: Long): Long {
|
||||
if (durationMs <= 0L) return durationMs
|
||||
return durationMs.coerceAtMost(maxDurationMs)
|
||||
}
|
||||
|
||||
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
|
||||
if (!isMaxProfile) return@subscribe
|
||||
val url = event.url
|
||||
if (url.contains("mapbox") && (url.contains("events.") || url.contains("telemetry"))) {
|
||||
event.canceled = true
|
||||
}
|
||||
}
|
||||
|
||||
HandlerThread::class.java.hookConstructor(HookStage.BEFORE) { param ->
|
||||
if (param.args().size < 2) return@hookConstructor
|
||||
val threadName = param.argNullable<String>(0)
|
||||
if (!isPerformanceSensitiveThread(threadName)) return@hookConstructor
|
||||
param.setArg(1, threadPriority)
|
||||
handlerThreadConstructorLog("name=$threadName priority=$threadPriority")
|
||||
}
|
||||
|
||||
HandlerThread::class.java.hook("start", HookStage.AFTER) { param ->
|
||||
@@ -110,16 +97,14 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
Process.setThreadPriority(tid, threadPriority)
|
||||
}
|
||||
}
|
||||
handlerThreadStartLog("name=${thread.name} tid=${thread.threadId} priority=$threadPriority")
|
||||
}
|
||||
|
||||
Thread::class.java.hook("start", HookStage.AFTER) { param ->
|
||||
val thread = param.thisObject<Thread>()
|
||||
if (!isPerformanceSensitiveThread(thread.name)) return@hook
|
||||
runCatching {
|
||||
thread.priority = Thread.MAX_PRIORITY
|
||||
thread.priority = if (isMaxProfile) Thread.MAX_PRIORITY else Thread.NORM_PRIORITY + 1
|
||||
}
|
||||
threadStartLog("name=${thread.name} priority=${thread.priority}")
|
||||
}
|
||||
|
||||
ThreadPoolExecutor::class.java.hookConstructor(HookStage.AFTER) { param ->
|
||||
@@ -131,7 +116,6 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
}
|
||||
executor.allowCoreThreadTimeOut(false)
|
||||
executor.prestartAllCoreThreads()
|
||||
executorLog("core=${executor.corePoolSize} max=${executor.maximumPoolSize} active=${executor.activeCount}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,13 +124,11 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
runCatching {
|
||||
dispatcher.maxRequests = maxRequests
|
||||
dispatcher.maxRequestsPerHost = maxRequestsPerHost
|
||||
dispatcherLog("maxRequests=${dispatcher.maxRequests} maxRequestsPerHost=${dispatcher.maxRequestsPerHost}")
|
||||
}
|
||||
}
|
||||
|
||||
ValueAnimator::class.java.hook("getDurationScale", HookStage.AFTER) { param ->
|
||||
param.setResult(durationScale)
|
||||
animatorLog("durationScale=$durationScale")
|
||||
}
|
||||
|
||||
ValueAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
@@ -155,7 +137,6 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
animatorDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
|
||||
ViewPropertyAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
@@ -164,7 +145,6 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
viewAnimatorDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
|
||||
Transition::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
@@ -173,7 +153,6 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
transitionDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
|
||||
Animation::class.java.hook("setDuration", HookStage.BEFORE) { param ->
|
||||
@@ -182,28 +161,25 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
if (updated != original) {
|
||||
param.setArg(0, updated)
|
||||
}
|
||||
animationDurationLog("requested=$original applied=${param.arg<Long>(0)}")
|
||||
}
|
||||
|
||||
RecyclerView::class.java.hookConstructor(HookStage.AFTER) { param ->
|
||||
val recyclerView = param.thisObject<RecyclerView>()
|
||||
recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
|
||||
recyclerView.overScrollMode = View.OVER_SCROLL_NEVER
|
||||
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
recyclerView.recycledViewPool.setMaxRecycledViews(0, 20)
|
||||
if (isMaxProfile) {
|
||||
recyclerView.itemAnimator = null
|
||||
}
|
||||
recyclerCtorLog("cache=$recyclerViewCacheSize max=$isMaxProfile class=${recyclerView::class.java.name}")
|
||||
}
|
||||
|
||||
RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param ->
|
||||
val recyclerView = param.thisObject<RecyclerView>()
|
||||
recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
|
||||
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
recyclerView.recycledViewPool.setMaxRecycledViews(0, 20)
|
||||
if (isMaxProfile) {
|
||||
recyclerView.itemAnimator = null
|
||||
}
|
||||
recyclerAdapterLog("cache=$recyclerViewCacheSize adapter=${param.argNullable<Any>(0)?.javaClass?.name}")
|
||||
}
|
||||
|
||||
RecyclerView::class.java.hook("setLayoutManager", HookStage.AFTER) { param ->
|
||||
@@ -212,15 +188,13 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
when (layoutManager) {
|
||||
is LinearLayoutManager -> {
|
||||
layoutManager.isItemPrefetchEnabled = true
|
||||
layoutManager.initialPrefetchItemCount = prefetchItemCount
|
||||
layoutManager.initialPrefetchItemCount = prefetchItemCount.coerceAtLeast(12)
|
||||
}
|
||||
is StaggeredGridLayoutManager -> {
|
||||
layoutManager.isItemPrefetchEnabled = true
|
||||
layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
}
|
||||
}
|
||||
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount")
|
||||
}
|
||||
|
||||
fun SQLiteDatabase.applyPerformancePragmas() {
|
||||
@@ -233,25 +207,21 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
}
|
||||
|
||||
SQLiteDatabase::class.java.hook("openDatabase", HookStage.AFTER) { param ->
|
||||
(param.getResult() as? SQLiteDatabase)?.also {
|
||||
it.applyPerformancePragmas()
|
||||
sqliteOpenLog("path=${param.argNullable<Any>(0)}")
|
||||
}
|
||||
(param.getResult() as? SQLiteDatabase)?.applyPerformancePragmas()
|
||||
}
|
||||
|
||||
SQLiteDatabase::class.java.hook("openOrCreateDatabase", HookStage.AFTER) { param ->
|
||||
(param.getResult() as? SQLiteDatabase)?.also {
|
||||
it.applyPerformancePragmas()
|
||||
sqliteCreateLog("path=${param.argNullable<Any>(0)}")
|
||||
}
|
||||
(param.getResult() as? SQLiteDatabase)?.applyPerformancePragmas()
|
||||
}
|
||||
|
||||
MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param ->
|
||||
val currentRate = param.arg<Int>(0)
|
||||
if (currentRate < minimumFrameRate) {
|
||||
param.setArg(0, minimumFrameRate)
|
||||
val applied = currentRate
|
||||
.coerceAtLeast(if (isMaxProfile) 30 else 24)
|
||||
.coerceAtMost(if (isMaxProfile) 60 else 45)
|
||||
if (applied != currentRate) {
|
||||
param.setArg(0, applied)
|
||||
}
|
||||
mediaRecorderLog("requested=$currentRate applied=${param.arg<Int>(0)}")
|
||||
}
|
||||
|
||||
OverScroller::class.java.hook("startScroll", HookStage.BEFORE) { param ->
|
||||
@@ -261,29 +231,84 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
if (updated != original) {
|
||||
param.setArg(4, updated)
|
||||
}
|
||||
overScrollerLog("requested=$original applied=${param.arg<Int>(4)}")
|
||||
}
|
||||
}
|
||||
|
||||
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
|
||||
val key = param.arg<CaptureRequest.Key<*>>(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<Any>(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))
|
||||
}
|
||||
OverScroller::class.java.hook("fling", HookStage.BEFORE) { param ->
|
||||
if (param.args().size >= 10) {
|
||||
val overX = param.arg<Int>(8)
|
||||
val overY = param.arg<Int>(9)
|
||||
if (overX != 0) param.setArg(8, 0)
|
||||
if (overY != 0) param.setArg(9, 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
|
||||
}
|
||||
}
|
||||
captureRequestLog("key=${key.name} value=${param.argNullable<Any>(1)}")
|
||||
|
||||
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<Any>()
|
||||
runCatching {
|
||||
nativeSetPrefetchTiles?.invoke(nativeMapView, true)
|
||||
nativeSetPrefetchZoomDelta?.invoke(nativeMapView, 6)
|
||||
nativeSetTransitionDelay?.invoke(nativeMapView, 0L)
|
||||
nativeSetTransitionDuration?.invoke(nativeMapView, 0L)
|
||||
nativeSetTransitionOptions?.invoke(
|
||||
nativeMapView,
|
||||
transitionOptionsCtor.newInstance(0L, 0L, false)
|
||||
)
|
||||
nativeCancelTransitions?.invoke(nativeMapView)
|
||||
}
|
||||
}
|
||||
|
||||
nativeMapViewClass.findRestrictedMethod { method ->
|
||||
method.name == "g" &&
|
||||
method.parameterCount == 6 &&
|
||||
method.parameterTypes.last() == Long::class.javaPrimitiveType
|
||||
}?.hook(HookStage.BEFORE) { param ->
|
||||
val original = param.arg<Long>(5)
|
||||
val applied = original.coerceAtMost(16L)
|
||||
if (applied != original) {
|
||||
param.setArg(5, applied)
|
||||
}
|
||||
runCatching { nativeCancelTransitions?.invoke(param.thisObject<Any>()) }
|
||||
}
|
||||
|
||||
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<Long>(2)
|
||||
val applied = original.coerceAtMost(8L)
|
||||
if (applied != original) {
|
||||
param.setArg(2, applied)
|
||||
}
|
||||
runCatching { nativeCancelTransitions?.invoke(param.thisObject<Any>()) }
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to install Snap Map transition hooks", it, "PerformanceMode")
|
||||
}
|
||||
|
||||
fun applyActivityPerformanceTuning(activity: Activity) {
|
||||
@@ -295,12 +320,10 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,5 +331,24 @@ class PerformanceMode : Feature("Performance Mode") {
|
||||
onNextActivityCreate {
|
||||
applyActivityPerformanceTuning(it)
|
||||
}
|
||||
|
||||
Dialog::class.java.hook("show", HookStage.AFTER) { param ->
|
||||
val dialog = param.nullableThisObject<Any>() as? Dialog ?: return@hook
|
||||
val window = dialog.window ?: return@hook
|
||||
runCatching {
|
||||
window.setWindowAnimations(0)
|
||||
window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
window.attributes = window.attributes.apply {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextureView::class.java.hookConstructor(HookStage.AFTER) { param ->
|
||||
val textureView = param.thisObject<TextureView>()
|
||||
runCatching {
|
||||
textureView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user