-
Notifications
You must be signed in to change notification settings - Fork 1.3k
15278 detekt rule runblocking #15942
15278 detekt rule runblocking #15942
Conversation
…d its config to the yaml
…st.kt to PerformanceStartupTest.kt and added runBlockingCount test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good – I have fair number of comments but they're mostly nits. Requesting to see the changes to make sure the naming seems sound.
app/src/test/java/org/mozilla/fenix/library/bookmarks/DesktopFoldersTest.kt
Outdated
Show resolved
Hide resolved
mozilla-detekt-rules/src/main/java/org/mozilla/fenix/detektrules/MozillaRunblockingCheck.kt
Outdated
Show resolved
Hide resolved
mozilla-detekt-rules/src/main/java/org/mozilla/fenix/detektrules/MozillaRunblockingCheck.kt
Outdated
Show resolved
Hide resolved
mozilla-detekt-rules/src/main/java/org/mozilla/fenix/detektrules/MozillaRunblockingCheck.kt
Outdated
Show resolved
Hide resolved
is the optimal solution. | ||
|
||
""" | ||
|
||
/** | ||
* A performance test to limit the number of StrictMode suppressions on startup. | ||
* This test was written by the perf team. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We changed the file name: we need to update CODEOWNERS.
config/detekt.yml
Outdated
@@ -117,6 +117,8 @@ mozilla-detekt-rules: | |||
excludes: "**/*Test.kt, **/*Spec.kt, **/test/**, **/androidTest/**" | |||
MozillaCorrectUnitTestRunner: | |||
active: true | |||
MozillaRunblockingCheck: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to update this name if you rename this
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) | ||
class RunBlockingCounter { | ||
companion object { | ||
var runBlockingCount = 0L |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this runs in production code, I'm starting to think we should prevent overflow. The app shouldn't have a hard limit on how long it can run. Maybe in the setter? We could probably change it back to an Int in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can always add it to the runBlockingIncrement
function. I just felt uneasy about adding more functionality (albeit its probably minimal) to a function that's just suppose to wrap one provided by Kotlin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you want to avoid adding too much functionality to a wrapping method?
/** | ||
* Wrapper around `runBlocking` | ||
*/ | ||
fun <T> runBlockingIncrement( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love this name. While it's telling us what it does, it's also kind of vague – increment what?
Naming this seems really hard. The intentions I can think of are:
- tell them what it does (*increment, *perf, *monitor, ...)
- tell them it's preferred (*safe, *preferred, ...)
Maybe runBlockingMonitored
? It sounds intimidating so maybe people will think twice before using it. 🤣 Or maybe they'll think we're the perf 👮♀️ :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that can work. I was thinking that 1) if the kdoc explains it further in details, then a looser name could be fine? Or 2) we could go longer, but more precise such as runBlockingPerformanceTracker
? It seems a bit less intimidating and its purpose is conveyed a lot more from the name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think loose names are only okay if the function is frequently used, e.g. map
or add
. Given this is a function people aren't supposed to call a lot anyway, I think it's fine to have a longer name. However, I wanted to avoid making it too tied to performance so people don't think, "perf police!"
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) | ||
class RunBlockingCounter { | ||
companion object { | ||
var runBlockingCount = 0L |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be synchronized (if it's synchronized, does it automatically get volatile too? I'm not sure)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I add @Synchronize set
for this variable, isn't there an issue where Thread 1 --> Increases counter, then runs its method, returns and Thread 2 --> waits for the method to return before increasing? Although I don't think that would happen, since the lock would be released as soon as we're done incrementing. I guess the only issue would be if a billion threads queue'd up at the same time to increment the counter, which I think is unlikely.
Maybe I'm overthinking it though, I added the @Synchronized
with my latest fix. I saw nothing locally but it was just a thought.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's possible. I'm concerned that the value will not be accessed correctly in concurrent situations, however.
Actually, I don't know how the java memory model works. Is assignment atomic? In that case, I don't think we need synchronized, just volatile. We could also change it to an AtomicInteger
(or whatever) to to avoid having to figure it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I remember, if the interger is volatile
it is kept in the main memory and access to that integer is the same throughout different threads. I think, however, that AtomicInteger
can solve this issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mcomella So I added AtomicInteger
but I also think we need to synchronized(...)
on the check for overflow since two threads my overlap. what do you think?
…moved RunBlockingCounter to perf package
…without restting counter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty close but we still have a few things to iron out such as:
- concurrency
- clarity of logic
@@ -39,20 +54,23 @@ private const val FAILURE_MSG = """StrictMode startup suppression count does not | |||
* | |||
* IF YOU UPDATE THE TEST NAME, UPDATE CODE OWNERS. |
There was a problem hiding this comment.
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?
is the optimal solution. | ||
|
||
""" | ||
|
||
/** | ||
* A performance test to limit the number of StrictMode suppressions on startup. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: update the comment
null | ||
fun installedSearchEngines(context: Context): SearchEngineList = | ||
runBlockingIncrement { | ||
val installedIdentifiers = installedSearchEngineIdentifiers(context) |
There was a problem hiding this comment.
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?
loadedSearchEngines.await().list.map { it.identifier } | ||
} | ||
fun allSearchEngineIdentifiers() = | ||
runBlockingIncrement { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: avoid blame
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() |
There was a problem hiding this comment.
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
config/detekt.yml
Outdated
@@ -117,6 +117,9 @@ mozilla-detekt-rules: | |||
excludes: "**/*Test.kt, **/*Spec.kt, **/test/**, **/androidTest/**" | |||
MozillaCorrectUnitTestRunner: | |||
active: true | |||
MozillaRunBlockingCheck: | |||
active: true | |||
excludes: "**/*Test.kt" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we should copy the same exclusion blocks as all the others. e.g. we don't want to enforce this in androidTest
either.
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) | ||
object RunBlockingCounter { | ||
var count = 0L | ||
@Synchronized set |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per my earlier comment, this might just need to be volatile – can you check how java assignment works? Is +=
atomic? Maybe we need AtomicInteger
to increment.
*/ | ||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) | ||
object RunBlockingCounter { | ||
var count = 0L |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle overflow? fwiw, I like handling it here, in the setter, rather than in runBlockingIncrement because it keeps the other method simple. That being said, it might not be possible with AtomicInteger
.
): T { | ||
RunBlockingCounter.count += 1 | ||
|
||
context?.let { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't do what you think it does. It does:
if (context != null) {
val returnValue = runBlocking(context) { action() }
if (returnValue != null) {
runBlocking(action())
}
}
I guess unless return return from the containing function and not the lambda. Even if the code is correct, the fact that it can be misinterpreted isn't great. In general, it's good to be careful when using symbols (?:
) or unintuitively short functions (let
, run
, and friends) to express something.
I would prefer
return if (context != null) {
runBlocking(it, ::action)
} else {
runBlocking(::action)
}
Note that the function reference is an optimization that avoids us having to create a new lambda (if the compiler isn't smart enough to do it for us). Note also the deduplication of the return
statement.
import kotlin.math.exp | ||
|
||
private const val VIOLATION_MSG = "Please use `org.mozilla.fenix.perf.RunBlockingCounter" + | ||
".fenixRunBlocking` instead because it allows us to monitor the code for performance " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs to be updated. Remember, the file name won't be used.
Codecov Report
@@ Coverage Diff @@
## master #15942 +/- ##
============================================
+ Coverage 29.99% 30.02% +0.02%
- Complexity 1204 1207 +3
============================================
Files 451 453 +2
Lines 18513 18525 +12
Branches 2546 2549 +3
============================================
+ Hits 5553 5562 +9
- Misses 12531 12532 +1
- Partials 429 431 +2
Continue to review full report at Codecov.
|
context: CoroutineContext? = null, | ||
action: suspend CoroutineScope.() -> T | ||
): T { | ||
synchronized(lock = Any()) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks good to me but I'd like some of the nits addressed. If we change incrementCounter
to an extension function as I suggested, it might be good to flag me for re-review on that one specific section.
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 |
There was a problem hiding this comment.
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.
@@ -137,7 +137,9 @@ 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we keep this as one line? I want to avoid these changes "double-spacing" the code.
} else { | ||
null | ||
fun installedSearchEngines(context: Context): SearchEngineList = runBlockingIncrement { | ||
val installedIdentifiers = installedSearchEngineIdentifiers(context) |
There was a problem hiding this comment.
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.
loadedSearchEngines.await().list.map { it.identifier } | ||
} | ||
fun allSearchEngineIdentifiers() = runBlockingIncrement { | ||
loadedSearchEngines.await().list.map { it.identifier } |
There was a problem hiding this comment.
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.
* 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: move to org.mozilla.fenix.perf
to match the production code
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny | ||
import kotlin.math.exp | ||
|
||
private const val VIOLATION_MSG = "Please use `org.mozilla.fenix.perf.RunBlockingCounter` instead " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This function name is outdated.
object RunBlockingCounter { | ||
var count = AtomicInteger(0) | ||
|
||
fun incrementCounter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think it's simpler to move this to be an extension function, e.g. fun AtomicInteger.getAndIncrementNoOverflow()
. It lends itself to better testing (why isn't this method tested? :P), re-use, and more greatly discourages touching any code outside of the AtomicInteger, unlike a function in a companion object.
do { | ||
prev = count.get() | ||
next = prev + 1 | ||
if (next == Integer.MAX_VALUE) next = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: while we're not going to hit this case, I think it's more honest to cap it at Integer.MAX_VALUE
rather than reset the counter. e.g. if you look at the counter after running the program after a few days and its 100, you might be a little confused.
*/ | ||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) | ||
object RunBlockingCounter { | ||
var count = AtomicInteger(0) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"
…ion and fixed indentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some brief clean up changes – these could land as a follow-up so we can get this landed.
Note that you'll also have to rebase.
/** | ||
* Increases an AtomicInteger safely. | ||
*/ | ||
fun AtomicInteger.getAndIncrementNoOverflow(counter: AtomicInteger) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't pass it as an argument – you should use this
instead.
/** | ||
* Increases an AtomicInteger safely. | ||
*/ | ||
fun AtomicInteger.getAndIncrementNoOverflow(counter: AtomicInteger) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we write tests for this?
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny | ||
import kotlin.math.exp | ||
|
||
private const val VIOLATION_MSG = "Please use `org.mozilla.fenix.perf.RunBlockingCounter.runBlockingIncrement`" + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's org.mozilla.fenix.perf.runBlockingImplement
– top level functions don't include the file name in their fully-qualified class name.
Added a
detekt
rule forrunBlocking
since we noticed that it can affect performance at times. So I switchedrunBlocking
torunBlockingCounter
(which just wraps therunBlocking
call) so that we can monitor the # of times it is being called (through tests and other means)Pull Request checklist
To download an APK when reviewing a PR: