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 object NativeThreadBlockage : Performance("native_thread_blockage")

internal object NativeThreadBlockageSample : Performance("native_thread_blockage_sample")

internal object ThermalState : Performance("thermal_state")

Check warning on line 27 in embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/EmbType.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/EmbType.kt#L27

Added line #L27 was not covered by tests
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,4 +313,15 @@
"emb.webview_info.tag" to tag
).toNonNullMap()
}

internal class ThermalState(

Check warning on line 317 in embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt#L317

Added line #L317 was not covered by tests
status: Int
) : SchemaType(
telemetryType = EmbType.Performance.ThermalState,
fixedObjectName = "thermal-state"

Check warning on line 321 in embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt#L319-L321

Added lines #L319 - L321 were not covered by tests
) {
override val schemaAttributes = mapOf(
"status" to status.toString()

Check warning on line 324 in embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt#L323-L324

Added lines #L323 - L324 were not covered by tests
)
}
}
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.InternalEmbraceLogger
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: InternalEmbraceLogger,
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) {

Check warning on line 68 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#L68

Added line #L68 was not covered by tests
thermalStatusListener?.let {
powerManager?.removeThermalStatusListener(it)
thermalStatusListener = null

Check warning on line 71 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#L71

Added line #L71 was not covered by tests
}
}
}

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

val timestamp = clock.now()

Check warning on line 81 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#L81

Added line #L81 was not covered by tests

// close previous span
if (span != null) {
captureSpanData(
countsTowardsLimits = false,
inputValidation = NoInputValidation,
captureAction = {

Check warning on line 88 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#L85-L88

Added lines #L85 - L88 were not covered by tests
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

Check warning on line 100 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#L94-L100

Added lines #L94 - L100 were not covered by tests
}
}
}

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

Check warning on line 108 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#L106-L108

Added lines #L106 - L108 were not covered by tests
)
}
}

internal data class ThermalState(
val status: Int,
val timestamp: Long

Check warning on line 115 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#L113-L115

Added lines #L113 - L115 were not covered by tests
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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 @@ -48,6 +49,7 @@ internal interface DataSourceModule {
val memoryWarningDataSource: DataSourceState<MemoryWarningDataSource>
val networkStatusDataSource: DataSourceState<NetworkStatusDataSource>
val sigquitDataSource: DataSourceState<SigquitDataSource>
val thermalStateDataSource: DataSourceState<ThermalStateDataSource>?
}

internal class DataSourceModuleImpl(
Expand Down Expand Up @@ -211,6 +213,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.InternalEmbraceLogger
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,
InternalEmbraceLogger(),
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 @@ -40,6 +40,7 @@ internal class DataSourceModuleImplTest {
assertNotNull(module.memoryWarningDataSource)
assertNotNull(module.networkStatusDataSource)
assertNotNull(module.sigquitDataSource)
assertEquals(11, module.getDataSources().size)
assertNotNull(module.thermalStateDataSource)
assertEquals(12, module.getDataSources().size)
}
}