Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate thermal states to OTel #821

Merged
merged 11 commits into from
May 13, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package io.embrace.android.embracesdk.features

import android.os.Build
import android.os.PowerManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.embrace.android.embracesdk.IntegrationTestRule
import io.embrace.android.embracesdk.arch.schema.EmbType
import io.embrace.android.embracesdk.config.remote.DataRemoteConfig
import io.embrace.android.embracesdk.config.remote.RemoteConfig
import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior
import io.embrace.android.embracesdk.fakes.fakeSdkModeBehavior
import io.embrace.android.embracesdk.findSpanAttribute
import io.embrace.android.embracesdk.findSpanSnapshotsOfType
import io.embrace.android.embracesdk.findSpansOfType
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
import io.embrace.android.embracesdk.recordSession
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowActivityManager

@Config(sdk = [Build.VERSION_CODES.Q], shadows = [ShadowActivityManager::class])
@RunWith(AndroidJUnit4::class)
internal class ThermalStateFeatureTest {

@Rule
@JvmField
val testRule: IntegrationTestRule = IntegrationTestRule()

@Before
fun setUp() {
setUpThermalCapture()
}

@Test
fun `single thermal state change generates a snapshot`() {
with(testRule) {
var startTimeMs = 0L
val message = checkNotNull(harness.recordSession {
startTimeMs = harness.overriddenClock.now()

val dataSource =
checkNotNull(bootstrapper.dataSourceModule.thermalStateDataSource?.dataSource)
dataSource.handleThermalStateChange(PowerManager.THERMAL_STATUS_NONE)
})

val snapshots = message.findSpanSnapshotsOfType(EmbType.Performance.ThermalState)
assertEquals(1, snapshots.size)
val snapshot = snapshots.single()

assertEquals("emb-thermal-state", snapshot.name)
assertEquals("perf.thermal_state", snapshot.findSpanAttribute("emb.type"))
assertEquals(PowerManager.THERMAL_STATUS_NONE.toString(), snapshot.findSpanAttribute("status"))
assertEquals(startTimeMs, snapshot.startTimeNanos.nanosToMillis())
}
}

@Test
fun `multiple thermal state changes generate spans`() {
val tickTimeMs = 3000L
with(testRule) {
var startTimeMs = 0L
val message = checkNotNull(harness.recordSession {
startTimeMs = harness.overriddenClock.now()

val dataSource =
checkNotNull(bootstrapper.dataSourceModule.thermalStateDataSource?.dataSource)
dataSource.handleThermalStateChange(PowerManager.THERMAL_STATUS_CRITICAL)
harness.overriddenClock.tick(tickTimeMs)
dataSource.handleThermalStateChange(PowerManager.THERMAL_STATUS_MODERATE)
harness.overriddenClock.tick(tickTimeMs)
dataSource.handleThermalStateChange(PowerManager.THERMAL_STATUS_NONE)
})

val spans = message.findSpansOfType(EmbType.Performance.ThermalState)
assertEquals(2, spans.size)

spans.forEach {
assertEquals("emb-thermal-state", it.name)
assertEquals("perf.thermal_state", it.findSpanAttribute("emb.type"))
}
val firstSpan = spans.first()
assertEquals(PowerManager.THERMAL_STATUS_CRITICAL.toString(), firstSpan.findSpanAttribute("status"))
assertEquals(startTimeMs, firstSpan.startTimeNanos.nanosToMillis())
assertEquals(startTimeMs + tickTimeMs, firstSpan.endTimeNanos.nanosToMillis())
val secondSpan = spans.last()
assertEquals(PowerManager.THERMAL_STATUS_MODERATE.toString(), secondSpan.findSpanAttribute("status"))
assertEquals(startTimeMs + tickTimeMs, secondSpan.startTimeNanos.nanosToMillis())
assertEquals(startTimeMs + tickTimeMs * 2, secondSpan.endTimeNanos.nanosToMillis())

val snapshots = message.findSpanSnapshotsOfType(EmbType.Performance.ThermalState)
assertEquals(1, snapshots.size)

val snapshot = snapshots.single()
assertEquals("emb-thermal-state", snapshot.name)
assertEquals("perf.thermal_state", snapshot.findSpanAttribute("emb.type"))
assertEquals(PowerManager.THERMAL_STATUS_NONE.toString(), snapshot.findSpanAttribute("status"))
assertEquals(startTimeMs + tickTimeMs * 2, snapshot.startTimeNanos.nanosToMillis())
}
}

private fun setUpThermalCapture() {
testRule.harness.overriddenConfigService.autoDataCaptureBehavior =
fakeAutoDataCaptureBehavior(
remoteCfg = {
RemoteConfig(
dataConfig = DataRemoteConfig(pctThermalStatusEnabled = 100.0f)
)
}
)
testRule.harness.overriddenConfigService.sdkModeBehavior =
fakeSdkModeBehavior(
remoteCfg = {
RemoteConfig(
pctBetaFeaturesEnabled = 100.0f
)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal sealed class EmbType(type: String, subtype: String?) : TelemetryType {
internal object NativeThreadBlockage : Performance("native_thread_blockage")

internal object NativeThreadBlockageSample : Performance("native_thread_blockage_sample")

internal object ThermalState : Performance("thermal_state")
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,15 @@ internal sealed class SchemaType(
)
.toNonNullMap()
}

internal class ThermalState(
status: Int
) : SchemaType(
telemetryType = EmbType.Performance.ThermalState,
fixedObjectName = "thermal-state"
) {
override val schemaAttributes = mapOf(
"status" to status.toString()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package io.embrace.android.embracesdk.capture.thermalstate

import android.os.Build
import android.os.PowerManager
import androidx.annotation.RequiresApi
import io.embrace.android.embracesdk.arch.datasource.NoInputValidation
import io.embrace.android.embracesdk.arch.datasource.SpanDataSourceImpl
import io.embrace.android.embracesdk.arch.datasource.startSpanCapture
import io.embrace.android.embracesdk.arch.destination.StartSpanData
import io.embrace.android.embracesdk.arch.destination.StartSpanMapper
import io.embrace.android.embracesdk.arch.limits.UpToLimitStrategy
import io.embrace.android.embracesdk.arch.schema.SchemaType
import io.embrace.android.embracesdk.internal.Systrace
import io.embrace.android.embracesdk.internal.clock.Clock
import io.embrace.android.embracesdk.internal.spans.SpanService
import io.embrace.android.embracesdk.internal.utils.Provider
import io.embrace.android.embracesdk.logging.EmbLogger
import io.embrace.android.embracesdk.spans.EmbraceSpan
import io.embrace.android.embracesdk.worker.BackgroundWorker
import io.embrace.android.embracesdk.worker.TaskPriority
import java.util.concurrent.Executor

@RequiresApi(Build.VERSION_CODES.Q)
internal class ThermalStateDataSource(
spanService: SpanService,
logger: EmbLogger,
private val backgroundWorker: BackgroundWorker,
private val clock: Clock,
powerManagerProvider: Provider<PowerManager?>
) : StartSpanMapper<ThermalState>, SpanDataSourceImpl(
destination = spanService,
logger = logger,
limitStrategy = UpToLimitStrategy(logger) { MAX_CAPTURED_THERMAL_STATES }
) {
private companion object {
private const val MAX_CAPTURED_THERMAL_STATES = 100
}

private var thermalStatusListener: PowerManager.OnThermalStatusChangedListener? = null

private val powerManager: PowerManager? by lazy(powerManagerProvider)

private var span: EmbraceSpan? = null

override fun enableDataCapture() {
backgroundWorker.submit(TaskPriority.LOW) {
Systrace.traceSynchronous("thermal-service-registration") {
thermalStatusListener = PowerManager.OnThermalStatusChangedListener {
handleThermalStateChange(it)

Check warning on line 49 in embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStateDataSource.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStateDataSource.kt#L49

Added line #L49 was not covered by tests
}
val pm = powerManager
if (pm != null) {
// Android API only accepts an executor. We don't want to directly expose those
// to everything in the codebase so we decorate the BackgroundWorker here as an
// alternative
val executor = Executor {
backgroundWorker.submit(runnable = it)

Check warning on line 57 in embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStateDataSource.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStateDataSource.kt#L57

Added line #L57 was not covered by tests
}
thermalStatusListener?.let {
pm.addThermalStatusListener(executor, it)
}
}
}
}
}

override fun disableDataCapture() {
backgroundWorker.submit(TaskPriority.LOW) {
thermalStatusListener?.let {
powerManager?.removeThermalStatusListener(it)
thermalStatusListener = null
}
}
}

fun handleThermalStateChange(status: Int?) {
if (status == null) {
return
}

val timestamp = clock.now()

// close previous span
if (span != null) {
captureSpanData(
countsTowardsLimits = false,
inputValidation = NoInputValidation,
captureAction = {
span?.stop(endTimeMs = timestamp)
}
)
}
// start a new span with the new thermal state
captureSpanData(
countsTowardsLimits = true,
inputValidation = NoInputValidation
) {
startSpanCapture(ThermalState(status, timestamp), ::toStartSpanData)
.apply {
span = this
}
}
}

override fun toStartSpanData(obj: ThermalState): StartSpanData {
return StartSpanData(
schemaType = SchemaType.ThermalState(obj.status),
spanStartTimeMs = obj.timestamp
)
}
}

internal data class ThermalState(
val status: Int,
val timestamp: Long
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import io.embrace.android.embracesdk.capture.crumbs.WebViewUrlDataSource
import io.embrace.android.embracesdk.capture.memory.MemoryWarningDataSource
import io.embrace.android.embracesdk.capture.powersave.LowPowerDataSource
import io.embrace.android.embracesdk.capture.session.SessionPropertiesDataSource
import io.embrace.android.embracesdk.capture.thermalstate.ThermalStateDataSource
import io.embrace.android.embracesdk.internal.utils.BuildVersionChecker
import io.embrace.android.embracesdk.internal.utils.Provider
import io.embrace.android.embracesdk.worker.WorkerName
Expand Down Expand Up @@ -50,6 +51,7 @@ internal interface DataSourceModule {
val networkStatusDataSource: DataSourceState<NetworkStatusDataSource>
val sigquitDataSource: DataSourceState<SigquitDataSource>
val rnActionDataSource: DataSourceState<RnActionDataSource>
val thermalStateDataSource: DataSourceState<ThermalStateDataSource>?
}

internal class DataSourceModuleImpl(
Expand Down Expand Up @@ -225,6 +227,30 @@ internal class DataSourceModuleImpl(
)
}

override val thermalStateDataSource: DataSourceState<ThermalStateDataSource>? by dataSourceState {
DataSourceState(
factory = { thermalService },
configGate = {
configService.autoDataCaptureBehavior.isThermalStatusCaptureEnabled() &&
configService.sdkModeBehavior.isBetaFeaturesEnabled()
}
)
}

private val thermalService: ThermalStateDataSource? by singleton {
if (BuildVersionChecker.isAtLeast(Build.VERSION_CODES.Q)) {
ThermalStateDataSource(
spanService = otelModule.spanService,
logger = initModule.logger,
backgroundWorker = workerThreadModule.backgroundWorker(WorkerName.BACKGROUND_REGISTRATION),
clock = initModule.clock,
powerManagerProvider = { systemServiceModule.powerManager }
)
} else {
null
}
}

private val configService = essentialServiceModule.configService

override fun getDataSources(): List<DataSourceState<*>> = values
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.embrace.android.embracesdk

import android.os.PowerManager
import io.embrace.android.embracesdk.arch.schema.EmbType
import io.embrace.android.embracesdk.capture.thermalstate.ThermalStateDataSource
import io.embrace.android.embracesdk.concurrency.BlockableExecutorService
import io.embrace.android.embracesdk.fakes.FakeClock
import io.embrace.android.embracesdk.fakes.FakeSpanService
import io.embrace.android.embracesdk.fakes.system.mockPowerManager
import io.embrace.android.embracesdk.logging.EmbLoggerImpl
import io.embrace.android.embracesdk.worker.BackgroundWorker
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

internal class ThermalStateDataSourceTest {

private lateinit var dataSource: ThermalStateDataSource
private lateinit var spanWriter: FakeSpanService
private val mockPowerManager = mockPowerManager()

@Before
fun setUp() {
spanWriter = FakeSpanService()
dataSource = ThermalStateDataSource(
spanWriter,
EmbLoggerImpl(),
BackgroundWorker(BlockableExecutorService()),
FakeClock(100),
) { mockPowerManager }
}

@Test
fun onThermalStateChanged() {
with(dataSource) {
handleThermalStateChange(PowerManager.THERMAL_STATUS_NONE)
handleThermalStateChange(PowerManager.THERMAL_STATUS_SEVERE)
handleThermalStateChange(PowerManager.THERMAL_STATUS_CRITICAL)
}
assertEquals(3, spanWriter.createdSpans.size)
spanWriter.createdSpans.forEach {
assertEquals(EmbType.Performance.ThermalState, it.type)
}
assertEquals(PowerManager.THERMAL_STATUS_NONE, spanWriter.createdSpans[0].attributes["status"]?.toInt())
assertEquals(PowerManager.THERMAL_STATUS_SEVERE, spanWriter.createdSpans[1].attributes["status"]?.toInt())
assertEquals(PowerManager.THERMAL_STATUS_CRITICAL, spanWriter.createdSpans[2].attributes["status"]?.toInt())
}

@Test
fun onLimitExceeded() {
repeat(250) {
dataSource.handleThermalStateChange(PowerManager.THERMAL_STATUS_SEVERE)
}

assertEquals(100, spanWriter.createdSpans.size)
}

@Test
fun onEnableAndDisable() {
verify(exactly = 0) { mockPowerManager.addThermalStatusListener(any(), any()) }
dataSource.enableDataCapture()
verify(exactly = 1) { mockPowerManager.addThermalStatusListener(any(), any()) }
dataSource.disableDataCapture()
verify(exactly = 1) { mockPowerManager.removeThermalStatusListener(any()) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal class DataSourceModuleImplTest {
assertNotNull(module.networkStatusDataSource)
assertNotNull(module.sigquitDataSource)
assertNotNull(module.rnActionDataSource)
assertEquals(12, module.getDataSources().size)
assertNotNull(module.thermalStateDataSource)
assertEquals(13, module.getDataSources().size)
}
}