Skip to content

Commit 6cf958a

Browse files
authored
[JankStatMonitor] Add early flagging and log internal errors instead of handled errors (#905)
1 parent 874a992 commit 6cf958a

File tree

3 files changed

+70
-28
lines changed

3 files changed

+70
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
**Fixed**
3131

32+
- Fix memory leak in JankStatsMonitor when activity is destroyed without `onPause` being called
3233
- Fix inaccurate calculation at `Battery Level Change Per Min`
3334

3435
### iOS

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/performance/JankStatsMonitor.kt

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ internal class JankStatsMonitor(
6262
JankStats.OnFrameListener {
6363
@VisibleForTesting
6464
internal var jankStats: JankStats? = null
65+
private var currentWindow: Window? = null
6566
private var performanceMetricsStateHolder: PerformanceMetricsState.Holder? = null
6667

6768
override fun start() {
@@ -101,7 +102,7 @@ internal class JankStatsMonitor(
101102
val errorMessage =
102103
"Unexpected frame duration. durationInNano: $durationNano." +
103104
" durationMillis: $durationMillis"
104-
logger.handleInternalError(errorMessage)
105+
logInternalError(errorMessage)
105106
return
106107
}
107108

@@ -121,7 +122,7 @@ internal class JankStatsMonitor(
121122
}
122123

123124
/**
124-
* [LifecycleEventObserver] callback to determine when Application is first created
125+
* [LifecycleEventObserver] callback to determine when the Application process is first resumed
125126
*/
126127
override fun onStateChanged(
127128
source: LifecycleOwner,
@@ -130,11 +131,11 @@ internal class JankStatsMonitor(
130131
if (!runtime.isEnabled(RuntimeFeature.DROPPED_EVENTS_MONITORING)) {
131132
return
132133
}
133-
if (event == Lifecycle.Event.ON_CREATE) {
134+
if (event == Lifecycle.Event.ON_RESUME) {
134135
windowManager.findFirstValidActivity()?.let {
135136
setJankStatsForCurrentWindow(it.window)
136137
}
137-
// We are done detecting initial Application ON_CREATE, we don't need to listen anymore
138+
// We are done detecting initial Application ON_RESUME, we don't need to listen anymore
138139
processLifecycleOwner.lifecycle.removeObserver(this)
139140
}
140141
}
@@ -147,7 +148,7 @@ internal class JankStatsMonitor(
147148
}
148149

149150
override fun onActivityPaused(activity: Activity) {
150-
stopCollection()
151+
stopCollectionIfNeeded(activity)
151152
}
152153

153154
override fun onActivityCreated(
@@ -173,7 +174,7 @@ internal class JankStatsMonitor(
173174
}
174175

175176
override fun onActivityDestroyed(activity: Activity) {
176-
// no-op
177+
stopCollectionIfNeeded(activity)
177178
}
178179

179180
fun trackScreenNameChanged(screenName: String) {
@@ -190,20 +191,25 @@ internal class JankStatsMonitor(
190191
return
191192
}
192193
jankStats = JankStats.createAndTrack(window, this)
194+
currentWindow = window
193195
performanceMetricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(window.decorView.rootView)
194196
jankStats?.jankHeuristicMultiplier = runtime.getConfigValue(RuntimeConfig.JANK_FRAME_HEURISTICS_MULTIPLIER).toFloat()
195197
} catch (illegalStateException: IllegalStateException) {
196-
logger.handleInternalError(
197-
"Couldn't create JankStats instance",
198-
illegalStateException,
199-
)
198+
logInternalError(errorMessage = "Couldn't create JankStats instance", throwable = illegalStateException)
199+
}
200+
}
201+
202+
private fun stopCollectionIfNeeded(activity: Activity) {
203+
if (activity.window == currentWindow) {
204+
stopCollection()
200205
}
201206
}
202207

203208
private fun stopCollection() {
204209
jankStats?.isTrackingEnabled = false
205210
performanceMetricsStateHolder?.state?.removeState(SCREEN_NAME_KEY)
206211
jankStats = null
212+
currentWindow = null
207213
performanceMetricsStateHolder = null
208214
}
209215

@@ -256,6 +262,20 @@ internal class JankStatsMonitor(
256262
}
257263
}
258264

265+
private fun logInternalError(
266+
errorMessage: String,
267+
throwable: Throwable? = null,
268+
) {
269+
logger.logInternal(
270+
type = LogType.INTERNALSDK,
271+
level = LogLevel.ERROR,
272+
arrayFields = ArrayFields.EMPTY,
273+
throwable = throwable,
274+
) {
275+
errorMessage
276+
}
277+
}
278+
259279
/**
260280
* The different type of Janks according to Play Vitals
261281
*/

platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/events/performance/JankStatsMonitorTest.kt

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.nhaarman.mockitokotlin2.mock
2323
import com.nhaarman.mockitokotlin2.verify
2424
import com.nhaarman.mockitokotlin2.whenever
2525
import io.bitdrift.capture.IInternalLogger
26+
import io.bitdrift.capture.LogLevel
2627
import io.bitdrift.capture.LogType
2728
import io.bitdrift.capture.Mocks
2829
import io.bitdrift.capture.common.IWindowManager
@@ -52,8 +53,8 @@ class JankStatsMonitorTest {
5253
private val windowManager: IWindowManager = mock()
5354
private val mainThreadHandler = Mocks.sameThreadHandler
5455

55-
private val errorMessageCaptor = argumentCaptor<String>()
5656
private val illegalStateExceptionCaptor = argumentCaptor<IllegalStateException>()
57+
private val logMessageCaptor = argumentCaptor<() -> String>()
5758

5859
private lateinit var application: Application
5960
private lateinit var activity: Activity
@@ -267,11 +268,15 @@ class JankStatsMonitorTest {
267268

268269
jankStatsMonitor.onActivityResumed(mockedActivity)
269270

270-
verify(logger).handleInternalError(
271-
errorMessageCaptor.capture(),
272-
illegalStateExceptionCaptor.capture(),
271+
verify(logger).logInternal(
272+
type = eq(LogType.INTERNALSDK),
273+
level = eq(LogLevel.ERROR),
274+
arrayFields = eq(ArrayFields.EMPTY),
275+
throwable = illegalStateExceptionCaptor.capture(),
276+
blocking = eq(false),
277+
message = logMessageCaptor.capture(),
273278
)
274-
assertThat(errorMessageCaptor.lastValue).isEqualTo("Couldn't create JankStats instance")
279+
assertThat(logMessageCaptor.firstValue()).isEqualTo("Couldn't create JankStats instance")
275280
assertThat(illegalStateExceptionCaptor.lastValue).isInstanceOf(IllegalStateException::class.java)
276281
assertThat(
277282
illegalStateExceptionCaptor.lastValue.message,
@@ -315,6 +320,27 @@ class JankStatsMonitorTest {
315320
)
316321
}
317322

323+
@Test
324+
fun onActivityDestroyed_afterOnStateChangedAttachesJankStats_shouldClearJankStatsWithoutLeak() {
325+
// 1. SDK starts, onStateChanged ON_CREATE and ON_RESUME triggers findFirstValidActivity
326+
// which returns our activity and attaches JankStats to its window
327+
jankStatsMonitor.onStateChanged(processLifecycleOwner, Lifecycle.Event.ON_CREATE)
328+
jankStatsMonitor.onStateChanged(processLifecycleOwner, Lifecycle.Event.ON_RESUME)
329+
330+
assertThat(jankStatsMonitor.jankStats).isNotNull()
331+
332+
// 2. Activity is destroyed WITHOUT going through onActivityPaused
333+
// (simulates finish() from onCreate, or activity was already paused when SDK started)
334+
jankStatsMonitor.onActivityDestroyed(activity)
335+
336+
// 3. Prior Leak: jankStats wasn't null before
337+
assertThat(jankStatsMonitor.jankStats).isNull()
338+
339+
jankStatsMonitor.onActivityResumed(activity)
340+
341+
assertThat(jankStatsMonitor.jankStats).isNotNull
342+
}
343+
318344
private fun triggerOnFrame(
319345
isJankyFrame: Boolean,
320346
durationInMilli: Long,
@@ -349,19 +375,14 @@ class JankStatsMonitorTest {
349375
}
350376

351377
private fun assertWrongDuration(expectedMessage: String) {
352-
verify(logger, never()).logInternal(
353-
any(),
354-
any(),
355-
any(),
356-
any(),
357-
anyOrNull(),
358-
any(),
359-
any(),
360-
)
361-
verify(logger).handleInternalError(
362-
errorMessageCaptor.capture(),
363-
eq(null),
378+
verify(logger).logInternal(
379+
type = eq(LogType.INTERNALSDK),
380+
level = eq(LogLevel.ERROR),
381+
arrayFields = eq(ArrayFields.EMPTY),
382+
throwable = anyOrNull(),
383+
blocking = eq(false),
384+
message = logMessageCaptor.capture(),
364385
)
365-
assertThat(errorMessageCaptor.lastValue).isEqualTo(expectedMessage)
386+
assertThat(logMessageCaptor.firstValue()).isEqualTo(expectedMessage)
366387
}
367388
}

0 commit comments

Comments
 (0)