Skip to content

Commit 55e46ef

Browse files
committed
Performance: Cache DateTimeFormatter instances to reduce allocations
1 parent 3065ccd commit 55e46ef

2 files changed

Lines changed: 68 additions & 6 deletions

File tree

app/src/main/java/com/leanbitlab/lwidget/AwidgetProvider.kt

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,21 @@ class AwidgetProvider : AppWidgetProvider() {
165165
}
166166

167167
companion object {
168+
169+
private var cachedLocale: java.util.Locale? = null
170+
private val formatters = java.util.concurrent.ConcurrentHashMap<String, java.time.format.DateTimeFormatter>()
171+
172+
private fun getFormatter(pattern: String): java.time.format.DateTimeFormatter {
173+
val currentLocale = java.util.Locale.getDefault()
174+
if (cachedLocale != currentLocale) {
175+
cachedLocale = currentLocale
176+
formatters.clear()
177+
}
178+
return formatters.getOrPut(pattern) {
179+
java.time.format.DateTimeFormatter.ofPattern(pattern, currentLocale)
180+
}
181+
}
182+
168183
const val ACTION_BATTERY_UPDATE = "com.leanbitlab.lwidget.ACTION_BATTERY_UPDATE"
169184
const val PERMISSION_READ_TASKS_ORG = "org.tasks.permission.READ_TASKS"
170185
const val PERMISSION_READ_TASKS_ASTRID = "com.todoroo.astrid.READ"
@@ -928,9 +943,9 @@ class AwidgetProvider : AppWidgetProvider() {
928943
}
929944

930945
private fun bindCalendarEvents(context: Context, views: RemoteViews, events: List<EventInfo>, textSizeSp: Float, primaryColor: Int, secondaryColor: Int, eventViews: List<Int>) {
931-
val timeFormatter = DateTimeFormatter.ofPattern("h:mm a", Locale.getDefault())
932-
val dayFormatter = DateTimeFormatter.ofPattern("EEE", Locale.getDefault())
933-
val dateFormatter = DateTimeFormatter.ofPattern("d MMM h:mma", Locale.getDefault())
946+
val timeFormatter = getFormatter("h:mm a")
947+
val dayFormatter = getFormatter("EEE")
948+
val dateFormatter = getFormatter("d MMM h:mma")
934949

935950
if (events.isEmpty()) {
936951
views.setTextViewText(eventViews[0], "No events today")
@@ -1157,7 +1172,7 @@ class AwidgetProvider : AppWidgetProvider() {
11571172
} else if (dueDate.isEqual(tomorrow)) {
11581173
" (Tomorrow)"
11591174
} else {
1160-
val df = DateTimeFormatter.ofPattern("MMM d", Locale.getDefault())
1175+
val df = getFormatter("MMM d")
11611176
" (${dueDate.format(df)})"
11621177
}
11631178
}
@@ -1222,7 +1237,7 @@ class AwidgetProvider : AppWidgetProvider() {
12221237
val zoneId = ZoneId.of(zoneIdStr)
12231238
val zdt = java.time.ZonedDateTime.now(zoneId)
12241239
val pattern = if (is12Hour) "h:mm a" else "H:mm"
1225-
val formatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault())
1240+
val formatter = getFormatter(pattern)
12261241
val timeStr = zdt.format(formatter)
12271242

12281243
// Format: "🌍 10:30 AM"
@@ -1243,7 +1258,7 @@ class AwidgetProvider : AppWidgetProvider() {
12431258

12441259
if (nextAlarm != null) {
12451260
val nextAlarmTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(nextAlarm.triggerTime), ZoneId.systemDefault())
1246-
val timeFormatter = DateTimeFormatter.ofPattern("h:mm a", Locale.getDefault())
1261+
val timeFormatter = getFormatter("h:mm a")
12471262
val timeText = nextAlarmTime.format(timeFormatter)
12481263

12491264
// Format: "| ⏰ 7:00 AM"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.leanbitlab.lwidget
2+
3+
import android.content.Context
4+
import android.widget.RemoteViews
5+
import org.junit.Test
6+
import org.junit.runner.RunWith
7+
import org.robolectric.RobolectricTestRunner
8+
import org.robolectric.RuntimeEnvironment
9+
import org.mockito.Mockito.mock
10+
import kotlin.system.measureNanoTime
11+
import java.time.format.DateTimeFormatter
12+
import java.util.Locale
13+
14+
@RunWith(RobolectricTestRunner::class)
15+
class AwidgetProviderBenchmarkTest {
16+
17+
@Test
18+
fun benchmarkFormatterCreation() {
19+
// Warmup
20+
for (i in 0 until 100) {
21+
DateTimeFormatter.ofPattern("h:mm a", Locale.getDefault())
22+
}
23+
24+
val iterations = 10000
25+
val timeNormal = measureNanoTime {
26+
for (i in 0 until iterations) {
27+
DateTimeFormatter.ofPattern("h:mm a", Locale.getDefault())
28+
}
29+
}
30+
31+
// Warmup Cached
32+
val m = AwidgetProvider.Companion::class.java.getDeclaredMethod("getFormatter", String::class.java)
33+
m.isAccessible = true
34+
for (i in 0 until 100) {
35+
m.invoke(AwidgetProvider.Companion, "h:mm a")
36+
}
37+
38+
val timeCached = measureNanoTime {
39+
for (i in 0 until iterations) {
40+
m.invoke(AwidgetProvider.Companion, "h:mm a")
41+
}
42+
}
43+
44+
println("BENCHMARK_NORMAL: ${timeNormal / iterations} ns per call")
45+
println("BENCHMARK_CACHED: ${timeCached / iterations} ns per call")
46+
}
47+
}

0 commit comments

Comments
 (0)