Skip to content

Commit 4bf5e11

Browse files
authored
Synchronize warming the reflection cache (airbnb#244)
In addition, we should have been checking for KVisibility == PUBLIC not isAccessible. As is, the second part of the reflection warming was filtering out all properties. Here is a benchmark in which I switched from FlowViewModel in which I added 80 @PersistState properties to another app and back several times with background processes turned off without declaredMemberProperties warming: 06-14 08:05:05.240 5295 5344 D Gabe : persistState: 21 06-14 08:05:05.308 5295 5295 D Gabe : persistState: 7 06-14 08:05:09.341 5390 5431 D Gabe : persistState: 32 06-14 08:05:12.328 5390 5390 D Gabe : persistState: 7 06-14 08:05:16.948 5484 5517 D Gabe : persistState: 28 06-14 08:05:19.604 5484 5484 D Gabe : persistState: 4 06-14 08:05:24.098 5569 5599 D Gabe : persistState: 37 06-14 08:05:24.568 5569 5569 D Gabe : persistState: 5 06-14 08:05:28.866 5652 5682 D Gabe : persistState: 16 06-14 08:05:29.542 5652 5652 D Gabe : persistState: 5 06-14 08:05:34.043 5736 5769 D Gabe : persistState: 21 with declaredMemberProperties warming: 06-14 08:05:50.617 5865 5916 D Gabe : persistState: 5 06-14 08:05:50.670 5865 5865 D Gabe : persistState: 8 06-14 08:05:54.826 5971 6004 D Gabe : persistState: 9 06-14 08:05:54.981 5971 5971 D Gabe : persistState: 3 06-14 08:05:59.318 6067 6100 D Gabe : persistState: 5 06-14 08:05:59.811 6067 6067 D Gabe : persistState: 5 06-14 08:06:03.791 6149 6185 D Gabe : persistState: 9 06-14 08:06:04.899 6149 6149 D Gabe : persistState: 4 06-14 08:06:10.128 6235 6265 D Gabe : persistState: 5 06-14 08:06:15.794 6318 6351 D Gabe : persistState: 6 Fixes airbnb#231
1 parent a6d0ccd commit 4bf5e11

File tree

2 files changed

+31
-25
lines changed

2 files changed

+31
-25
lines changed

mvrx/src/main/kotlin/com/airbnb/mvrx/BaseMvRxViewModel.kt

+30-24
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,11 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
4141
private val lastDeliveredStates = ConcurrentHashMap<String, Any>()
4242
private val activeSubscriptions = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
4343

44+
internal val state: S
45+
get() = stateStore.state
46+
4447
init {
45-
// Kotlin reflection has a large overhead the first time you run it
46-
// but then is pretty fast on subsequent times. Running these methods now will
47-
// initialize kotlin reflect and warm the cache so that when persistState() gets
48-
// called synchronously in onSaveInstanceState() on the main thread, it will be
49-
// much faster.
50-
// This improved performance 10-100x for a state with 100 @PersistState properties.
51-
Completable.fromCallable {
52-
initialState::class.primaryConstructor?.parameters?.forEach { it.annotations }
53-
initialState::class.declaredMemberProperties.asSequence()
54-
.filter { it.isAccessible }
55-
.forEach { prop ->
56-
@Suppress("UNCHECKED_CAST")
57-
(prop as? KProperty1<S, Any?>)?.get(initialState)
58-
}
59-
}.subscribeOn(Schedulers.computation()).subscribe()
48+
Completable.fromCallable { warmReflectionCache(initialState) }.subscribeOn(Schedulers.computation()).subscribe()
6049

6150
if (this.debugMode) {
6251
mutableStateChecker = MutableStateChecker(initialState)
@@ -66,8 +55,25 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
6655
}
6756
}
6857

69-
internal val state: S
70-
get() = stateStore.state
58+
/**
59+
* Kotlin reflection has a large overhead the first time you run it
60+
* but then is pretty fast on subsequent times. Running these methods now will
61+
* initialize kotlin reflect and warm the cache so that when persistState() gets
62+
* called synchronously in onSaveInstanceState() on the main thread, it will be much faster.
63+
* This improved performance 10-100x for a state with 100 @PersistState properties.
64+
*
65+
* This is also @Synchronized to prevent a ConcurrentModificationException in kotlin-reflect: https://gist.github.com/gpeal/27a5747b3c351d4bd592a8d2d58f134a
66+
*/
67+
@Synchronized
68+
fun warmReflectionCache(initialState: S) {
69+
initialState::class.primaryConstructor?.parameters?.forEach { it.annotations }
70+
initialState::class.declaredMemberProperties.asSequence()
71+
.filter { it.visibility == KVisibility.PUBLIC }
72+
.forEach { prop ->
73+
@Suppress("UNCHECKED_CAST")
74+
(prop as? KProperty1<S, Any?>)?.get(initialState)
75+
}
76+
}
7177

7278
@CallSuper
7379
override fun onCleared() {
@@ -108,14 +114,14 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
108114
if (changedProp != null) {
109115
throw IllegalArgumentException(
110116
"Impure reducer set on ${this@BaseMvRxViewModel::class.simpleName}! " +
111-
"${changedProp.name} changed from ${changedProp.get(firstState)} " +
112-
"to ${changedProp.get(secondState)}. " +
113-
"Ensure that your state properties properly implement hashCode."
117+
"${changedProp.name} changed from ${changedProp.get(firstState)} " +
118+
"to ${changedProp.get(secondState)}. " +
119+
"Ensure that your state properties properly implement hashCode."
114120
)
115121
} else {
116122
throw IllegalArgumentException(
117123
"Impure reducer set on ${this@BaseMvRxViewModel::class.simpleName}! Differing states were provided by the same reducer." +
118-
"Ensure that your state properties properly implement hashCode. First state: $firstState -> Second state: $secondState"
124+
"Ensure that your state properties properly implement hashCode. First state: $firstState -> Second state: $secondState"
119125
)
120126
}
121127
}
@@ -562,9 +568,9 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
562568
if (activeSubscriptions.contains(deliveryMode.subscriptionId)) {
563569
throw IllegalStateException(
564570
"Subscribing with a duplicate subscription id: ${deliveryMode.subscriptionId}. " +
565-
"If you have multiple uniqueOnly subscriptions in a MvRx view that listen to the same properties " +
566-
"you must use a custom subscription id. If you are using a custom MvRxView, make sure you are using the proper" +
567-
"lifecycle owner. See BaseMvRxFragment for an example."
571+
"If you have multiple uniqueOnly subscriptions in a MvRx view that listen to the same properties " +
572+
"you must use a custom subscription id. If you are using a custom MvRxView, make sure you are using the proper" +
573+
"lifecycle owner. See BaseMvRxFragment for an example."
568574
)
569575
}
570576
activeSubscriptions.add(deliveryMode.subscriptionId)

mvrx/src/test/kotlin/com/airbnb/mvrx/BaseTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ abstract class BaseTest {
3737
// this prevents StackOverflowErrors when scheduling with a delay
3838
override fun scheduleDirect(@NonNull run: Runnable, delay: Long, @NonNull unit: TimeUnit): Disposable = super.scheduleDirect(run, 0, unit)
3939

40-
override fun createWorker(): Scheduler.Worker = ExecutorScheduler.ExecutorWorker(Executor { it.run() })
40+
override fun createWorker(): Worker = ExecutorScheduler.ExecutorWorker(Executor { it.run() }, true)
4141
}
4242
RxJavaPlugins.setNewThreadSchedulerHandler { immediate }
4343
RxJavaPlugins.setComputationSchedulerHandler { immediate }

0 commit comments

Comments
 (0)