15278 detekt rule runblocking (#15942)

* For #15278: added CoroutineManager to count runBlocking calls

* For #15278: Added actual detekt rule for runblocking and its config to the yaml

* For #15278: Added unit test for RunblockingCounter

* For #15278: renamed StrictModeStartupSuppressionCountTest.kt to PerformanceStartupTest.kt and added runBlockingCount test

* Lint fix

* For #15278: made runblocking a Long to prevent overflow

* For #15278: fixed MozRunblocking name, description and moved RunBlockingCounter to perf package

* For #15278:Renamed MozillaRunblockingCheck to MozillaRunBlockingCheck

* For #15278: Added setup for unit test, since it failed without restting counter

* For #15278: Fixed naming for RunBlocking lint check

* For #15278: removed changes made to test to use runBlockingIncrement

* For #15728: added test exclusion for runBlocking check

* For #15278: changed null check and added Synchronized to count setter

* For #15278: fix for nits

* For #15278: added StartupExcessiveResourceUseTest to CODEOWNERS

* For #15278: fixed for nits

* For #15278: Moved increment function to extension function and fixed indentation

* For #15278: Added tests for Atomic Integer extension and nit fix
upstream-sync
MarcLeclair 4 years ago committed by GitHub
parent d46fc7b142
commit 7b1af41b40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -50,6 +50,8 @@
*Application.kt @mozilla-mobile/Performance
*StrictMode*kt @mozilla-mobile/Performance
*ConstraintLayoutPerfDetector* @mozilla-mobile/Performance
*MozillaRunBlockingCheck.kt @mozilla-mobile/Performance
*StartupExcessiveResourceUseTest.kt @mozilla-mobile/Performance
# We want to be aware of new features behind flags as well as features
# about to be enabled.

@ -9,13 +9,15 @@ import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.perf.RunBlockingCounter
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.HomeActivityTestRule
// PLEASE CONSULT WITH PERF TEAM BEFORE CHANGING THIS VALUE.
private const val EXPECTED_SUPPRESSION_COUNT = 11
private const val FAILURE_MSG = """StrictMode startup suppression count does not match expected count.
private const val EXPECTED_RUNBLOCKING_COUNT = 2
private const val STRICTMODE_FAILURE_MSG = """StrictMode startup suppression count does not match expected count.
If this PR removed code that suppressed StrictMode, great! Please decrement the suppression count.
@ -28,31 +30,53 @@ private const val FAILURE_MSG = """StrictMode startup suppression count does not
"""
private const val RUNBLOCKING_FAILURE_MSG = """RunblockingCounter startup count does not match expected count.
If this PR removed code that removed a RunBlocking call, great! Please decrement the suppression count.
Did this PR add or call code that incremented the RunBlockingCounter?
Did you know that using runBlocking can have negative perfomance implications?
If so, please do your best to implement a solution without blocking the main thread.
Please consult the perf team if you have questions or believe that using runBlocking
is the optimal solution.
"""
/**
* A performance test to limit the number of StrictMode suppressions on startup.
* A performance test to limit the number of StrictMode suppressions and number of runBlocking used
* on startup.
*
* This test was written by the perf team.
*
* StrictMode detects main thread IO, which is often indicative of a performance issue.
* It's easy to suppress StrictMode so we wrote a test to ensure we have a discussion
* if the StrictMode count changes. The perf team is code owners for this file so they
* should be notified when the count is modified.
* if the StrictMode count changes.
*
* RunBlocking is mostly used to return values to a thread from a coroutine. However, if that
* coroutine takes too long, it can lead that thread to block every other operations.
*
* The perf team is code owners for this file so they should be notified when the count is modified.
*
* IF YOU UPDATE THE TEST NAME, UPDATE CODE OWNERS.
*/
class StrictModeStartupSuppressionCountTest {
class StartupExcessiveResourceUseTest {
@get:Rule
val activityTestRule = HomeActivityTestRule(skipOnboarding = true)
private val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@Test
fun verifyStrictModeSuppressionCount() {
fun verifyRunBlockingAndStrictModeSuppresionCount() {
uiDevice.waitForIdle() // wait for async UI to load.
// This might cause intermittents: at an arbitrary point after start up (such as the visual
// completeness queue), we might run code on the main thread that suppresses StrictMode,
// causing this number to fluctuate depending on device speed. We'll deal with it if it occurs.
val actual = activityTestRule.activity.components.strictMode.suppressionCount.toInt()
assertEquals(FAILURE_MSG, EXPECTED_SUPPRESSION_COUNT, actual)
val actualSuppresionCount = activityTestRule.activity.components.strictMode.suppressionCount.toInt()
val actualRunBlocking = RunBlockingCounter.count.get()
assertEquals(STRICTMODE_FAILURE_MSG, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertEquals(RUNBLOCKING_FAILURE_MSG, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking)
}
}

@ -19,7 +19,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction
@ -44,6 +43,7 @@ import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
@ -137,7 +137,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary.
if (!megazordSetup.isCompleted) {
runBlocking { megazordSetup.await(); }
runBlockingIncrement { megazordSetup.await() }
}
}

@ -4,10 +4,10 @@
package org.mozilla.fenix.components.history
import kotlinx.coroutines.runBlocking
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.concept.storage.VisitInfo
import mozilla.components.concept.storage.VisitType
import org.mozilla.fenix.perf.runBlockingIncrement
/**
* An Interface for providing a paginated list of [VisitInfo]
@ -32,7 +32,7 @@ fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider
numberOfItems: Long,
onComplete: (List<VisitInfo>) -> Unit
) {
runBlocking {
runBlockingIncrement {
val history = getVisitsPaginated(
offset,
numberOfItems,

@ -12,7 +12,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.runBlocking
import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.components.metrics.Event.AppAllStartup
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source
@ -25,6 +24,7 @@ import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.ERROR
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.HOT
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.COLD
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.WARM
import org.mozilla.fenix.perf.runBlockingIncrement
import java.lang.reflect.Modifier.PRIVATE
/**
@ -186,7 +186,7 @@ class AppStartupTelemetry(
* the application potentially closes.
*/
fun onStop() {
runBlocking {
runBlockingIncrement {
recordMetric()
}
}

@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.provider.AssetsSearchEngineProvider
import mozilla.components.browser.search.provider.SearchEngineList
@ -27,6 +26,7 @@ import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.runBlockingIncrement
import java.util.Locale
@SuppressWarnings("TooManyFunctions")
@ -141,7 +141,7 @@ open class FenixSearchEngineProvider(
* are readily available throughout the app. Includes all installed engines, both
* default and custom
*/
fun installedSearchEngines(context: Context): SearchEngineList = runBlocking {
fun installedSearchEngines(context: Context): SearchEngineList = runBlockingIncrement {
val installedIdentifiers = installedSearchEngineIdentifiers(context)
val defaultList = searchEngines.await()
@ -161,15 +161,15 @@ open class FenixSearchEngineProvider(
)
}
fun allSearchEngineIdentifiers() = runBlocking {
fun allSearchEngineIdentifiers() = runBlockingIncrement {
loadedSearchEngines.await().list.map { it.identifier }
}
fun uninstalledSearchEngines(context: Context): SearchEngineList = runBlocking {
fun uninstalledSearchEngines(context: Context): SearchEngineList = runBlockingIncrement {
val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = loadedSearchEngines.await()
return@runBlocking engineList.copy(
return@runBlockingIncrement engineList.copy(
list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) }
)
}
@ -182,7 +182,7 @@ open class FenixSearchEngineProvider(
context: Context,
searchEngine: SearchEngine,
isCustom: Boolean = false
) = runBlocking {
) = runBlockingIncrement {
if (isCustom) {
val searchUrl = searchEngine.getSearchTemplate()
CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl)
@ -201,7 +201,7 @@ open class FenixSearchEngineProvider(
context: Context,
searchEngine: SearchEngine,
isCustom: Boolean = false
) = runBlocking {
) = runBlockingIncrement {
if (isCustom) {
CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier)
reload()

@ -9,7 +9,6 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.CustomTabConfig
@ -30,6 +29,7 @@ import mozilla.components.support.utils.toSafeIntent
import org.json.JSONException
import org.json.JSONObject
import org.mozilla.fenix.R
import org.mozilla.fenix.perf.runBlockingIncrement
import java.io.File
import java.io.IOException
@ -61,7 +61,7 @@ class FennecWebAppIntentProcessor(
val url = safeIntent.dataString
return if (!url.isNullOrEmpty() && matches(intent)) {
val webAppManifest = runBlocking { loadManifest(safeIntent, url) }
val webAppManifest = runBlockingIncrement { loadManifest(safeIntent, url) }
val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN)
session.webAppManifest = webAppManifest

@ -0,0 +1,19 @@
/* 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/. */
package org.mozilla.fenix.ext
import java.util.concurrent.atomic.AtomicInteger
/**
* Increases an AtomicInteger safely.
*/
fun AtomicInteger.getAndIncrementNoOverflow() {
var prev: Int
var next: Int
do {
prev = this.get()
next = if (prev == Integer.MAX_VALUE) prev else prev + 1
} while (!this.compareAndSet(prev, next))
}

@ -11,13 +11,13 @@ import android.util.Patterns
import android.webkit.URLUtil
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.lib.publicsuffixlist.ext.urlToTrimmedHost
import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
import org.mozilla.fenix.perf.runBlockingIncrement
import java.io.IOException
import java.net.IDN
import java.util.Locale
@ -92,9 +92,10 @@ private fun Uri.isIpv6(): Boolean {
/**
* Trim a host's prefix and suffix
*/
fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String = runBlocking {
urlToTrimmedHost(publicSuffixList).await()
}
fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String =
runBlockingIncrement {
urlToTrimmedHost(publicSuffixList).await()
}
/**
* Trims a URL string of its scheme and common prefixes.

@ -0,0 +1,45 @@
/* 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 invoke runBlocking with some
// monitoring by wrapping the raw methods. This lint check tells us not to use the raw
// methods so we suppress the check.
@file:Suppress("MozillaRunBlockingCheck")
package org.mozilla.fenix.perf
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import org.mozilla.fenix.ext.getAndIncrementNoOverflow
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext
/**
* Counts the number of runBlocking calls made
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
object RunBlockingCounter {
val count = AtomicInteger(0)
}
/**
* Wrapper around `runBlocking`. RunBlocking seems to be a "fix-all" to return values to the thread
* where the coroutine is called. The official doc explains runBlocking: "Runs a new coroutine and
* blocks the current thread interruptibly until its completion`. This can block our main thread
* which could lead to significant jank. This wrapper aims to count the number of runBlocking call
* to try to limit them as much as possible to encourage alternatives solutions whenever this function
* might be needed.
*/
fun <T> runBlockingIncrement(
context: CoroutineContext? = null,
action: suspend CoroutineScope.() -> T
): T {
RunBlockingCounter.count.getAndIncrementNoOverflow()
return if (context != null) {
runBlocking(context) { action() }
} else {
runBlocking { action() }
}
}

@ -25,7 +25,6 @@ import kotlinx.android.synthetic.main.search_engine_radio_button.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import mozilla.components.browser.search.SearchEngine
import org.mozilla.fenix.BrowserDirection
@ -37,6 +36,7 @@ import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.settings.SupportUtils
import java.util.Locale
@ -51,7 +51,7 @@ class AddSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine),
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
availableEngines = runBlocking {
availableEngines = runBlockingIncrement {
requireContext()
.components
.search

@ -5,10 +5,10 @@
package org.mozilla.fenix
import android.content.Context
import kotlinx.coroutines.runBlocking
import mozilla.components.support.migration.FennecMigrator
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.migration.MigrationTelemetryListener
import org.mozilla.fenix.perf.runBlockingIncrement
/**
* An application class which knows how to migrate Fennec data.
@ -81,7 +81,7 @@ class MigratingFenixApplication : FenixApplication() {
.migrateSettings()
.build()
runBlocking {
runBlockingIncrement {
migrator.migrateAsync(components.migrationStore).await()
}
}

@ -0,0 +1,40 @@
/* 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/. */
package org.mozilla.fenix.ext
import org.junit.Assert.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
/* 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/. */
class AtomicIntegerTest {
@Test
fun `Safely increment an AtomicInteger from different coroutines`() {
val integer = AtomicInteger(0)
runBlocking {
for (i in 1..2) {
launch(Dispatchers.Default) {
integer.getAndIncrementNoOverflow()
}
}
}
assertEquals(integer.get(), 2)
}
@Test
fun `Incrementing the AtomicInteger should not overflow`() {
val integer = AtomicInteger(Integer.MAX_VALUE)
integer.getAndIncrementNoOverflow()
assertEquals(integer.get(), Integer.MAX_VALUE)
}
}

@ -0,0 +1,24 @@
/* 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/. */
package org.mozilla.fenix.perf
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class RunBlockingCounterTest {
@Before
fun setup() {
RunBlockingCounter.count.set(0)
}
@Test
fun `GIVEN we call our custom runBlocking method with counter THEN the latter should increase`() {
assertEquals(0, RunBlockingCounter.count.get())
runBlockingIncrement {}
assertEquals(1, RunBlockingCounter.count.get())
}
}

@ -117,6 +117,9 @@ mozilla-detekt-rules:
excludes: "**/*Test.kt, **/*Spec.kt, **/test/**, **/androidTest/**"
MozillaCorrectUnitTestRunner:
active: true
MozillaRunBlockingCheck:
active: true
excludes: "**/*Test.kt, **/*Spec.kt, **/test/**, **/androidTest/**"
empty-blocks:
active: true

@ -16,7 +16,8 @@ class CustomRulesetProvider : RuleSetProvider {
listOf(
MozillaBannedPropertyAccess(config),
MozillaStrictModeSuppression(config),
MozillaCorrectUnitTestRunner(config)
MozillaCorrectUnitTestRunner(config),
MozillaRunBlockingCheck(config)
)
)
}

@ -0,0 +1,39 @@
/* 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/. */
package org.mozilla.fenix.detektrules
import io.gitlab.arturbosch.detekt.api.*
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
import kotlin.math.exp
private const val VIOLATION_MSG = "Please use `org.mozilla.fenix.perf.runBlockingImplement` instead" +
"because it allows us to monitor the code for performance regressions."
/**
* A check to prevent us from working around mechanisms we implemented in
* @see org.mozilla.fenix.perf.RunBlockingCounter.runBlockingIncrement to count how many runBlocking
* are used.
*
* IF YOU UPDATE THIS FILE NAME, UPDATE CODE OWNERS.
*/
class MozillaRunBlockingCheck(config: Config) : Rule(config) {
override val issue = Issue(
"MozillaRunBlockingCheck",
Severity.Performance,
"Prevents us from working around mechanisms we implemented to count how many " +
"runBlocking are used",
Debt.TWENTY_MINS
)
override fun visitImportDirective(importDirective: KtImportDirective) {
if (importDirective.importPath?.toString() == "kotlinx.coroutines.runBlocking") {
report(CodeSmell(issue, Entity.from(importDirective), VIOLATION_MSG))
}
}
}
Loading…
Cancel
Save