Skip to content

Commit 4425a1b

Browse files
markushiadinauer
authored andcommitted
fix(android): Ensure frame metrics listeners are registered/unregistered on the main thread (#4582)
* fix(android): Ensure frame metrics listeners are registered/unregistered on the main thread * Fix race conditions * Update Changelog * Update CHANGELOG.md * Address PR feedback
1 parent d4ba02b commit 4425a1b

File tree

3 files changed

+106
-18
lines changed

3 files changed

+106
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
}
2323
```
2424
- Fix abstract method error in `SentrySupportSQLiteDatabase` ([#4597](https://github.com/getsentry/sentry-java/pull/4597))
25+
- Ensure frame metrics listeners are registered/unregistered on the main thread ([#4582](https://github.com/getsentry/sentry-java/pull/4582))
2526

2627
## 8.18.0
2728

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public final class SentryFrameMetricsCollector implements Application.ActivityLi
3838

3939
private final @NotNull BuildInfoProvider buildInfoProvider;
4040
private final @NotNull Set<Window> trackedWindows = new CopyOnWriteArraySet<>();
41+
4142
private final @NotNull ILogger logger;
4243
private @Nullable Handler handler;
4344
private @Nullable WeakReference<Window> currentWindow;
@@ -282,17 +283,20 @@ public void stopCollection(final @Nullable String listenerId) {
282283

283284
@SuppressLint("NewApi")
284285
private void stopTrackingWindow(final @NotNull Window window) {
285-
if (trackedWindows.contains(window)) {
286-
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N) {
287-
try {
288-
windowFrameMetricsManager.removeOnFrameMetricsAvailableListener(
289-
window, frameMetricsAvailableListener);
290-
} catch (Exception e) {
291-
logger.log(SentryLevel.ERROR, "Failed to remove frameMetricsAvailableListener", e);
292-
}
293-
}
294-
trackedWindows.remove(window);
295-
}
286+
new Handler(Looper.getMainLooper())
287+
.post(
288+
() -> {
289+
try {
290+
// Re-check if we should still remove the listener for this window
291+
// in case trackCurrentWindow was called in the meantime
292+
if (trackedWindows.remove(window)) {
293+
windowFrameMetricsManager.removeOnFrameMetricsAvailableListener(
294+
window, frameMetricsAvailableListener);
295+
}
296+
} catch (Throwable e) {
297+
logger.log(SentryLevel.ERROR, "Failed to remove frameMetricsAvailableListener", e);
298+
}
299+
});
296300
}
297301

298302
private void setCurrentWindow(final @NotNull Window window) {
@@ -305,18 +309,29 @@ private void setCurrentWindow(final @NotNull Window window) {
305309

306310
@SuppressLint("NewApi")
307311
private void trackCurrentWindow() {
308-
Window window = currentWindow != null ? currentWindow.get() : null;
312+
@Nullable Window window = currentWindow != null ? currentWindow.get() : null;
309313
if (window == null || !isAvailable) {
310314
return;
311315
}
312316

313-
if (!trackedWindows.contains(window) && !listenerMap.isEmpty()) {
317+
if (listenerMap.isEmpty()) {
318+
return;
319+
}
314320

315-
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N && handler != null) {
316-
trackedWindows.add(window);
317-
windowFrameMetricsManager.addOnFrameMetricsAvailableListener(
318-
window, frameMetricsAvailableListener, handler);
319-
}
321+
if (handler != null) {
322+
// Ensure the addOnFrameMetricsAvailableListener is called on the main thread
323+
new Handler(Looper.getMainLooper())
324+
.post(
325+
() -> {
326+
if (trackedWindows.add(window)) {
327+
try {
328+
windowFrameMetricsManager.addOnFrameMetricsAvailableListener(
329+
window, frameMetricsAvailableListener, handler);
330+
} catch (Throwable e) {
331+
logger.log(SentryLevel.ERROR, "Failed to add frameMetricsAvailableListener", e);
332+
}
333+
}
334+
});
320335
}
321336
}
322337

@@ -373,13 +388,19 @@ default void addOnFrameMetricsAvailableListener(
373388
final @NotNull Window window,
374389
final @Nullable Window.OnFrameMetricsAvailableListener frameMetricsAvailableListener,
375390
final @Nullable Handler handler) {
391+
if (frameMetricsAvailableListener == null) {
392+
return;
393+
}
376394
window.addOnFrameMetricsAvailableListener(frameMetricsAvailableListener, handler);
377395
}
378396

379397
@RequiresApi(api = Build.VERSION_CODES.N)
380398
default void removeOnFrameMetricsAvailableListener(
381399
final @NotNull Window window,
382400
final @Nullable Window.OnFrameMetricsAvailableListener frameMetricsAvailableListener) {
401+
if (frameMetricsAvailableListener == null) {
402+
return;
403+
}
383404
window.removeOnFrameMetricsAvailableListener(frameMetricsAvailableListener);
384405
}
385406
}

sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ class SentryFrameMetricsCollectorTest {
148148
collector.startCollection(mock())
149149
assertEquals(0, fixture.addOnFrameMetricsAvailableListenerCounter)
150150
collector.onActivityStarted(fixture.activity)
151+
// Execute pending main looper tasks since addOnFrameMetricsAvailableListener is posted to main
152+
// thread
153+
Shadows.shadowOf(Looper.getMainLooper()).idle()
151154
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
152155
}
153156

@@ -157,8 +160,12 @@ class SentryFrameMetricsCollectorTest {
157160

158161
collector.startCollection(mock())
159162
collector.onActivityStarted(fixture.activity)
163+
// Execute pending add operations
164+
Shadows.shadowOf(Looper.getMainLooper()).idle()
160165
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
161166
collector.onActivityStopped(fixture.activity)
167+
// Execute pending remove operations
168+
Shadows.shadowOf(Looper.getMainLooper()).idle()
162169
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
163170
}
164171

@@ -181,6 +188,8 @@ class SentryFrameMetricsCollectorTest {
181188
collector.onActivityStarted(fixture.activity)
182189
assertEquals(0, fixture.addOnFrameMetricsAvailableListenerCounter)
183190
collector.startCollection(mock())
191+
// Execute pending main looper tasks
192+
Shadows.shadowOf(Looper.getMainLooper()).idle()
184193
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
185194
}
186195

@@ -189,9 +198,13 @@ class SentryFrameMetricsCollectorTest {
189198
val collector = fixture.getSut(context)
190199
val id = collector.startCollection(mock())
191200
collector.onActivityStarted(fixture.activity)
201+
// Execute pending add operations
202+
Shadows.shadowOf(Looper.getMainLooper()).idle()
192203

193204
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
194205
collector.stopCollection(id)
206+
// Execute pending remove operations
207+
Shadows.shadowOf(Looper.getMainLooper()).idle()
195208
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
196209
}
197210

@@ -205,9 +218,13 @@ class SentryFrameMetricsCollectorTest {
205218

206219
collector.onActivityStarted(fixture.activity)
207220
collector.onActivityStarted(fixture.activity)
221+
// Execute pending add operations
222+
Shadows.shadowOf(Looper.getMainLooper()).idle()
208223

209224
collector.onActivityStopped(fixture.activity)
210225
collector.onActivityStopped(fixture.activity)
226+
// Execute pending remove operations
227+
Shadows.shadowOf(Looper.getMainLooper()).idle()
211228

212229
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
213230
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
@@ -228,9 +245,13 @@ class SentryFrameMetricsCollectorTest {
228245
collector.startCollection(mock())
229246
collector.onActivityStarted(fixture.activity)
230247
collector.onActivityStarted(fixture.activity2)
248+
// Execute pending add operations
249+
Shadows.shadowOf(Looper.getMainLooper()).idle()
231250
assertEquals(2, fixture.addOnFrameMetricsAvailableListenerCounter)
232251
collector.onActivityStopped(fixture.activity)
233252
collector.onActivityStopped(fixture.activity2)
253+
// Execute pending remove operations
254+
Shadows.shadowOf(Looper.getMainLooper()).idle()
234255
assertEquals(2, fixture.removeOnFrameMetricsAvailableListenerCounter)
235256
}
236257

@@ -240,10 +261,13 @@ class SentryFrameMetricsCollectorTest {
240261
val id1 = collector.startCollection(mock())
241262
val id2 = collector.startCollection(mock())
242263
collector.onActivityStarted(fixture.activity)
264+
Shadows.shadowOf(Looper.getMainLooper()).idle()
243265
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
244266
collector.stopCollection(id1)
267+
Shadows.shadowOf(Looper.getMainLooper()).idle()
245268
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
246269
collector.stopCollection(id2)
270+
Shadows.shadowOf(Looper.getMainLooper()).idle()
247271
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
248272
}
249273

@@ -511,6 +535,48 @@ class SentryFrameMetricsCollectorTest {
511535
)
512536
}
513537

538+
@Test
539+
fun `collector calls addOnFrameMetricsAvailableListener on main thread`() {
540+
val collector = fixture.getSut(context)
541+
542+
collector.startCollection(mock())
543+
collector.onActivityStarted(fixture.activity)
544+
545+
assertEquals(0, fixture.addOnFrameMetricsAvailableListenerCounter)
546+
Shadows.shadowOf(Looper.getMainLooper()).idle()
547+
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
548+
}
549+
550+
@Test
551+
fun `collector calls removeOnFrameMetricsAvailableListener on main thread`() {
552+
val collector = fixture.getSut(context)
553+
collector.startCollection(mock())
554+
collector.onActivityStarted(fixture.activity)
555+
Shadows.shadowOf(Looper.getMainLooper()).idle()
556+
557+
collector.onActivityStopped(fixture.activity)
558+
assertEquals(0, fixture.removeOnFrameMetricsAvailableListenerCounter)
559+
Shadows.shadowOf(Looper.getMainLooper()).idle()
560+
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
561+
}
562+
563+
@Test
564+
fun `collector prevents race condition when stop is called immediately after start`() {
565+
val collector = fixture.getSut(context)
566+
567+
collector.startCollection(mock())
568+
collector.onActivityStarted(fixture.activity)
569+
collector.onActivityStopped(fixture.activity)
570+
571+
// Now execute all pending operations
572+
Shadows.shadowOf(Looper.getMainLooper()).idle()
573+
574+
// as the listeners are posted to the main thread, we expect an add followed by a remove
575+
assertEquals(1, fixture.addOnFrameMetricsAvailableListenerCounter)
576+
assertEquals(1, fixture.removeOnFrameMetricsAvailableListenerCounter)
577+
assertEquals(0, collector.getProperty<Set<Window>>("trackedWindows").size)
578+
}
579+
514580
private fun createMockWindow(refreshRate: Float = 60F): Window {
515581
val mockWindow = mock<Window>()
516582
val mockDisplay = mock<Display>()

0 commit comments

Comments
 (0)