-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
19cc978
commit d88f961
Showing
4 changed files
with
236 additions
and
1 deletion.
There are no files selected for viewing
226 changes: 226 additions & 0 deletions
226
...d-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/features/AnrFeatureTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
package io.embrace.android.embracesdk.features | ||
|
||
import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
import io.embrace.android.embracesdk.IntegrationTestRule | ||
import io.embrace.android.embracesdk.anr.detection.BlockedThreadDetector | ||
import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService | ||
import io.embrace.android.embracesdk.fakes.FakeClock | ||
import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule | ||
import io.embrace.android.embracesdk.fakes.injection.FakeInitModule | ||
import io.embrace.android.embracesdk.fakes.injection.FakeWorkerThreadModule | ||
import io.embrace.android.embracesdk.injection.AnrModuleImpl | ||
import io.embrace.android.embracesdk.internal.clock.nanosToMillis | ||
import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData | ||
import io.embrace.android.embracesdk.payload.SessionMessage | ||
import io.embrace.android.embracesdk.recordSession | ||
import io.embrace.android.embracesdk.worker.WorkerName | ||
import java.util.concurrent.atomic.AtomicReference | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Rule | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
|
||
private const val START_TIME_MS = 10000000000L | ||
private const val INTERVAL_MS = 100L | ||
private const val ANR_THRESHOLD_MS = 1000L | ||
private const val MAX_SAMPLE_COUNT = 80 | ||
private const val MAX_INTERVAL_COUNT = 5 | ||
private const val SPAN_NAME = "emb-thread-blockage" | ||
|
||
@RunWith(AndroidJUnit4::class) | ||
internal class AnrFeatureTest { | ||
|
||
private lateinit var anrMonitorExecutor: BlockingScheduledExecutorService | ||
private lateinit var blockedThreadDetector: BlockedThreadDetector | ||
|
||
@Rule | ||
@JvmField | ||
val testRule: IntegrationTestRule = IntegrationTestRule( | ||
harnessSupplier = { | ||
val clock = FakeClock(currentTime = START_TIME_MS) | ||
val initModule = FakeInitModule(clock) | ||
val workerThreadModule = | ||
FakeWorkerThreadModule(initModule, WorkerName.ANR_MONITOR).apply { | ||
anrMonitorThread = AtomicReference(Thread.currentThread()) | ||
} | ||
anrMonitorExecutor = workerThreadModule.executor | ||
val anrModule = AnrModuleImpl( | ||
initModule, | ||
FakeEssentialServiceModule(), | ||
workerThreadModule | ||
) | ||
blockedThreadDetector = anrModule.blockedThreadDetector | ||
|
||
IntegrationTestRule.Harness( | ||
currentTimeMs = START_TIME_MS, | ||
overriddenClock = clock, | ||
overriddenInitModule = initModule, | ||
overriddenWorkerThreadModule = workerThreadModule, | ||
fakeAnrModule = anrModule | ||
) | ||
} | ||
) | ||
|
||
@Test | ||
fun `trigger ANRs`() { | ||
val firstSampleCount = 20 | ||
val secondSampleCount = 10 | ||
|
||
with(testRule.harness) { | ||
var secondAnrStartTime: Long? = null | ||
val message = recordSession { | ||
triggerAnr(firstSampleCount) | ||
secondAnrStartTime = overriddenClock.now() | ||
triggerAnr(secondSampleCount) | ||
} | ||
checkNotNull(message) | ||
|
||
// assert ANRs received | ||
val spans = message.findAnrSpans() | ||
assertEquals(2, spans.size) | ||
assertAnrReceived(spans[0], START_TIME_MS, firstSampleCount) | ||
assertAnrReceived(spans[1], checkNotNull(secondAnrStartTime), secondSampleCount) | ||
} | ||
} | ||
|
||
@Test | ||
fun `exceed max samples for one interval`() { | ||
val sampleCount = 100 | ||
|
||
with(testRule.harness) { | ||
val message = recordSession { | ||
triggerAnr(sampleCount) | ||
} | ||
checkNotNull(message) | ||
|
||
// assert ANRs received | ||
val spans = message.findAnrSpans() | ||
val span = spans.single() | ||
assertAnrReceived(span, START_TIME_MS, sampleCount) | ||
} | ||
} | ||
|
||
@Test | ||
fun `exceed max intervals for one session`() { | ||
val initialSamples = 10 | ||
val extraSamples = 5 | ||
val intervalCount = 8 | ||
val startTimes = mutableListOf<Long>() | ||
|
||
with(testRule.harness) { | ||
val message = recordSession { | ||
repeat(intervalCount) { index -> | ||
startTimes.add(overriddenClock.now()) | ||
triggerAnr(initialSamples + (index * extraSamples)) | ||
} | ||
} | ||
checkNotNull(message) | ||
|
||
// assert ANRs received | ||
val spans = message.findAnrSpans() | ||
assertEquals(intervalCount, spans.size) | ||
|
||
repeat(intervalCount) { index -> | ||
val span = spans[index] | ||
val expectedSamples = initialSamples + (index * extraSamples) | ||
|
||
// older intervals get dropped because they have fewer samples. | ||
val intervalCode = when { | ||
index < intervalCount - MAX_INTERVAL_COUNT -> "1" | ||
else -> "0" | ||
} | ||
assertAnrReceived(span, startTimes[index], expectedSamples, intervalCode) | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
fun `in progress ANR added to payload`() { | ||
val sampleCount = 10 | ||
|
||
with(testRule.harness) { | ||
val message = recordSession { | ||
triggerAnr(sampleCount, incomplete = true) | ||
} | ||
checkNotNull(message) | ||
|
||
// assert ANRs received | ||
val spans = message.findAnrSpans() | ||
val span = spans.single() | ||
assertAnrReceived(span, START_TIME_MS, sampleCount, endTime = 0) | ||
} | ||
} | ||
|
||
private fun assertAnrReceived( | ||
span: EmbraceSpanData, | ||
startTime: Long, | ||
sampleCount: Int, | ||
expectedIntervalCode: String = "0", | ||
endTime: Long = startTime + ANR_THRESHOLD_MS + (sampleCount * INTERVAL_MS) | ||
) { | ||
// assert span start/end times | ||
assertEquals(startTime, span.startTimeNanos.nanosToMillis()) | ||
assertEquals(endTime, span.endTimeNanos.nanosToMillis()) | ||
|
||
// assert span attributes | ||
assertEquals(expectedIntervalCode, span.attributes["interval_code"]) | ||
assertEquals("perf.thread_blockage", span.attributes["emb.type"]) | ||
|
||
val events = span.events | ||
|
||
events.forEachIndexed { index, event -> | ||
assertEquals("perf.thread_blockage_sample", event.name) | ||
|
||
// assert attributes | ||
val attrs = event.attributes | ||
assertEquals("perf.thread_blockage_sample", attrs["emb.type"]) | ||
assertEquals("0", attrs["sample_overhead"]) | ||
|
||
val expectedCode = when { | ||
index < MAX_SAMPLE_COUNT -> "0" | ||
else -> "1" | ||
} | ||
assertEquals(expectedCode, attrs["sample_code"]) | ||
|
||
// assert interval time | ||
val expectedTime = startTime + ANR_THRESHOLD_MS + ((index + 1) * INTERVAL_MS) | ||
assertEquals(expectedTime, event.timestampNanos.nanosToMillis()) | ||
} | ||
} | ||
|
||
/** | ||
* Triggers an ANR by simulating the main thread getting blocked & unblocked. Time is controlled | ||
* with a fake Clock instance & a blockable executor that runs the blockage checks. | ||
*/ | ||
private fun IntegrationTestRule.Harness.triggerAnr( | ||
sampleCount: Int, | ||
intervalMs: Long = INTERVAL_MS, | ||
incomplete: Boolean = false | ||
) { | ||
with(anrMonitorExecutor) { | ||
blockingMode = true | ||
|
||
// increase time by initial delay, and kick off check that marks thread as blocked | ||
moveForwardAndRunBlocked(0) | ||
moveForwardAndRunBlocked(ANR_THRESHOLD_MS) | ||
|
||
// run the thread block check & create a sample | ||
repeat(sampleCount) { | ||
moveForwardAndRunBlocked(intervalMs) | ||
} | ||
|
||
if (!incomplete) { | ||
// simulate the main thread becoming responsive again, ending the ANR interval | ||
blockedThreadDetector.onTargetThreadResponse(overriddenClock.now()) | ||
} | ||
|
||
// AnrService#getCapturedData() currently gets a Callable with a timeout, so we | ||
// need to allow all jobs on the executor to finish running for spans to be | ||
// included in the payload. | ||
blockingMode = false | ||
runCurrentlyBlocked() | ||
} | ||
} | ||
|
||
private fun SessionMessage.findAnrSpans() = checkNotNull(spans?.filter { it.name == SPAN_NAME }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters