Skip to content

Commit becc656

Browse files
Added shared prefs edit benchmark (commit vs apply)
1 parent bf3823e commit becc656

13 files changed

Lines changed: 502 additions & 0 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.techyourchance.android.benchmarks.shared_prefs_write
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import com.techyourchance.android.common.datetime.DateTimeProvider
6+
import com.techyourchance.android.common.maths.LinearFitCalculator
7+
import kotlinx.coroutines.asCoroutineDispatcher
8+
import kotlinx.coroutines.withContext
9+
import java.util.concurrent.Executors
10+
import javax.inject.Inject
11+
12+
class SharedPrefsWriteBenchmarkUseCase @Inject constructor(
13+
private val context: Context,
14+
private val dateTimeProvider: DateTimeProvider,
15+
private val linearFitCalculator: LinearFitCalculator,
16+
) {
17+
18+
data class Result(
19+
val resultWithCommit: SharedPrefsWriteResult,
20+
val resultWithApply: SharedPrefsWriteResult,
21+
)
22+
23+
private val coroutinesDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
24+
25+
suspend fun runBenchmark(valueToWrite: String): Result {
26+
return withContext(coroutinesDispatcher) {
27+
28+
val sharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
29+
30+
val commitRawTimingsNano = runBenchmarkWithWriteFunction(sharedPrefs, valueToWrite) {
31+
it.commit()
32+
}
33+
34+
val applyRawTimingsNano = runBenchmarkWithWriteFunction(sharedPrefs, valueToWrite) {
35+
it.apply()
36+
}
37+
38+
val resultWithCommit = computeResult(commitRawTimingsNano)
39+
40+
val resultWithApply = computeResult(applyRawTimingsNano)
41+
42+
Result(resultWithCommit, resultWithApply)
43+
}
44+
}
45+
46+
private fun runBenchmarkWithWriteFunction(
47+
sharedPrefs: SharedPreferences,
48+
sharedPrefValue: String,
49+
editorWriteFunction: (SharedPreferences.Editor) -> Unit
50+
): MutableMap<Int, MutableMap<Int, Long>> {
51+
52+
var sharedPrefsEditor: SharedPreferences.Editor? = null
53+
var sharedPrefKey = ""
54+
var startNano = 0L
55+
var endNano = 0L
56+
57+
val rawTimingsNano = mutableMapOf<Int, MutableMap<Int, Long>>()
58+
59+
for (benchmarkIteration in 0 until NUM_ITERATIONS) {
60+
sharedPrefs.edit().clear().commit()
61+
62+
rawTimingsNano[benchmarkIteration] = mutableMapOf()
63+
for (prefEntryIndex in 0 until NUM_OF_WRITES_PER_ITERATION) {
64+
65+
sharedPrefKey =
66+
PREF_KEY_PREFIX + prefEntryIndex.toString().padEnd(NUM_OF_WRITES_PER_ITERATION.toString().length)
67+
68+
startNano = dateTimeProvider.getNanoTime()
69+
sharedPrefsEditor = sharedPrefs.edit().putString(sharedPrefKey, sharedPrefValue)
70+
editorWriteFunction(sharedPrefsEditor)
71+
endNano = dateTimeProvider.getNanoTime()
72+
73+
rawTimingsNano[benchmarkIteration]!![prefEntryIndex] = endNano - startNano
74+
75+
}
76+
}
77+
return rawTimingsNano
78+
}
79+
80+
81+
private fun computeResult(rawTimingsNano: Map<Int, MutableMap<Int, Long>>): SharedPrefsWriteResult {
82+
val averageWriteDurationsWithCommit = computeAverageWriteDurationsNano(rawTimingsNano)
83+
return SharedPrefsWriteResult(
84+
averageWriteDurationsWithCommit,
85+
linearFitCalculator.calculateLinearFit(
86+
averageWriteDurationsWithCommit.map { entry -> Pair(entry.key.toDouble(), entry.value.toDouble()) }
87+
)
88+
)
89+
}
90+
91+
private fun computeAverageWriteDurationsNano(sharedPrefsWriteTimings: Map<Int, Map<Int, Long>>): Map<Int, Long> {
92+
val sumsOfWriteTimes = computeSumsWithInternalKeys(sharedPrefsWriteTimings)
93+
return sumsOfWriteTimes.mapValues { entry ->
94+
(entry.value.toDouble() / sharedPrefsWriteTimings.size).toLong()
95+
}
96+
}
97+
98+
private fun computeSumsWithInternalKeys(externalMap: Map<Int, Map<Int, Long>>): Map<Int, Long> {
99+
val sums = mutableMapOf<Int, Long>()
100+
for ((externalKey, internalMap) in externalMap) {
101+
for ((internalKey, value) in internalMap) {
102+
sums[internalKey] = sums.getOrDefault(internalKey, 0) + value
103+
}
104+
}
105+
return sums
106+
}
107+
108+
companion object {
109+
110+
private const val SHARED_PREFS_NAME = "benchmark_shared_prefs"
111+
private const val NUM_OF_WRITES_PER_ITERATION = 100
112+
private const val PREF_KEY_PREFIX = "key"
113+
private const val NUM_ITERATIONS = 10
114+
}
115+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.techyourchance.android.benchmarks.shared_prefs_write
2+
3+
import com.techyourchance.android.common.maths.LinearFitCoefficients
4+
5+
data class SharedPrefsWriteResult(
6+
val entryIndexToAverageEditDurationsNano: Map<Int, Long>,
7+
val linearFitCoefficients: LinearFitCoefficients,
8+
)

app/src/main/java/com/techyourchance/android/common/dependencyinjection/controller/ControllerComponent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.techyourchance.android.screens.animations.dotsprogress.DotsProgressAn
2020
import com.techyourchance.android.screens.benchmarks.backgroundtasksmemorybenchmark.BackgroundTasksMemoryBenchmarkFragment
2121
import com.techyourchance.android.screens.benchmarks.benchmarkslist.BenchmarksListFragment
2222
import com.techyourchance.android.screens.benchmarks.backgroundtasksstartupbenchmark.BackgroundTasksStartupBenchmarkFragment
23+
import com.techyourchance.android.screens.benchmarks.sharedprefs.SharedPrefsBenchmarkFragment
2324
import com.techyourchance.android.screens.composeoverlay.ComposeOverlayFragment
2425
import com.techyourchance.android.screens.composenavbottombar.ComposeNavBottomBarActivity
2526
import com.techyourchance.android.screens.handlerlooper.HandlerLooperFragment
@@ -54,6 +55,7 @@ interface ControllerComponent {
5455
fun inject(fragment: ComposeOverlayFragment)
5556
fun inject(fragment: AnimatedMessagesFragment)
5657
fun inject(fragment: HandlerLooperFragment)
58+
fun inject(fragment: SharedPrefsBenchmarkFragment)
5759

5860
// Dialogs
5961
fun inject(dialog: PromptDialog)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.techyourchance.android.common.random
2+
3+
import javax.inject.Inject
4+
import java.lang.StringBuilder
5+
import java.security.SecureRandom
6+
import java.util.concurrent.locks.ReentrantLock
7+
import kotlin.concurrent.withLock
8+
9+
class RandomStringsGenerator @Inject constructor() {
10+
11+
fun getRandomAlphanumericString(length: Int): String {
12+
return getRandomStringFromAlphabet(ALPHANUMERIC, length)
13+
}
14+
15+
fun getRandomNumericString(length: Int): String {
16+
return getRandomStringFromAlphabet(NUMERIC, length)
17+
}
18+
19+
private fun getRandomStringFromAlphabet(alphabet: String, length: Int): String {
20+
val sb = StringBuilder(length)
21+
for (i in 0 until length) {
22+
var position: Int
23+
Companion.LOCK.withLock { // thread-safe protection of SecureRandom (just in case)
24+
position = SECURE_RANDOM.nextInt(alphabet.length)
25+
}
26+
sb.append(alphabet[position])
27+
}
28+
return sb.toString()
29+
}
30+
31+
companion object {
32+
private val SECURE_RANDOM = SecureRandom()
33+
private val LOCK = ReentrantLock()
34+
35+
private const val ALPHANUMERIC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
36+
private const val NUMERIC = "0123456789"
37+
}
38+
}

app/src/main/java/com/techyourchance/android/screens/benchmarks/benchmarkslist/BenchmarksListFragment.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class BenchmarksListFragment : BaseFragment(), BenchmarksListViewMvc.Listener {
4141
getString(R.string.screen_background_tasks_memory_benchmark),
4242
ScreenSpec.BackgroundTasksMemoryBenchmark()
4343
),
44+
FromBenchmarksListDestination(
45+
getString(R.string.screen_shared_prefs_benchmark),
46+
ScreenSpec.SharedPrefsBenchmark
47+
),
4448
)
4549
}
4650

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.techyourchance.android.screens.benchmarks.sharedprefs
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import com.techyourchance.android.benchmarks.shared_prefs_write.SharedPrefsWriteBenchmarkUseCase
8+
import com.techyourchance.android.common.random.RandomStringsGenerator
9+
import com.techyourchance.android.screens.common.ScreensNavigator
10+
import com.techyourchance.android.screens.common.dialogs.DialogsNavigator
11+
import com.techyourchance.android.screens.common.fragments.BaseFragment
12+
import com.techyourchance.android.screens.common.mvcviews.ViewMvcFactory
13+
import kotlinx.coroutines.Job
14+
import kotlinx.coroutines.launch
15+
import javax.inject.Inject
16+
17+
class SharedPrefsBenchmarkFragment : BaseFragment(), SharedPrefsBenchmarkViewMvc.Listener {
18+
19+
@Inject lateinit var viewMvcFactory: ViewMvcFactory
20+
@Inject lateinit var dialogsNavigator: DialogsNavigator
21+
@Inject lateinit var screensNavigator: ScreensNavigator
22+
@Inject lateinit var sharedPrefsWriteBenchmarkUseCase: SharedPrefsWriteBenchmarkUseCase
23+
@Inject lateinit var randomStringsGenerator: RandomStringsGenerator
24+
25+
private lateinit var viewMvc: SharedPrefsBenchmarkViewMvc
26+
27+
private var startBenchmark = false
28+
29+
private var benchmarkJob: Job? = null
30+
31+
override fun onCreate(savedInstanceState: Bundle?) {
32+
controllerComponent.inject(this)
33+
super.onCreate(savedInstanceState)
34+
}
35+
36+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
37+
viewMvc = viewMvcFactory.newSharedPrefsBenchmarkViewMvc(container)
38+
return viewMvc.getRootView()
39+
}
40+
41+
override fun onStart() {
42+
super.onStart()
43+
viewMvc.registerListener(this)
44+
viewMvc.showBenchmarkStopped()
45+
if (startBenchmark) {
46+
startBenchmark()
47+
}
48+
}
49+
50+
override fun onStop() {
51+
super.onStop()
52+
viewMvc.unregisterListener(this)
53+
benchmarkJob?.cancel()
54+
}
55+
56+
override fun onToggleBenchmarkClicked() {
57+
benchmarkJob?.let {
58+
if (it.isActive) {
59+
it.cancel()
60+
} else {
61+
startBenchmark()
62+
}
63+
} ?: startBenchmark()
64+
}
65+
66+
private fun startBenchmark() {
67+
benchmarkJob = coroutineScope.launch {
68+
try {
69+
viewMvc.showBenchmarkStarted()
70+
val valueToWrite = randomStringsGenerator.getRandomAlphanumericString(PREF_VALUE_LENGTH)
71+
val result = sharedPrefsWriteBenchmarkUseCase.runBenchmark(valueToWrite)
72+
viewMvc.bindBenchmarkResults(
73+
PREF_VALUE_LENGTH,
74+
result.resultWithCommit,
75+
result.resultWithApply
76+
)
77+
} finally {
78+
viewMvc.showBenchmarkStopped()
79+
}
80+
}
81+
}
82+
83+
override fun onBackClicked() {
84+
screensNavigator.navigateBack()
85+
}
86+
87+
companion object {
88+
89+
private const val PREF_VALUE_LENGTH = 100
90+
91+
fun newInstance(): SharedPrefsBenchmarkFragment {
92+
val args = Bundle(3)
93+
val fragment = SharedPrefsBenchmarkFragment()
94+
fragment.arguments = args
95+
return fragment
96+
}
97+
}
98+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.techyourchance.android.screens.benchmarks.sharedprefs
2+
3+
import com.techyourchance.android.benchmarks.shared_prefs_write.SharedPrefsWriteResult
4+
import com.techyourchance.android.screens.common.mvcviews.BaseObservableViewMvc
5+
6+
abstract class SharedPrefsBenchmarkViewMvc: BaseObservableViewMvc<SharedPrefsBenchmarkViewMvc.Listener>() {
7+
8+
interface Listener {
9+
fun onBackClicked()
10+
fun onToggleBenchmarkClicked()
11+
}
12+
13+
abstract fun bindBenchmarkResults(
14+
prefValueLength: Int,
15+
resultWithCommit: SharedPrefsWriteResult,
16+
resultWithApply: SharedPrefsWriteResult,
17+
)
18+
abstract fun showBenchmarkStarted()
19+
abstract fun showBenchmarkStopped()
20+
}

0 commit comments

Comments
 (0)