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