Skip to content

Commit

Permalink
test: add feature test for anrs
Browse files Browse the repository at this point in the history
  • Loading branch information
fractalwrench committed Apr 25, 2024
1 parent 19cc978 commit d88f961
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 1 deletion.
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 })
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ internal class AnrModuleImpl(
)
}

private val blockedThreadDetector by singleton {
val blockedThreadDetector by singleton {
BlockedThreadDetector(
configService = configService,
clock = initModule.clock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ internal class BlockingScheduledExecutorService(
val scheduledTasks = PriorityBlockingQueue(10, BlockedScheduledFutureTaskComparator())
private val delegateExecutorService = BlockableExecutorService(blockingMode = blockingMode)

var blockingMode: Boolean
get() = delegateExecutorService.blockingMode
set(value) {
delegateExecutorService.blockingMode = value
}

/**
* Run all tasks due to run at the current time and return when all the tasks have finished running. This does not include tasks
* submitted during the running of these tasks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.embrace.android.embracesdk.worker.ScheduledWorker
import io.embrace.android.embracesdk.worker.WorkerName
import io.embrace.android.embracesdk.worker.WorkerThreadModule
import io.embrace.android.embracesdk.worker.WorkerThreadModuleImpl
import java.util.concurrent.atomic.AtomicReference

internal class FakeWorkerThreadModule(
fakeInitModule: FakeInitModule = FakeInitModule(),
Expand All @@ -33,4 +34,6 @@ internal class FakeWorkerThreadModule(
else -> base.scheduledWorker(workerName)
}
}

override var anrMonitorThread: AtomicReference<Thread> = base.anrMonitorThread
}

0 comments on commit d88f961

Please sign in to comment.