blob: 46d82a4d9cd22fd79810dfaae6468f39ca97bc34 [file]
package com.android.healthconnect.testapps.toolbox.viewmodels
import android.health.connect.HealthConnectManager
import android.health.connect.TimeInstantRangeFilter
import android.health.connect.TimeRangeFilter
import android.health.connect.datatypes.HeartRateRecord
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.healthconnect.testapps.toolbox.seed.SeedData
import com.android.healthconnect.testapps.toolbox.seed.SeedData.Companion.NUMBER_OF_SERIES_RECORDS_TO_INSERT
import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.ceil
import kotlin.math.min
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class PerformanceTestingViewModel : ViewModel() {
private val _performanceInsertedRecordsState =
MutableLiveData<PerformanceInsertedRecordsState>()
val performanceInsertedRecordsState: LiveData<PerformanceInsertedRecordsState>
get() = _performanceInsertedRecordsState
private val _performanceReadRecordsState = MutableLiveData<PerformanceReadRecordsState>()
val performanceReadRecordsState: LiveData<PerformanceReadRecordsState>
get() = _performanceReadRecordsState
fun beginInsertingData(seedDataInParallel: Boolean) {
_performanceInsertedRecordsState.postValue(
PerformanceInsertedRecordsState.BeginInserting(seedDataInParallel))
}
fun beginReadingData() {
_performanceReadRecordsState.postValue(PerformanceReadRecordsState.BeginReading)
}
fun insertRecordsForPerformanceTesting(
numberOfBatches: Long,
numberOfRecordsPerBatch: Long,
seedDataClass: SeedData,
) {
viewModelScope.launch {
try {
for (i in 1..numberOfBatches) {
withContext(Dispatchers.IO) {
seedDataClass.seedHeartRateData(numberOfRecordsPerBatch)
}
}
_performanceInsertedRecordsState.postValue(PerformanceInsertedRecordsState.Success)
} catch (ex: Exception) {
_performanceInsertedRecordsState.postValue(
PerformanceInsertedRecordsState.Error(ex.localizedMessage))
}
}
}
fun insertRecordsForPerformanceTestingOverASpanOfTime(
numberOfBatches: Long,
numberOfRecordsPerBatch: Long,
numberOfMinutes: Long,
seedDataClass: SeedData,
) {
viewModelScope.launch {
try {
val frequency =
scheduleTaskAtFixedRate(numberOfBatches, numberOfMinutes) {
insertBlocking(seedDataClass, numberOfRecordsPerBatch, 5)
}
_performanceInsertedRecordsState.postValue(
PerformanceInsertedRecordsState.InsertingOverSpanOfTime(frequency))
} catch (ex: Exception) {
_performanceInsertedRecordsState.postValue(
PerformanceInsertedRecordsState.Error(ex.localizedMessage))
}
}
}
fun insertRecordsForPerformanceTestingInParallel(
numberOfRecordsToInsert: Long,
seedDataClass: SeedData,
) {
var numOfCalls: Int =
ceil(numberOfRecordsToInsert.toDouble() / NUMBER_OF_SERIES_RECORDS_TO_INSERT).toInt()
viewModelScope.launch(Dispatchers.IO) {
val numberOfRecordsFailedToInsert = AtomicLong(0L)
val workerPool: ExecutorService = Executors.newFixedThreadPool(4)
while (numOfCalls-- > 0) {
workerPool.execute {
val recordsToInsertInIteration =
min(numberOfRecordsToInsert, NUMBER_OF_SERIES_RECORDS_TO_INSERT)
try {
insertBlocking(seedDataClass, recordsToInsertInIteration, 5)
} catch (ex: Exception) {
numberOfRecordsFailedToInsert.getAndAdd(recordsToInsertInIteration)
}
}
}
try {
workerPool.shutdown()
workerPool.awaitTermination(2, TimeUnit.MINUTES)
if (numberOfRecordsFailedToInsert.get() > 0L) {
_performanceInsertedRecordsState.postValue(
PerformanceInsertedRecordsState.Error(
"Failed to insert ${numberOfRecordsFailedToInsert.get()}"))
} else {
_performanceInsertedRecordsState.postValue(
PerformanceInsertedRecordsState.Success)
}
} catch (ex: Exception) {
_performanceInsertedRecordsState.postValue(
PerformanceInsertedRecordsState.Error(ex.localizedMessage))
}
}
}
private fun insertBlocking(
seedDataClass: SeedData,
numberOfRecordsToInsert: Long,
retryCount: Int,
) {
try {
runBlocking { seedDataClass.seedHeartRateData(numberOfRecordsToInsert) }
} catch (ex: Exception) {
if (retryCount != 0) {
insertBlocking(seedDataClass, numberOfRecordsToInsert, retryCount - 1)
}
}
}
fun readRecordsForPerformanceTesting(
numberOfBatches: Long,
numberOfRecordsPerBatch: Long,
manager: HealthConnectManager,
) {
val timeRangeFilter = getReadTimeRangeFilter()
viewModelScope.launch {
try {
for (i in 1..numberOfBatches) {
withContext(Dispatchers.IO) {
GeneralUtils.readRecords(
HeartRateRecord::class.java,
timeRangeFilter,
numberOfRecordsPerBatch,
manager)
}
}
_performanceReadRecordsState.postValue(PerformanceReadRecordsState.Success)
} catch (ex: Exception) {
_performanceReadRecordsState.postValue(
PerformanceReadRecordsState.Error(ex.localizedMessage))
}
}
}
fun readRecordsForPerformanceTestingOverASpanOfTime(
numberOfBatches: Long,
numberOfRecordsPerBatch: Long,
numberOfMinutes: Long,
manager: HealthConnectManager,
) {
val timeRangeFilter = getReadTimeRangeFilter()
viewModelScope.launch {
try {
val frequency =
scheduleTaskAtFixedRate(numberOfBatches, numberOfMinutes) {
runBlocking {
GeneralUtils.readRecords(
HeartRateRecord::class.java,
timeRangeFilter,
numberOfRecordsPerBatch,
manager)
}
}
_performanceReadRecordsState.postValue(
PerformanceReadRecordsState.ReadingOverSpanOfTime(frequency))
} catch (ex: Exception) {
_performanceReadRecordsState.postValue(
PerformanceReadRecordsState.Error(ex.localizedMessage))
}
}
}
private fun getReadTimeRangeFilter(): TimeRangeFilter {
val start = Instant.now().truncatedTo(ChronoUnit.DAYS)
val end = start.plus(Duration.ofHours(23)).plus(Duration.ofMinutes(59))
return TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build()
}
private fun scheduleTaskAtFixedRate(
numberOfBatches: Long,
numberOfMinutes: Long,
runnable: Runnable,
): Long {
val scheduler = Executors.newScheduledThreadPool(1)
val taskFrequency: Long = (numberOfMinutes * 60 * 1000) / numberOfBatches // In milliseconds
val handler =
scheduler.scheduleAtFixedRate(
runnable, taskFrequency, taskFrequency, TimeUnit.MILLISECONDS)
val canceller = Runnable { handler.cancel(false) }
scheduler.schedule(canceller, numberOfMinutes, TimeUnit.MINUTES)
return taskFrequency
}
sealed class PerformanceInsertedRecordsState {
data class Error(val errorMessage: String) : PerformanceInsertedRecordsState()
object Success : PerformanceInsertedRecordsState()
data class InsertingOverSpanOfTime(val timeDifferenceBetweenEachBatchInsert: Long) :
PerformanceInsertedRecordsState()
data class BeginInserting(val seedDataInParallel: Boolean) :
PerformanceInsertedRecordsState()
}
sealed class PerformanceReadRecordsState {
data class Error(val errorMessage: String) : PerformanceReadRecordsState()
object Success : PerformanceReadRecordsState()
object BeginReading : PerformanceReadRecordsState()
data class ReadingOverSpanOfTime(val timeDifferenceBetweenEachBatchInsert: Long) :
PerformanceReadRecordsState()
}
}