Skip to content

Commit

Permalink
Migrate thermal states to OTel (#821)
Browse files Browse the repository at this point in the history
  • Loading branch information
leandro-godon committed May 13, 2024
1 parent 3ddac16 commit d2b2093
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 1 deletion.
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)
}
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)
}
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)
}
}

0 comments on commit d2b2093

Please sign in to comment.