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 fixupstream-sync
parent
d46fc7b142
commit
7b1af41b40
@ -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))
|
||||||
|
}
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -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…
Reference in New Issue