Skip to content
This repository has been archived by the owner on Feb 20, 2023. It is now read-only.

15278 detekt rule runblocking #15942

Merged
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
266786b
For #15278: added CoroutineManager to count runBlocking calls
MarcLeclair Oct 7, 2020
da7b1ed
For #15278: Added actual detekt rule for runblocking and its config t…
MarcLeclair Oct 15, 2020
1214bb6
For #15278: Added unit test for RunblockingCounter
MarcLeclair Oct 15, 2020
3fd69e2
For #15278: renamed StrictModeStartupSuppressionCountTest.kt to Perfo…
MarcLeclair Oct 15, 2020
d526980
Lint fix
MarcLeclair Oct 15, 2020
d1db99f
For #15278: made runblocking a Long to prevent overflow
MarcLeclair Oct 15, 2020
5f1c747
For #15278: fixed MozRunblocking name, description and moved RunBlock…
MarcLeclair Oct 20, 2020
c277469
For #15278:Renamed MozillaRunblockingCheck to MozillaRunBlockingCheck
MarcLeclair Oct 20, 2020
de6a96d
For #15278: Added setup for unit test, since it failed without restti…
MarcLeclair Oct 20, 2020
745e9af
For #15278: Fixed naming for RunBlocking lint check
MarcLeclair Oct 20, 2020
da0f4c6
For #15278: removed changes made to test to use runBlockingIncrement
MarcLeclair Oct 20, 2020
bb5636d
For #15728: added test exclusion for runBlocking check
MarcLeclair Oct 20, 2020
a121a9b
For #15278: changed null check and added Synchronized to count setter
MarcLeclair Oct 20, 2020
8d63ebc
For #15278: fix for nits
MarcLeclair Oct 27, 2020
36fc3e1
For #15278: added StartupExcessiveResourceUseTest to CODEOWNERS
MarcLeclair Oct 28, 2020
676fb6b
For #15278: fixed for nits
MarcLeclair Oct 29, 2020
5f2da80
For #15278: Moved increment function to extension function and fixed …
MarcLeclair Oct 30, 2020
b265a37
For #15278: Added tests for Atomic Integer extension and nit fix
MarcLeclair Nov 2, 2020
e0be45d
merged master
MarcLeclair Nov 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
*Application.kt @mozilla-mobile/Performance
*StrictMode*kt @mozilla-mobile/Performance
*ConstraintLayoutPerfDetector.kt @mozilla-mobile/Performance
*MozillaRunBlockingCheck.kt @mozilla-mobile/Performance

# We want to be aware of new features behind flags as well as features
# about to be enabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -28,6 +30,19 @@ 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: update the comment

* This test was written by the perf team.
Expand All @@ -39,20 +54,23 @@ private const val FAILURE_MSG = """StrictMode startup suppression count does not
*
* IF YOU UPDATE THE TEST NAME, UPDATE CODE OWNERS.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you read the comment? 😝 Can you add this to 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an AtomicInteger now – we need to call get() before doing the comparison.


assertEquals(STRICTMODE_FAILURE_MSG, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertEquals(RUNBLOCKING_FAILURE_MSG, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking)
}
}
4 changes: 2 additions & 2 deletions app/src/main/java/org/mozilla/fenix/FenixApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(); }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -32,7 +32,7 @@ fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider
numberOfItems: Long,
onComplete: (List<VisitInfo>) -> Unit
) {
runBlocking {
runBlockingIncrement {
val history = getVisitsPaginated(
offset,
numberOfItems,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

/**
Expand Down Expand Up @@ -186,7 +186,7 @@ class AppStartupTelemetry(
* the application potentially closes.
*/
fun onStop() {
runBlocking {
runBlockingIncrement {
recordMetric()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -116,40 +116,47 @@ open class FenixSearchEngineProvider(
* @return a list of all SearchEngines that are currently active. These are the engines that
* are readily available throughout the app.
*/
fun installedSearchEngines(context: Context): SearchEngineList = runBlocking {
val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = searchEngines.await()

engineList.copy(
list = engineList.list.filter {
installedIdentifiers.contains(it.identifier)
}.sortedBy { it.name.toLowerCase(Locale.getDefault()) },
default = engineList.default?.let {
if (installedIdentifiers.contains(it.identifier)) {
it
} else {
null
fun installedSearchEngines(context: Context): SearchEngineList =
runBlockingIncrement {
val installedIdentifiers = installedSearchEngineIdentifiers(context)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we avoid breaking blame? Or is it impossible because the line is too long?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Blame is still broken here: it's double-indented. I think it'd be fixed if we corrected the indentation.

val engineList = searchEngines.await()

engineList.copy(
list = engineList.list.filter {
installedIdentifiers.contains(it.identifier)
}.sortedBy { it.name.toLowerCase(Locale.getDefault()) },
default = engineList.default?.let {
if (installedIdentifiers.contains(it.identifier)) {
it
} else {
null
}
}
}
)
}
)
}

fun allSearchEngineIdentifiers() = runBlocking {
loadedSearchEngines.await().list.map { it.identifier }
}
fun allSearchEngineIdentifiers() =
runBlockingIncrement {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: avoid blame

loadedSearchEngines.await().list.map { it.identifier }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same indentation issues here and below.

}

fun uninstalledSearchEngines(context: Context): SearchEngineList = runBlocking {
val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = loadedSearchEngines.await()
fun uninstalledSearchEngines(context: Context): SearchEngineList =
runBlockingIncrement {
val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = loadedSearchEngines.await()

engineList.copy(list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) })
}
engineList.copy(list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) })
}

override suspend fun loadSearchEngines(context: Context): SearchEngineList {
return installedSearchEngines(context)
}

fun installSearchEngine(context: Context, searchEngine: SearchEngine, isCustom: Boolean = false) = runBlocking {
fun installSearchEngine(
context: Context,
searchEngine: SearchEngine,
isCustom: Boolean = false
) = runBlockingIncrement {
if (isCustom) {
val searchUrl = searchEngine.getSearchTemplate()
CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl)
Expand All @@ -162,14 +169,19 @@ open class FenixSearchEngineProvider(
}
}

fun uninstallSearchEngine(context: Context, searchEngine: SearchEngine, isCustom: Boolean = false) = runBlocking {
fun uninstallSearchEngine(
context: Context,
searchEngine: SearchEngine,
isCustom: Boolean = false
) = runBlockingIncrement {
if (isCustom) {
CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier)
reload()
} else {
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
installedIdentifiers.remove(searchEngine.identifier)
prefs(context).edit().putStringSet(localeAwareInstalledEnginesKey(), installedIdentifiers).apply()
prefs(context).edit()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you didn't change this line so don't break blame

.putStringSet(localeAwareInstalledEnginesKey(), installedIdentifiers).apply()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +27,7 @@ import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
import org.json.JSONObject
import org.mozilla.fenix.R
import org.mozilla.fenix.perf.runBlockingIncrement
import java.io.File
import java.io.IOException

Expand Down Expand Up @@ -58,7 +58,12 @@ class FennecWebAppIntentProcessor(
val url = safeIntent.dataString

return if (!url.isNullOrEmpty() && matches(intent)) {
val webAppManifest = runBlocking { loadManifest(safeIntent, url) }
val webAppManifest = runBlockingIncrement {
loadManifest(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: one line for loadManifest (if not the whole block)

safeIntent,
url
)
}

val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN)
session.webAppManifest = webAppManifest
Expand Down
9 changes: 5 additions & 4 deletions app/src/main/java/org/mozilla/fenix/ext/String.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
48 changes: 48 additions & 0 deletions app/src/main/java/org/mozilla/fenix/perf/RunBlockingCounter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* 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 java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext

/**
* Counts the number of runBlocking calls made
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
object RunBlockingCounter {
var count = AtomicInteger(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: val

}

/**
* 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 have negative
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "negative side-effects on our main thread"? Maybe we should just say, "block the main thread"

* side-effects on the 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 {
synchronized(lock = Any()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't synchronized properly: each time we go through this, we lock a new instance so there is never any contention for this code block. You need to use a shared instance.

That being said, do we know that this class throws an exception on overflow? Maybe it just does something safe.

If not, I think the synchronization block could be replaced with something like getAndUpdate: https://developer.android.com/reference/java/util/concurrent/atomic/AtomicInteger?hl=en#getAndUpdate(java.util.function.IntUnaryOperator)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcomella so getAndUpdate is min API 24. Also, the AtomicInteger seems to wrap around like a regular integer. We could just have a function in the object and let it handle it itself?

if (RunBlockingCounter.count.get() >= Int.MAX_VALUE) RunBlockingCounter.count.set(0)
else RunBlockingCounter.count.addAndGet(1)
}

return if (context != null) {
runBlocking(context) { action() }
} else {
runBlocking { action() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
Loading