Skip to content

Commit dc22c86

Browse files
committed
Reuse buffers during reading the timezone data for Windows
This should lower the memory consumption during startup.
1 parent da6f678 commit dc22c86

File tree

1 file changed

+74
-36
lines changed

1 file changed

+74
-36
lines changed

core/windows/src/internal/TzdbInRegistry.kt

Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package kotlinx.datetime.internal
77

88
import kotlinx.datetime.*
99
import kotlinx.cinterop.*
10-
import platform.posix.*
1110
import platform.windows.*
1211

1312
internal class TzdbInRegistry: TimeZoneDatabase {
@@ -77,31 +76,38 @@ internal class TzdbInRegistry: TimeZoneDatabase {
7776
https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
7877
*/
7978
private const val MAX_KEY_LENGTH = 128
79+
private const val KEY_BUFFER_SIZE = MAX_KEY_LENGTH + 1
8080

8181
internal fun processTimeZonesInRegistry(onTimeZone: (String, PerYearZoneRulesData, List<Pair<Int, PerYearZoneRulesData>>) -> Unit) {
8282
memScoped {
83-
withRegistryKey(HKEY_LOCAL_MACHINE!!, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones", { err ->
83+
alloc<HKEYVar>().withRegistryKey(HKEY_LOCAL_MACHINE!!, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones", { err ->
8484
throw IllegalStateException("Error while opening the registry to fetch the time zones (err = $err): ${getLastWindowsError()}")
8585
}) { hKey ->
8686
var index = 0u
87+
val cbDataBuffer = alloc<DWORDVar>()
88+
val zoneInfoBuffer = RegistryTimeZoneInfoBuffer.allocate(this, cbDataBuffer)
89+
val dwordBuffer = RegistryDwordBuffer.allocate(this, cbDataBuffer)
90+
val tzHkeyBuffer = alloc<HKEYVar>()
91+
val dynamicDstHkeyBuffer = alloc<HKEYVar>()
92+
val windowsTzNameBuffer = allocArray<WCHARVar>(KEY_BUFFER_SIZE)
93+
val windowsTzNameSizeBuffer = alloc<UIntVar>()
8794
while (true) {
88-
val windowsTzName = allocArray<WCHARVar>(MAX_KEY_LENGTH + 1)
89-
val bufSize = alloc<UIntVar>().apply { value = MAX_KEY_LENGTH.toUInt() + 1u }
90-
when (RegEnumKeyExW(hKey, index++, windowsTzName, bufSize.ptr, null, null, null, null)) {
95+
windowsTzNameSizeBuffer.value = KEY_BUFFER_SIZE.convert()
96+
when (RegEnumKeyExW(hKey, index++, windowsTzNameBuffer, windowsTzNameSizeBuffer.ptr, null, null, null, null)) {
9197
ERROR_SUCCESS -> {
92-
withRegistryKey(hKey, windowsTzName.toKString(), { err ->
98+
tzHkeyBuffer.withRegistryKey(hKey, windowsTzNameBuffer.toKString(), { err ->
9399
throw IllegalStateException(
94-
"Error while opening the registry to fetch the time zone '${windowsTzName.toKString()} (err = $err)': ${getLastWindowsError()}"
100+
"Error while opening the registry to fetch the time zone '${windowsTzNameBuffer.toKString()} (err = $err)': ${getLastWindowsError()}"
95101
)
96102
}) { tzHKey ->
97103
// first, read the current data from the TZI value
98-
val tziRecord = getRegistryValue<REG_TZI_FORMAT>(tzHKey, "TZI")
104+
val tziRecord = zoneInfoBuffer.readZoneRules(tzHKey, "TZI")
99105
val historicData = try {
100-
readHistoricDataFromRegistry(tzHKey)
106+
dynamicDstHkeyBuffer.readHistoricDataFromRegistry(tzHKey, dwordBuffer, zoneInfoBuffer)
101107
} catch (e: IllegalStateException) {
102108
emptyList()
103109
}
104-
onTimeZone(windowsTzName.toKString(), tziRecord.toZoneRules(), historicData)
110+
onTimeZone(windowsTzNameBuffer.toKString(), tziRecord, historicData)
105111
}
106112
}
107113
ERROR_MORE_DATA -> throw IllegalStateException("The name of a time zone in the registry was too long")
@@ -122,46 +128,78 @@ internal fun processTimeZonesInRegistry(onTimeZone: (String, PerYearZoneRulesDat
122128
*
123129
* @throws IllegalStateException if the 'Dynamic DST' key is present but malformed.
124130
*/
125-
private fun MemScope.readHistoricDataFromRegistry(tzHKey: HKEY): List<Pair<Int, PerYearZoneRulesData>> {
126-
return withRegistryKey(tzHKey, "Dynamic DST", { emptyList() }) { dynDstHKey ->
127-
val firstEntry = getRegistryValue<DWORDVar>(dynDstHKey, "FirstEntry").value.toInt()
128-
val lastEntry = getRegistryValue<DWORDVar>(dynDstHKey, "LastEntry").value.toInt()
131+
private fun HKEYVar.readHistoricDataFromRegistry(
132+
tzHKey: HKEY, dwordBuffer: RegistryDwordBuffer, zoneRulesBuffer: RegistryTimeZoneInfoBuffer
133+
): List<Pair<Int, PerYearZoneRulesData>> =
134+
withRegistryKey(tzHKey, "Dynamic DST", { emptyList() }) { dynDstHKey ->
135+
val firstEntry = dwordBuffer.readValue(dynDstHKey, "FirstEntry")
136+
val lastEntry = dwordBuffer.readValue(dynDstHKey, "LastEntry")
129137
(firstEntry..lastEntry).map { year ->
130-
year to getRegistryValue<REG_TZI_FORMAT>(dynDstHKey, year.toString()).toZoneRules()
138+
year to zoneRulesBuffer.readZoneRules(dynDstHKey, year.toString())
131139
}
132140
}
133-
}
134141

135-
private inline fun<T> MemScope.withRegistryKey(hKey: HKEY, subKeyName: String, onError: (Int) -> T, block: (HKEY) -> T): T {
136-
val subHKey: HKEYVar = alloc()
137-
val err = RegOpenKeyExW(hKey, subKeyName, 0u, KEY_READ.toUInt(), subHKey.ptr)
142+
private inline fun<T> HKEYVar.withRegistryKey(hKey: HKEY, subKeyName: String, onError: (Int) -> T, block: (HKEY) -> T): T {
143+
val err = RegOpenKeyExW(hKey, subKeyName, 0u, KEY_READ.toUInt(), ptr)
138144
return if (err != ERROR_SUCCESS) { onError(err) } else {
139145
try {
140-
block(subHKey.value!!)
146+
block(value!!)
141147
} finally {
142-
RegCloseKey(subHKey.value)
148+
RegCloseKey(value)
143149
}
144150
}
145151
}
146152

147-
private inline fun<reified T: CVariable> MemScope.getRegistryValue(hKey: HKEY, valueName: String): T {
148-
val buffer = alloc<T>()
149-
val cbData = alloc<DWORDVar>().apply { value = sizeOf<T>().convert() }
150-
val err = RegQueryValueExW(hKey, valueName, null, null, buffer.reinterpret<BYTEVar>().ptr, cbData.ptr)
151-
check(err == ERROR_SUCCESS) { "The expected Windows registry value '$valueName' could not be accessed (err = $err)': ${getLastWindowsError()}" }
152-
check(cbData.value.toLong() == sizeOf<T>()) { "Expected '$valueName' to have size ${sizeOf<T>()}, but got ${cbData.value}" }
153-
return buffer
153+
private fun getRegistryValue(
154+
cbDataBuffer: DWORDVar, buffer: CPointer<BYTEVar>, bufferSize: UInt, hKey: HKEY, valueName: String
155+
) {
156+
cbDataBuffer.value = bufferSize
157+
val err = RegQueryValueExW(hKey, valueName, null, null, buffer, cbDataBuffer.ptr)
158+
check(err == ERROR_SUCCESS) {
159+
"The expected Windows registry value '$valueName' could not be accessed (err = $err)': ${getLastWindowsError()}"
160+
}
161+
check(cbDataBuffer.value.convert<UInt>() == bufferSize) {
162+
"Expected '$valueName' to have size $bufferSize, but got ${cbDataBuffer.value}"
163+
}
154164
}
155165

156-
private fun _REG_TZI_FORMAT.toZoneRules(): PerYearZoneRulesData {
157-
val standardOffset = UtcOffset(minutes = -(StandardBias + Bias))
158-
val daylightOffset = UtcOffset(minutes = -(DaylightBias + Bias))
159-
if (DaylightDate.wMonth == 0.convert<WORD>()) {
160-
return PerYearZoneRulesData(standardOffset, null)
166+
private class RegistryTimeZoneInfoBuffer private constructor(
167+
private val buffer: CPointer<BYTEVar>,
168+
private val cbData: DWORDVar,
169+
) {
170+
companion object {
171+
fun allocate(scope: MemScope, cbData: DWORDVar): RegistryTimeZoneInfoBuffer =
172+
RegistryTimeZoneInfoBuffer(scope.allocArray<BYTEVar>(sizeOf<REG_TZI_FORMAT>().convert()), cbData)
173+
}
174+
175+
fun readZoneRules(tzHKey: HKEY, name: String): PerYearZoneRulesData {
176+
getRegistryValue(cbData, buffer, sizeOf<REG_TZI_FORMAT>().convert(), tzHKey, name)
177+
return with(buffer.reinterpret<REG_TZI_FORMAT>().pointed) {
178+
val standardOffset = UtcOffset(minutes = -(StandardBias + Bias))
179+
val daylightOffset = UtcOffset(minutes = -(DaylightBias + Bias))
180+
if (DaylightDate.wMonth == 0.convert<WORD>()) {
181+
return PerYearZoneRulesData(standardOffset, null)
182+
}
183+
val changeToDst = RecurringZoneRules.Rule(DaylightDate.toMonthDayTime(), standardOffset, daylightOffset)
184+
val changeToStd = RecurringZoneRules.Rule(StandardDate.toMonthDayTime(), daylightOffset, standardOffset)
185+
PerYearZoneRulesData(standardOffset, changeToDst to changeToStd)
186+
}
187+
}
188+
}
189+
190+
private class RegistryDwordBuffer private constructor(
191+
private val buffer: CPointer<DWORDVar>,
192+
private val cbData: DWORDVar,
193+
) {
194+
companion object {
195+
fun allocate(scope: MemScope, cbData: DWORDVar): RegistryDwordBuffer =
196+
RegistryDwordBuffer(scope.alloc<DWORDVar>().ptr, cbData)
197+
}
198+
199+
fun readValue(hKey: HKEY, valueName: String): Int {
200+
getRegistryValue(cbData, buffer.reinterpret(), sizeOf<DWORDVar>().convert(), hKey, valueName)
201+
return buffer.pointed.value.toInt()
161202
}
162-
val changeToDst = RecurringZoneRules.Rule(DaylightDate.toMonthDayTime(), standardOffset, daylightOffset)
163-
val changeToStd = RecurringZoneRules.Rule(StandardDate.toMonthDayTime(), daylightOffset, standardOffset)
164-
return PerYearZoneRulesData(standardOffset, changeToDst to changeToStd)
165203
}
166204

167205
/* this code is explained at

0 commit comments

Comments
 (0)