Skip to content

Commit c6620d7

Browse files
generatedunixname1608173377072046meta-codesync[bot]
authored andcommitted
Fix NullPointerException in ImageTracker.removeDrawable due to concurrent access
Reviewed By: oprisnik Differential Revision: D116669954 fbshipit-source-id: 5a939f2294a2fbd75d9096123311a2fd628c72f7
1 parent 1566dd8 commit c6620d7

2 files changed

Lines changed: 127 additions & 5 deletions

File tree

vito/tools/liveeditor/src/main/java/com/facebook/fresco/vito/tools/liveeditor/ImageTracker.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,30 @@ open class ImageTracker : BaseVitoImagePerfListener() {
1515
private val drawables: MutableList<WeakReference<FrescoDrawableInterface>> = ArrayList()
1616

1717
val drawableCount: Int
18-
get() = drawables.size
18+
get() = synchronized(drawables) { drawables.size }
1919

20-
fun getDrawableOrNull(index: Int): FrescoDrawableInterface? = drawables.getOrNull(index)?.get()
20+
fun getDrawableOrNull(index: Int): FrescoDrawableInterface? =
21+
synchronized(drawables) { drawables.getOrNull(index)?.get() }
2122

2223
fun reset() {
23-
drawables.clear()
24+
synchronized(drawables) { drawables.clear() }
2425
}
2526

2627
private fun trackDrawable(drawable: FrescoDrawableInterface) {
27-
drawables.add(WeakReference(drawable))
28+
synchronized(drawables) { drawables.add(WeakReference(drawable)) }
2829
}
2930

3031
private fun removeDrawable(drawable: FrescoDrawableInterface) {
3132
// Remove the Drawable and any null drawables that have been collected (weak reference)
32-
drawables.removeAll { it.get() == drawable || it.get() == null }
33+
synchronized(drawables) {
34+
val iterator = drawables.iterator()
35+
while (iterator.hasNext()) {
36+
val ref = iterator.next()
37+
if (ref.get() == null || ref.get() == drawable) {
38+
iterator.remove()
39+
}
40+
}
41+
}
3342
}
3443

3544
override fun onImageMount(drawable: FrescoDrawableInterface) {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.fresco.vito.tools.liveeditor
9+
10+
import android.graphics.RectF
11+
import android.graphics.drawable.Drawable
12+
import com.facebook.drawee.drawable.VisibilityCallback
13+
import com.facebook.fresco.vito.core.FrescoDrawableInterface
14+
import com.facebook.fresco.vito.core.ImagePerfLoggingListener
15+
import com.facebook.fresco.vito.core.VitoImagePerfListener
16+
import com.facebook.fresco.vito.core.VitoImageRequest
17+
import com.facebook.fresco.vito.core.impl.BaseVitoImagePerfListener
18+
import com.facebook.fresco.vito.listener.ImageListener
19+
import java.util.concurrent.CopyOnWriteArrayList
20+
import java.util.concurrent.CountDownLatch
21+
import java.util.concurrent.TimeUnit
22+
import org.assertj.core.api.Assertions.assertThat
23+
import org.junit.Test
24+
import org.junit.runner.RunWith
25+
import org.robolectric.RobolectricTestRunner
26+
27+
/**
28+
* Stress test for the unsynchronized `ArrayList` inside [ImageTracker]. `onImageMount` /
29+
* `onImageUnmount` are invoked from view attach/detach callbacks on whatever thread mounts the
30+
* image, so the backing list is mutated concurrently.
31+
*/
32+
@RunWith(RobolectricTestRunner::class)
33+
class ImageTrackerConcurrencyTest {
34+
35+
private class FakeFrescoDrawable(override val imageId: Long) : FrescoDrawableInterface {
36+
override var callerContext: Any? = null
37+
override val imagePerfListener: VitoImagePerfListener = BaseVitoImagePerfListener()
38+
override var uiFramework: String? = null
39+
override var forceReloadIfImageAlreadySet: Boolean = false
40+
override var retriggerListenersIfImageAlreadySet: Boolean = false
41+
override val actualImageDrawable: Drawable? = null
42+
override val isFetchSubmitted: Boolean = false
43+
override var imageRequest: VitoImageRequest? = null
44+
override var imageListener: ImageListener? = null
45+
override var extras: Any? = null
46+
override var refetchRunnable: Runnable? = null
47+
48+
override fun setMutateDrawables(mutateDrawables: Boolean) = Unit
49+
50+
override fun hasImage(): Boolean = false
51+
52+
override fun setFetchSubmitted(fetchSubmitted: Boolean) = Unit
53+
54+
override fun setVisibilityCallback(visibilityCallback: VisibilityCallback?) = Unit
55+
56+
override fun setOverlayDrawable(drawable: Drawable?): Drawable? = null
57+
58+
override fun getImagePerfLoggingListener(): ImagePerfLoggingListener? = null
59+
60+
override fun setIntrinsicSize(width: Int, height: Int) = Unit
61+
62+
override fun configureWhenUnderlyingChanged() = Unit
63+
64+
override fun getActualImageBounds(outBounds: RectF) = Unit
65+
66+
override fun hasBitmapWithGainmap(): Boolean = false
67+
68+
override fun reportVisible(visible: Boolean) = Unit
69+
}
70+
71+
@Test
72+
fun testMountUnmount_whenCalledConcurrently_thenNoException() {
73+
val threadCount = 16
74+
val opsPerThread = 4_000
75+
val failures = CopyOnWriteArrayList<Throwable>()
76+
77+
var iteration = 0
78+
while (iteration < 5 && failures.isEmpty()) {
79+
iteration++
80+
val tracker = ImageTracker()
81+
val start = CountDownLatch(1)
82+
val done = CountDownLatch(threadCount)
83+
val threads =
84+
(0 until threadCount).map { threadIndex ->
85+
Thread {
86+
try {
87+
start.await()
88+
for (op in 0 until opsPerThread) {
89+
val drawable = FakeFrescoDrawable((threadIndex * opsPerThread + op).toLong())
90+
tracker.onImageMount(drawable)
91+
tracker.onImageUnmount(drawable)
92+
}
93+
} catch (t: Throwable) {
94+
failures.add(t)
95+
} finally {
96+
done.countDown()
97+
}
98+
}
99+
}
100+
threads.forEach { it.start() }
101+
start.countDown()
102+
done.await(120, TimeUnit.SECONDS)
103+
threads.forEach { it.join(1_000) }
104+
}
105+
106+
assertThat(failures)
107+
.describedAs(
108+
"ImageTracker.onImageMount/onImageUnmount threw under concurrent access: " +
109+
failures.joinToString("\n") { it.toString() },
110+
)
111+
.isEmpty()
112+
}
113+
}

0 commit comments

Comments
 (0)