You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/perf/StrictModeManager.kt

182 lines
8.4 KiB
Kotlin

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This class implements the alternative ways to suppress StrictMode with performance
// monitoring by wrapping the raw methods. This lint check tells us not to use the raw
// methods so we suppress the check.
@file:Suppress("MozillaStrictModeSuppression")
package org.mozilla.fenix.perf
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.StrictMode
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import mozilla.components.support.ktx.android.os.resetAfter
import org.mozilla.fenix.Config
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.utils.ManufacturerCodes
import org.mozilla.fenix.utils.Mockable
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicLong
private const val DELAY_TO_REMOVE_STRICT_MODE_MILLIS = 1000L
private val logger = Performance.logger
private val mainLooper = Looper.getMainLooper()
/**
* Manages strict mode settings for the application.
*/
@Mockable
class StrictModeManager(
config: Config,
// Ideally, we'd pass in a more specific value but there is a circular dependency: StrictMode
// is passed into Core but we'd need to pass in Core here. Instead, we take components and later
// fetch the value we need from it.
private val components: Components
) {
private val isEnabledByBuildConfig = config.channel.isDebug
/**
* The number of times StrictMode has been suppressed. StrictMode can be used to prevent main
* thread IO but it's easy to suppress. We use this value, in combination with:
* - a test: that fails if the suppression count increases
* - a lint check: to ensure this value gets used instead of functions that work around it
* - code owners: to prevent modifications to these above items without perf knowing
* to make suppressions a more deliberate act.
*
* This is an Atomic* so it can be safely incremented from any thread.
*/
@VisibleForTesting(otherwise = PRIVATE)
val suppressionCount = AtomicLong(0)
/***
* Enables strict mode for debug purposes. meant to be run only in the main process.
* @param setPenaltyDeath boolean value to decide setting the penaltyDeath as a penalty.
*/
fun enableStrictMode(setPenaltyDeath: Boolean) {
if (isEnabledByBuildConfig) {
val threadPolicy = StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
if (setPenaltyDeath) {
threadPolicy.penaltyDeathWithIgnores()
}
StrictMode.setThreadPolicy(threadPolicy.build())
val builder = StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectActivityLeaks()
.detectFileUriExposure()
.penaltyLog()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.detectContentUriWithoutPermission()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.detectNonSdkApiUsage()
}
StrictMode.setVmPolicy(builder.build())
}
}
/**
* Revert strict mode to disable penalty based on fragment lifecycle since strict mode
* needs to switch to penalty logs. Using the fragment life cycle allows decoupling from any
* specific fragment.
*/
fun attachListenerToDisablePenaltyDeath(fragmentManager: FragmentManager) {
fragmentManager.registerFragmentLifecycleCallbacks(object :
FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
fm.unregisterFragmentLifecycleCallbacks(this)
// If we don't post when using penaltyListener on P+, the violation listener is never
// called. My best guess is that, unlike penaltyDeath, the violations are not
// delivered instantaneously so posting gives time for the violation listeners to
// run before they are removed here. This may be a race so we give the listeners a
// little extra time to run too though this way we may accidentally trigger
// violations for non-startup, which we haven't planned to do yet.
Handler(mainLooper).postDelayed({
enableStrictMode(setPenaltyDeath = false)
}, DELAY_TO_REMOVE_STRICT_MODE_MILLIS)
} }, false)
}
/**
* Runs the given [functionBlock] and sets the given [StrictMode.ThreadPolicy] after its
* completion when in a build configuration that has StrictMode enabled. If StrictMode is
* not enabled, simply runs the [functionBlock]. This function is written in the style of
* [AutoCloseable.use].
*
* This function contains perf improvements so it should be
* called instead of [mozilla.components.support.ktx.android.os.resetAfter] (using the wrong
* method should be prevented by a lint check). This is significantly less convenient to run than
* when it was written as an extension function on [StrictMode.ThreadPolicy] but I think this is
* okay: it shouldn't be easy to ignore StrictMode.
*
* @return the value returned by [functionBlock].
*/
fun <R> resetAfter(policy: StrictMode.ThreadPolicy, functionBlock: () -> R): R {
// Calling resetAfter takes 1-2ms (unknown device) so we only execute it if StrictMode can
// actually be enabled. https://github.com/mozilla-mobile/fenix/issues/11617
return if (isEnabledByBuildConfig) {
// This can overflow and crash. However, it's unlikely we'll suppress StrictMode 9
// quintillion times in a build config where StrictMode is enabled so we don't handle it
// because it'd increase complexity.
val suppressionCount = suppressionCount.incrementAndGet()
// We log so that devs are more likely to notice that we're suppressing StrictMode violations.
// We add profiler markers so that the perf team can easily identify IO locations in profiles.
logger.warn("StrictMode violation suppressed: #$suppressionCount")
if (Thread.currentThread() == mainLooper.thread) { // markers only supported on main thread.
components.core.engine.profiler?.addMarker("StrictMode.suppression", "Count: $suppressionCount")
}
policy.resetAfter(functionBlock)
} else {
functionBlock()
}
}
}
/**
* There are certain manufacturers that have custom font classes for the OS systems.
* These classes violates the [StrictMode] policies on startup. As a workaround, we create
* an exception list for these manufacturers so that dialogs do not show up on start up.
* To add a new manufacturer to the list, log "Build.MANUFACTURER" from the device to get the
* exact name of the manufacturer.
*/
private val strictModeExceptionList = setOf(ManufacturerCodes.HUAWEI, ManufacturerCodes.ONE_PLUS)
private fun StrictMode.ThreadPolicy.Builder.penaltyDeathWithIgnores(): StrictMode.ThreadPolicy.Builder {
// This workaround was added before we realized we can ignored based on violation contents
// (see code below). This solution - blanket disabling StrictMode on some manufacturers - isn't
// great so, if we have time, we should consider reimplementing these fixes using the methods below.
if (Build.MANUFACTURER in strictModeExceptionList) {
return this
}
// If we want to apply ignores based on stack trace contents to APIs below P, we can use this methodology:
// https://medium.com/@tokudu/how-to-whitelist-strictmode-violations-on-android-based-on-stacktrace-eb0018e909aa
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
penaltyDeath()
} else {
// Ideally, we'd use a shared thread pool but we don't have any on the system currently
// (all shared ones are coroutine dispatchers).
val executor = Executors.newSingleThreadExecutor()
penaltyListener(executor, ThreadPenaltyDeathWithIgnoresListener())
}
return this
}