Skip to content

Build TimeZone.abbreviationDictionary on first use - #136

Merged
piercifani merged 1 commit into
skiptools:mainfrom
piercifani:lazy-timezone-abbreviations
Oct 19, 2026
Merged

piercifani merged 1 commit into
skiptools:mainfrom
piercifani:lazy-timezone-abbreviations

Conversation

@piercifani

Copy link
Copy Markdown
Contributor

#95 added TimeZone.abbreviationDictionary as a stored static var with an initializer that walks every known zone and asks ICU for its short standard and daylight names. In Swift that initializer is lazy. On Android it becomes part of the TimeZone class initializer, so the walk runs the first time anything touches TimeZone, whether or not abbreviations are ever used.

UserDefaults touches it during its own static init (UserDefaults → ISO8601DateFormatter → TimeZone), so in practice the walk runs on the main thread at app launch, usually as the app's first UserDefaults read:

android.icu.text.TimeZoneNames.getDisplayName
skip.foundation.TimeZone.abbreviationDictionary$lambda$1(TimeZone.kt:230)
skip.foundation.TimeZone.<clinit>(TimeZone.kt:224)
skip.foundation.ISO8601DateFormatter.<init>(ISO8601DateFormatter.kt:33)
skip.foundation.UserDefaults.<clinit>(UserDefaults.kt:600)

abbreviationDictionary is now a computed property backed by a cache. It's built the first time it's read, which in practice means TimeZone(abbreviation:). The public API is unchanged: reads return the same dictionary, and assigning it still replaces it. The cache isn't synchronized, so concurrent first reads may each build the same dictionary, which is harmless.

Measurements

A production Skip Fuse app on a low-end Android 14 device (Redmi 10 5G, 4 GB RAM), cold starts with a main-thread stall watchdog (stalls over 250 ms), 5–6 runs per build. The two builds are identical apart from this change:

before after
Launch-time main-thread message containing the TimeZone init 1491–1643 ms 1087–1210 ms
Cold start to first frame (am start -W, WaitTime) 2326–2441 ms 1886–2032 ms

The ~400 ms difference is the dictionary walk. The rest of that message is app code.

Testing

  • New test_abbreviationDictionaryIsBuiltOnFirstUse (Robolectric): it clears the cache, then creates time zones by identifier and by offset and reads TimeZone.current, and checks that the dictionary hasn't been built yet. A TimeZone(abbreviation:) lookup then builds it.
  • The existing abbreviation tests (test_abbreviationDictionary, test_abbreviationDictionaryResolvesSystemZones, test_initializingTimeZoneWithAbbreviation, test_initializingTimeZoneWithCustomAbbreviations) pass unchanged.
  • swift test --filter XCSkipTests passes.

Skip Pull Request Checklist:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device

  • AI was used to generate or assist with generating this PR. Claude Code wrote the change and the test, ran the tests on Robolectric, and measured the effect on an Android device.

🤖 Generated with Claude Code

On Android a stored static's initializer runs when the class loads, so
the walk over every known zone ran the first time anything touched
TimeZone. UserDefaults does that during its own static init, putting
the walk on the main thread at app launch. Build and cache the
dictionary on first read instead; assigning it still replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cla-bot cla-bot Bot added the cla-signed label Oct 19, 2026
@piercifani
piercifani merged commit 8b50c7d into skiptools:main Oct 19, 2026
2 checks passed
@piercifani
piercifani deleted the lazy-timezone-abbreviations branch October 19, 2026 19:31

@marcprux marcprux left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants