1 package com.android.healthconnect.testapps.toolbox.viewmodels 2 3 import android.health.connect.HealthConnectManager 4 import android.health.connect.TimeInstantRangeFilter 5 import android.health.connect.TimeRangeFilter 6 import android.health.connect.datatypes.HeartRateRecord 7 import androidx.lifecycle.LiveData 8 import androidx.lifecycle.MutableLiveData 9 import androidx.lifecycle.ViewModel 10 import androidx.lifecycle.viewModelScope 11 import com.android.healthconnect.testapps.toolbox.seed.SeedData 12 import com.android.healthconnect.testapps.toolbox.seed.SeedData.Companion.NUMBER_OF_SERIES_RECORDS_TO_INSERT 13 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils 14 import java.time.Duration 15 import java.time.Instant 16 import java.time.temporal.ChronoUnit 17 import java.util.concurrent.ExecutorService 18 import java.util.concurrent.Executors 19 import java.util.concurrent.TimeUnit 20 import java.util.concurrent.atomic.AtomicLong 21 import kotlin.math.ceil 22 import kotlin.math.min 23 import kotlinx.coroutines.Dispatchers 24 import kotlinx.coroutines.launch 25 import kotlinx.coroutines.runBlocking 26 import kotlinx.coroutines.withContext 27 28 class PerformanceTestingViewModel : ViewModel() { 29 30 private val _performanceInsertedRecordsState = 31 MutableLiveData<PerformanceInsertedRecordsState>() 32 val performanceInsertedRecordsState: LiveData<PerformanceInsertedRecordsState> 33 get() = _performanceInsertedRecordsState 34 35 private val _performanceReadRecordsState = MutableLiveData<PerformanceReadRecordsState>() 36 val performanceReadRecordsState: LiveData<PerformanceReadRecordsState> 37 get() = _performanceReadRecordsState 38 beginInsertingDatanull39 fun beginInsertingData(seedDataInParallel: Boolean) { 40 _performanceInsertedRecordsState.postValue( 41 PerformanceInsertedRecordsState.BeginInserting(seedDataInParallel)) 42 } 43 beginReadingDatanull44 fun beginReadingData() { 45 _performanceReadRecordsState.postValue(PerformanceReadRecordsState.BeginReading) 46 } 47 insertRecordsForPerformanceTestingnull48 fun insertRecordsForPerformanceTesting( 49 numberOfBatches: Long, 50 numberOfRecordsPerBatch: Long, 51 seedDataClass: SeedData, 52 ) { 53 viewModelScope.launch { 54 try { 55 for (i in 1..numberOfBatches) { 56 withContext(Dispatchers.IO) { 57 seedDataClass.seedHeartRateData(numberOfRecordsPerBatch) 58 } 59 } 60 _performanceInsertedRecordsState.postValue(PerformanceInsertedRecordsState.Success) 61 } catch (ex: Exception) { 62 _performanceInsertedRecordsState.postValue( 63 PerformanceInsertedRecordsState.Error(ex.localizedMessage)) 64 } 65 } 66 } 67 insertRecordsForPerformanceTestingOverASpanOfTimenull68 fun insertRecordsForPerformanceTestingOverASpanOfTime( 69 numberOfBatches: Long, 70 numberOfRecordsPerBatch: Long, 71 numberOfMinutes: Long, 72 seedDataClass: SeedData, 73 ) { 74 viewModelScope.launch { 75 try { 76 77 val frequency = 78 scheduleTaskAtFixedRate(numberOfBatches, numberOfMinutes) { 79 insertBlocking(seedDataClass, numberOfRecordsPerBatch, 5) 80 } 81 82 _performanceInsertedRecordsState.postValue( 83 PerformanceInsertedRecordsState.InsertingOverSpanOfTime(frequency)) 84 } catch (ex: Exception) { 85 _performanceInsertedRecordsState.postValue( 86 PerformanceInsertedRecordsState.Error(ex.localizedMessage)) 87 } 88 } 89 } 90 insertRecordsForPerformanceTestingInParallelnull91 fun insertRecordsForPerformanceTestingInParallel( 92 numberOfRecordsToInsert: Long, 93 seedDataClass: SeedData, 94 ) { 95 var numOfCalls: Int = 96 ceil(numberOfRecordsToInsert.toDouble() / NUMBER_OF_SERIES_RECORDS_TO_INSERT).toInt() 97 98 viewModelScope.launch(Dispatchers.IO) { 99 val numberOfRecordsFailedToInsert = AtomicLong(0L) 100 val workerPool: ExecutorService = Executors.newFixedThreadPool(4) 101 102 while (numOfCalls-- > 0) { 103 workerPool.execute { 104 val recordsToInsertInIteration = 105 min(numberOfRecordsToInsert, NUMBER_OF_SERIES_RECORDS_TO_INSERT) 106 try { 107 insertBlocking(seedDataClass, recordsToInsertInIteration, 5) 108 } catch (ex: Exception) { 109 numberOfRecordsFailedToInsert.getAndAdd(recordsToInsertInIteration) 110 } 111 } 112 } 113 114 try { 115 workerPool.shutdown() 116 workerPool.awaitTermination(2, TimeUnit.MINUTES) 117 if (numberOfRecordsFailedToInsert.get() > 0L) { 118 _performanceInsertedRecordsState.postValue( 119 PerformanceInsertedRecordsState.Error( 120 "Failed to insert ${numberOfRecordsFailedToInsert.get()}")) 121 } else { 122 _performanceInsertedRecordsState.postValue( 123 PerformanceInsertedRecordsState.Success) 124 } 125 } catch (ex: Exception) { 126 _performanceInsertedRecordsState.postValue( 127 PerformanceInsertedRecordsState.Error(ex.localizedMessage)) 128 } 129 } 130 } 131 insertBlockingnull132 private fun insertBlocking( 133 seedDataClass: SeedData, 134 numberOfRecordsToInsert: Long, 135 retryCount: Int, 136 ) { 137 try { 138 runBlocking { seedDataClass.seedHeartRateData(numberOfRecordsToInsert) } 139 } catch (ex: Exception) { 140 if (retryCount != 0) { 141 insertBlocking(seedDataClass, numberOfRecordsToInsert, retryCount - 1) 142 } 143 } 144 } 145 readRecordsForPerformanceTestingnull146 fun readRecordsForPerformanceTesting( 147 numberOfBatches: Long, 148 numberOfRecordsPerBatch: Long, 149 manager: HealthConnectManager, 150 ) { 151 val timeRangeFilter = getReadTimeRangeFilter() 152 153 viewModelScope.launch { 154 try { 155 for (i in 1..numberOfBatches) { 156 withContext(Dispatchers.IO) { 157 GeneralUtils.readRecords( 158 HeartRateRecord::class.java, 159 timeRangeFilter, 160 numberOfRecordsPerBatch, 161 manager) 162 } 163 } 164 165 _performanceReadRecordsState.postValue(PerformanceReadRecordsState.Success) 166 } catch (ex: Exception) { 167 _performanceReadRecordsState.postValue( 168 PerformanceReadRecordsState.Error(ex.localizedMessage)) 169 } 170 } 171 } 172 readRecordsForPerformanceTestingOverASpanOfTimenull173 fun readRecordsForPerformanceTestingOverASpanOfTime( 174 numberOfBatches: Long, 175 numberOfRecordsPerBatch: Long, 176 numberOfMinutes: Long, 177 manager: HealthConnectManager, 178 ) { 179 val timeRangeFilter = getReadTimeRangeFilter() 180 181 viewModelScope.launch { 182 try { 183 val frequency = 184 scheduleTaskAtFixedRate(numberOfBatches, numberOfMinutes) { 185 runBlocking { 186 GeneralUtils.readRecords( 187 HeartRateRecord::class.java, 188 timeRangeFilter, 189 numberOfRecordsPerBatch, 190 manager) 191 } 192 } 193 _performanceReadRecordsState.postValue( 194 PerformanceReadRecordsState.ReadingOverSpanOfTime(frequency)) 195 } catch (ex: Exception) { 196 _performanceReadRecordsState.postValue( 197 PerformanceReadRecordsState.Error(ex.localizedMessage)) 198 } 199 } 200 } 201 getReadTimeRangeFilternull202 private fun getReadTimeRangeFilter(): TimeRangeFilter { 203 val start = Instant.now().truncatedTo(ChronoUnit.DAYS) 204 val end = start.plus(Duration.ofHours(23)).plus(Duration.ofMinutes(59)) 205 return TimeInstantRangeFilter.Builder().setStartTime(start).setEndTime(end).build() 206 } 207 scheduleTaskAtFixedRatenull208 private fun scheduleTaskAtFixedRate( 209 numberOfBatches: Long, 210 numberOfMinutes: Long, 211 runnable: Runnable, 212 ): Long { 213 val scheduler = Executors.newScheduledThreadPool(1) 214 val taskFrequency: Long = (numberOfMinutes * 60 * 1000) / numberOfBatches // In milliseconds 215 216 val handler = 217 scheduler.scheduleAtFixedRate( 218 runnable, taskFrequency, taskFrequency, TimeUnit.MILLISECONDS) 219 val canceller = Runnable { handler.cancel(false) } 220 scheduler.schedule(canceller, numberOfMinutes, TimeUnit.MINUTES) 221 return taskFrequency 222 } 223 224 sealed class PerformanceInsertedRecordsState { 225 data class Error(val errorMessage: String) : PerformanceInsertedRecordsState() 226 object Success : PerformanceInsertedRecordsState() 227 data class InsertingOverSpanOfTime(val timeDifferenceBetweenEachBatchInsert: Long) : 228 PerformanceInsertedRecordsState() 229 230 data class BeginInserting(val seedDataInParallel: Boolean) : 231 PerformanceInsertedRecordsState() 232 } 233 234 sealed class PerformanceReadRecordsState { 235 data class Error(val errorMessage: String) : PerformanceReadRecordsState() 236 object Success : PerformanceReadRecordsState() 237 object BeginReading : PerformanceReadRecordsState() 238 data class ReadingOverSpanOfTime(val timeDifferenceBetweenEachBatchInsert: Long) : 239 PerformanceReadRecordsState() 240 } 241 } 242