-
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.
Migrate thermal states to OTel (#821)
- Loading branch information
1 parent
3ddac16
commit d2b2093
Showing
7 changed files
with
347 additions
and
1 deletion.
There are no files selected for viewing
123 changes: 123 additions & 0 deletions
123
...sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/features/ThermalStateTest.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,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 | ||
) | ||
} | ||
) | ||
} | ||
} |
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
116 changes: 116 additions & 0 deletions
116
...rc/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStateDataSource.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,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 | ||
) |
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
67 changes: 67 additions & 0 deletions
67
...ace-android-sdk/src/test/java/io/embrace/android/embracesdk/ThermalStateDataSourceTest.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,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()) } | ||
} | ||
} |
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