Skip to content

Commit 1763514

Browse files
authored
Use a new TimeZone ICU API instead of directing time zone through ucal (#1570)
* Add a benchmark file and scheme for TimeZone * Use a new TimeZone ICU API instead of directing time zone through ucal Switch to use the uatimezone API when we only need timezone information. This is not only faster but also allows us to not have to lock around the shared `UCalendar`. 125359180 * Store the internal ICU timezone object as a non-optional value * Add memory to TimeZone benchmark * Add a benchmark for initiating time zone * add a benchmark for creation and secondsFromGMT:
1 parent c4bde00 commit 1763514

File tree

5 files changed

+160
-70
lines changed

5 files changed

+160
-70
lines changed

Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import FoundationInternationalization
2020
import Foundation
2121
#endif
2222

23+
#if FOUNDATION_FRAMEWORK
24+
// FOUNDATION_FRAMEWORK has a scheme per benchmark file, so only include one benchmark here.
25+
let benchmarks = {
26+
calendarBenchmarks()
27+
}
28+
#endif
29+
2330
func calendarBenchmarks() {
2431

2532
Benchmark.defaultConfiguration.maxIterations = 1_000
@@ -130,7 +137,6 @@ func calendarBenchmarks() {
130137
}
131138

132139
// MARK: - Allocations
133-
134140
let reference = Date(timeIntervalSince1970: 1474666555.0) //2016-09-23T14:35:55-0700
135141

136142
let allocationsConfiguration = Benchmark.Configuration(
@@ -208,7 +214,6 @@ func calendarBenchmarks() {
208214
assert(identifier == "en_US")
209215
}
210216
}
211-
212217
// MARK: - Identifiers
213218

214219
Benchmark("identifierFromComponents", configuration: .init(scalingFactor: .mega)) { benchmark in

Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import FoundationInternationalization
2020
import Foundation
2121
#endif
2222

23+
#if FOUNDATION_FRAMEWORK
24+
// FOUNDATION_FRAMEWORK has a scheme per benchmark file, so only include one benchmark here.
25+
let benchmarks = {
26+
localeBenchmarks()
27+
}
28+
#endif
29+
2330
func localeBenchmarks() {
2431
Benchmark.defaultConfiguration.maxIterations = 1_000
2532
Benchmark.defaultConfiguration.maxDuration = .seconds(3)
@@ -57,4 +64,3 @@ func localeBenchmarks() {
5764
}
5865
}
5966
}
60-
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Benchmark
14+
import func Benchmark.blackHole
15+
16+
#if os(macOS) && USE_PACKAGE
17+
import FoundationEssentials
18+
import FoundationInternationalization
19+
#else
20+
import Foundation
21+
#endif
22+
23+
#if FOUNDATION_FRAMEWORK
24+
// FOUNDATION_FRAMEWORK has a scheme per benchmark file, so only include one benchmark here.
25+
let benchmarks = {
26+
timeZoneBenchmarks()
27+
}
28+
#endif
29+
30+
let testDates = {
31+
var now = Date.now
32+
var dates: [Date] = []
33+
for i in 0...10000 {
34+
dates.append(Date(timeInterval: Double(i * 3600), since: now))
35+
}
36+
return dates
37+
}()
38+
39+
func timeZoneBenchmarks() {
40+
41+
Benchmark.defaultConfiguration.maxIterations = 1_000
42+
Benchmark.defaultConfiguration.maxDuration = .seconds(3)
43+
Benchmark.defaultConfiguration.scalingFactor = .kilo
44+
Benchmark.defaultConfiguration.metrics = [.cpuTotal, .mallocCountTotal, .throughput, .peakMemoryResident]
45+
46+
guard let t = TimeZone(identifier: "America/Los_Angeles") else {
47+
fatalError("unexpected failure when creating time zone")
48+
}
49+
50+
Benchmark("secondsFromGMT", configuration: .init(scalingFactor: .mega)) { benchmark in
51+
for d in testDates {
52+
let s = t.secondsFromGMT(for: d)
53+
blackHole(s)
54+
}
55+
}
56+
57+
Benchmark("creatingTimeZones", configuration: .init(scalingFactor: .mega)) { benchmark in
58+
for name in NSTimeZone.knownTimeZoneNames {
59+
let t = TimeZone(identifier: name)
60+
blackHole(t)
61+
}
62+
}
63+
64+
Benchmark("secondsFromGMT_manyTimeZones", configuration: .init(scalingFactor: .mega)) { benchmark in
65+
for name in NSTimeZone.knownTimeZoneNames {
66+
let t = TimeZone(identifier: name)!
67+
for d in testDates {
68+
let s = t.secondsFromGMT(for: d)
69+
blackHole(s)
70+
}
71+
blackHole(t)
72+
}
73+
}
74+
}
75+
76+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import Benchmark
22

3+
4+
#if os(macOS) && USE_PACKAGE
35
let benchmarks = {
46
calendarBenchmarks()
57
localeBenchmarks()
8+
timeZoneBenchmarks()
69
}
10+
#endif

Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift

Lines changed: 66 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable {
4343
init?(secondsFromGMT: Int) {
4444
fatalError("Unexpected init")
4545
}
46-
46+
47+
// This is safe because it's only mutated at deinit time
48+
nonisolated(unsafe) private let _timeZone : UnsafePointer<UTimeZone?>
49+
4750
// This type is safely sendable because it is guarded by a lock in _TimeZoneICU and we never vend it outside of the lock so it can only ever be accessed from within the lock
4851
struct State : @unchecked Sendable {
4952
/// Access must be serialized
@@ -82,6 +85,9 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable {
8285
guard let c = $0.calendar(identifier) else { return }
8386
ucal_close(c)
8487
}
88+
89+
let mutableT = UnsafeMutablePointer(mutating: _timeZone)
90+
uatimezone_close(mutableT)
8591
}
8692

8793
required init?(identifier: String) {
@@ -99,6 +105,20 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable {
99105
}
100106
}
101107

108+
var status = U_ZERO_ERROR
109+
let timeZone : UnsafeMutablePointer<UTimeZone?>? = Array(identifier.utf16).withUnsafeBufferPointer {
110+
let uatimezone = uatimezone_open($0.baseAddress, Int32($0.count), &status)
111+
guard status.isSuccess else {
112+
return nil
113+
}
114+
return uatimezone
115+
}
116+
117+
guard let timeZone else {
118+
return nil
119+
}
120+
121+
self._timeZone = UnsafePointer(timeZone)
102122
self.name = name
103123
lock = LockedState(initialState: State())
104124
}
@@ -113,27 +133,15 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable {
113133
}
114134

115135
func secondsFromGMT(for date: Date) -> Int {
116-
return lock.withLock {
117-
let udate = date.udate
118-
guard let c = $0.calendar(identifier) else {
119-
return 0
120-
}
121-
122-
var status = U_ZERO_ERROR
123-
ucal_setMillis(c, udate, &status)
124-
125-
let zoneOffset = ucal_get(c, UCAL_ZONE_OFFSET, &status)
126-
guard status.isSuccess else {
127-
return 0
128-
}
129-
130-
status = U_ZERO_ERROR
131-
let dstOffset = ucal_get(c, UCAL_DST_OFFSET, &status)
132-
guard status.isSuccess else {
133-
return 0
134-
}
135-
return Int((zoneOffset + dstOffset) / 1000)
136+
var rawOffset: Int32 = 0
137+
var dstOffset: Int32 = 0
138+
var status: UErrorCode = U_ZERO_ERROR
139+
uatimezone_getOffset(_timeZone, date.udate, 0, &rawOffset, &dstOffset, &status)
140+
guard status.checkSuccessAndLogError("error getting uatimezone offset") else {
141+
return 0
136142
}
143+
144+
return Int((rawOffset + dstOffset) / 1000)
137145
}
138146

139147
func abbreviation(for date: Date) -> String? {
@@ -149,60 +157,51 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable {
149157
}
150158

151159
func daylightSavingTimeOffset(for date: Date) -> TimeInterval {
152-
lock.withLock {
153-
let udate = date.udate
154-
155-
guard let c = $0.calendar(identifier) else { return 0.0 }
156-
var status = U_ZERO_ERROR
157-
ucal_setMillis(c, udate, &status)
158-
let offset = ucal_get(c, UCAL_DST_OFFSET, &status)
159-
if status.isSuccess {
160-
return TimeInterval(Double(offset) / 1000.0)
161-
} else {
162-
return 0.0
163-
}
160+
var rawOffset_unused: Int32 = 0
161+
var dstOffset: Int32 = 0
162+
var status = U_ZERO_ERROR
163+
uatimezone_getOffset(_timeZone, date.udate, 0, &rawOffset_unused, &dstOffset, &status)
164+
if status.isSuccess {
165+
return TimeInterval(Double(dstOffset) / 1000.0)
166+
} else {
167+
return 0.0
164168
}
165169
}
166170

167171
func nextDaylightSavingTimeTransition(after date: Date) -> Date? {
168-
lock.withLock {
169-
guard let c = $0.calendar(identifier) else { return nil }
170-
return Self.nextDaylightSavingTimeTransition(forLocked: c, startingAt: date, limit: Date.validCalendarRange.upperBound)
171-
}
172-
}
172+
var status = U_ZERO_ERROR
173+
var answer = UDate(0.0)
174+
let success = uatimezone_getTimeZoneTransitionDate(_timeZone, date.udate, UCAL_TZ_TRANSITION_NEXT, &answer, &status)
173175

174-
func rawAndDaylightSavingTimeOffset(for date: Date, repeatedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former, skippedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former) -> (rawOffset: Int, daylightSavingOffset: TimeInterval) {
175-
return lock.withLock {
176-
guard let calendar = $0.calendar(identifier) else { return (0, 0) }
177-
var rawOffset: Int32 = 0
178-
var dstOffset: Int32 = 0
179-
var status = U_ZERO_ERROR
180-
let origMillis = ucal_getMillis(calendar, &status)
181-
defer {
182-
ucal_setMillis(calendar, origMillis, &status)
183-
}
184-
ucal_setMillis(calendar, date.udate, &status)
185-
186-
let icuDuplicatedTime: UTimeZoneLocalOption
187-
switch repeatedTimePolicy {
188-
case .former:
189-
icuDuplicatedTime = UCAL_TZ_LOCAL_FORMER
190-
case .latter:
191-
icuDuplicatedTime = UCAL_TZ_LOCAL_LATTER
192-
}
176+
let limit = Date.validCalendarRange.upperBound
177+
guard (success != 0) && status.isSuccess && answer < limit.udate else {
178+
return nil
179+
}
180+
return Date(udate: answer)
181+
}
193182

194-
let icuSkippedTime: UTimeZoneLocalOption
195-
switch skippedTimePolicy {
196-
case .former:
197-
icuSkippedTime = UCAL_TZ_LOCAL_FORMER
198-
case .latter:
199-
icuSkippedTime = UCAL_TZ_LOCAL_LATTER
200-
}
201-
202-
ucal_getTimeZoneOffsetFromLocal(calendar, icuSkippedTime, icuDuplicatedTime, &rawOffset, &dstOffset, &status)
183+
func rawAndDaylightSavingTimeOffset(for date: Date, repeatedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former, skippedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former) -> (rawOffset: Int, daylightSavingOffset: TimeInterval) {
184+
var rawOffset: Int32 = 0
185+
var dstOffset: Int32 = 0
186+
var status = U_ZERO_ERROR
187+
let icuDuplicatedTime: UTimeZoneLocalOption
188+
switch repeatedTimePolicy {
189+
case .former:
190+
icuDuplicatedTime = UCAL_TZ_LOCAL_FORMER
191+
case .latter:
192+
icuDuplicatedTime = UCAL_TZ_LOCAL_LATTER
193+
}
203194

204-
return (Int(rawOffset / 1000), TimeInterval(dstOffset / 1000))
195+
let icuSkippedTime: UTimeZoneLocalOption
196+
switch skippedTimePolicy {
197+
case .former:
198+
icuSkippedTime = UCAL_TZ_LOCAL_FORMER
199+
case .latter:
200+
icuSkippedTime = UCAL_TZ_LOCAL_LATTER
205201
}
202+
203+
uatimezone_getOffsetFromLocal(_timeZone, icuSkippedTime, icuDuplicatedTime, date.udate, &rawOffset, &dstOffset, &status)
204+
return (Int(rawOffset / 1000), TimeInterval(dstOffset / 1000))
206205
}
207206

208207
func localizedName(for style: TimeZone.NameStyle, locale: Locale?) -> String? {

0 commit comments

Comments
 (0)