From 58ae9a638609f865120225f8f660e6740c45a542 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Tue, 23 Sep 2025 16:11:03 -0700 Subject: [PATCH 1/6] Add a benchmark file and scheme for TimeZone --- .../BenchmarkCalendar.swift | 9 ++- .../BenchmarkLocale.swift | 8 ++- .../BenchmarkTimeZone.swift | 58 +++++++++++++++++++ .../InternationalizationBenchmark.swift | 4 ++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift index 55f072b1e..eeb89a6b1 100644 --- a/Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift @@ -20,6 +20,13 @@ import FoundationInternationalization import Foundation #endif +#if FOUNDATION_FRAMEWORK +// FOUNDATION_FRAMEWORK has a scheme per benchmark file, so only include one benchmark here. +let benchmarks = { + calendarBenchmarks() +} +#endif + func calendarBenchmarks() { Benchmark.defaultConfiguration.maxIterations = 1_000 @@ -130,7 +137,6 @@ func calendarBenchmarks() { } // MARK: - Allocations - let reference = Date(timeIntervalSince1970: 1474666555.0) //2016-09-23T14:35:55-0700 let allocationsConfiguration = Benchmark.Configuration( @@ -208,7 +214,6 @@ func calendarBenchmarks() { assert(identifier == "en_US") } } - // MARK: - Identifiers Benchmark("identifierFromComponents", configuration: .init(scalingFactor: .mega)) { benchmark in diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift index 273fdd5b5..1367c7094 100644 --- a/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift @@ -20,6 +20,13 @@ import FoundationInternationalization import Foundation #endif +#if FOUNDATION_FRAMEWORK +// FOUNDATION_FRAMEWORK has a scheme per benchmark file, so only include one benchmark here. +let benchmarks = { + localeBenchmarks() +} +#endif + func localeBenchmarks() { Benchmark.defaultConfiguration.maxIterations = 1_000 Benchmark.defaultConfiguration.maxDuration = .seconds(3) @@ -57,4 +64,3 @@ func localeBenchmarks() { } } } - diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift new file mode 100644 index 000000000..6c930ac86 --- /dev/null +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Benchmark +import func Benchmark.blackHole + +#if os(macOS) && USE_PACKAGE +import FoundationEssentials +import FoundationInternationalization +#else +import Foundation +#endif + +#if FOUNDATION_FRAMEWORK +// FOUNDATION_FRAMEWORK has a scheme per benchmark file, so only include one benchmark here. +let benchmarks = { + timeZoneBenchmarks() +} +#endif + +let testDates = { + var now = Date.now + var dates: [Date] = [] + for i in 0...10000 { + dates.append(Date(timeInterval: Double(i * 3600), since: now)) + } + return dates +}() + +func timeZoneBenchmarks() { + + Benchmark.defaultConfiguration.maxIterations = 1_000 + Benchmark.defaultConfiguration.maxDuration = .seconds(3) + Benchmark.defaultConfiguration.scalingFactor = .kilo + Benchmark.defaultConfiguration.metrics = [.cpuTotal, .mallocCountTotal, .throughput] + + guard let t = TimeZone(identifier: "America/Los_Angeles") else { + fatalError("unexpected failure when creating time zone") + } + + Benchmark("secondsFromGMT", configuration: .init(scalingFactor: .mega)) { benchmark in + for d in testDates { + let s = t.secondsFromGMT(for: d) + blackHole(s) + } + } +} + + diff --git a/Benchmarks/Benchmarks/Internationalization/InternationalizationBenchmark.swift b/Benchmarks/Benchmarks/Internationalization/InternationalizationBenchmark.swift index 759e9eba7..fcd26a0c1 100644 --- a/Benchmarks/Benchmarks/Internationalization/InternationalizationBenchmark.swift +++ b/Benchmarks/Benchmarks/Internationalization/InternationalizationBenchmark.swift @@ -1,6 +1,10 @@ import Benchmark + +#if os(macOS) && USE_PACKAGE let benchmarks = { calendarBenchmarks() localeBenchmarks() + timeZoneBenchmarks() } +#endif From a6fc015bc17d1fea7a5d8e56cc26ccfd73e0d38f Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Tue, 21 Oct 2025 10:24:33 -0700 Subject: [PATCH 2/6] 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 --- .../TimeZone/TimeZone_ICU.swift | 145 ++++++++++-------- 1 file changed, 80 insertions(+), 65 deletions(-) diff --git a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift index 931807868..510651333 100644 --- a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift +++ b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift @@ -43,7 +43,10 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { init?(secondsFromGMT: Int) { fatalError("Unexpected init") } - + + // This is safe because it's only mutated at deinit time + nonisolated(unsafe) private let _timeZone : UnsafePointer? + // 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 struct State : @unchecked Sendable { /// Access must be serialized @@ -82,6 +85,14 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { guard let c = $0.calendar(identifier) else { return } ucal_close(c) } + + guard let t = _timeZone else { + return + } + + let mutableT = UnsafeMutablePointer(mutating: t) + uatimezone_close(mutableT) + } required init?(identifier: String) { @@ -99,6 +110,21 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } } + var status = U_ZERO_ERROR + let timeZone : UnsafeMutablePointer? = Array(identifier.utf16).withUnsafeBufferPointer { + let uatimezone = uatimezone_open($0.baseAddress, Int32($0.count), &status) + guard status.isSuccess else { + return nil + } + return uatimezone + } + + if let timeZone { + self._timeZone = UnsafePointer(timeZone) + } else { + self._timeZone = nil + } + self.name = name lock = LockedState(initialState: State()) } @@ -113,27 +139,18 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } func secondsFromGMT(for date: Date) -> Int { - return lock.withLock { - let udate = date.udate - guard let c = $0.calendar(identifier) else { - return 0 - } - - var status = U_ZERO_ERROR - ucal_setMillis(c, udate, &status) - - let zoneOffset = ucal_get(c, UCAL_ZONE_OFFSET, &status) - guard status.isSuccess else { - return 0 - } - - status = U_ZERO_ERROR - let dstOffset = ucal_get(c, UCAL_DST_OFFSET, &status) - guard status.isSuccess else { - return 0 - } - return Int((zoneOffset + dstOffset) / 1000) + guard let t = _timeZone else { + return 0 + } + var rawOffset: Int32 = 0 + var dstOffset: Int32 = 0 + var status: UErrorCode = U_ZERO_ERROR + uatimezone_getOffset(t, date.udate, 0, &rawOffset, &dstOffset, &status) + guard status.checkSuccessAndLogError("error getting uatimezone offset") else { + return 0 } + + return Int((rawOffset + dstOffset) / 1000) } func abbreviation(for date: Date) -> String? { @@ -149,60 +166,58 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } func daylightSavingTimeOffset(for date: Date) -> TimeInterval { - lock.withLock { - let udate = date.udate + guard let t = _timeZone else { return 0.0 } - guard let c = $0.calendar(identifier) else { return 0.0 } - var status = U_ZERO_ERROR - ucal_setMillis(c, udate, &status) - let offset = ucal_get(c, UCAL_DST_OFFSET, &status) - if status.isSuccess { - return TimeInterval(Double(offset) / 1000.0) - } else { - return 0.0 - } + var rawOffset_unused: Int32 = 0 + var dstOffset: Int32 = 0 + var status = U_ZERO_ERROR + uatimezone_getOffset(t, date.udate, 0, &rawOffset_unused, &dstOffset, &status) + if status.isSuccess { + return TimeInterval(Double(dstOffset) / 1000.0) + } else { + return 0.0 } } func nextDaylightSavingTimeTransition(after date: Date) -> Date? { - lock.withLock { - guard let c = $0.calendar(identifier) else { return nil } - return Self.nextDaylightSavingTimeTransition(forLocked: c, startingAt: date, limit: Date.validCalendarRange.upperBound) - } - } + guard let t = _timeZone else { + return nil + } - func rawAndDaylightSavingTimeOffset(for date: Date, repeatedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former, skippedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former) -> (rawOffset: Int, daylightSavingOffset: TimeInterval) { - return lock.withLock { - guard let calendar = $0.calendar(identifier) else { return (0, 0) } - var rawOffset: Int32 = 0 - var dstOffset: Int32 = 0 - var status = U_ZERO_ERROR - let origMillis = ucal_getMillis(calendar, &status) - defer { - ucal_setMillis(calendar, origMillis, &status) - } - ucal_setMillis(calendar, date.udate, &status) - - let icuDuplicatedTime: UTimeZoneLocalOption - switch repeatedTimePolicy { - case .former: - icuDuplicatedTime = UCAL_TZ_LOCAL_FORMER - case .latter: - icuDuplicatedTime = UCAL_TZ_LOCAL_LATTER - } + var status = U_ZERO_ERROR + var answer = UDate(0.0) + let success = uatimezone_getTimeZoneTransitionDate(t, date.udate, UCAL_TZ_TRANSITION_NEXT, &answer, &status) - let icuSkippedTime: UTimeZoneLocalOption - switch skippedTimePolicy { - case .former: - icuSkippedTime = UCAL_TZ_LOCAL_FORMER - case .latter: - icuSkippedTime = UCAL_TZ_LOCAL_LATTER - } + let limit = Date.validCalendarRange.upperBound + guard (success != 0) && status.isSuccess && answer < limit.udate else { + return nil + } + return Date(udate: answer) + } - ucal_getTimeZoneOffsetFromLocal(calendar, icuSkippedTime, icuDuplicatedTime, &rawOffset, &dstOffset, &status) + func rawAndDaylightSavingTimeOffset(for date: Date, repeatedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former, skippedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former) -> (rawOffset: Int, daylightSavingOffset: TimeInterval) { + guard let t = _timeZone else { return (0, 0) } + var rawOffset: Int32 = 0 + var dstOffset: Int32 = 0 + var status = U_ZERO_ERROR + let icuDuplicatedTime: UTimeZoneLocalOption + switch repeatedTimePolicy { + case .former: + icuDuplicatedTime = UCAL_TZ_LOCAL_FORMER + case .latter: + icuDuplicatedTime = UCAL_TZ_LOCAL_LATTER + } - return (Int(rawOffset / 1000), TimeInterval(dstOffset / 1000)) + let icuSkippedTime: UTimeZoneLocalOption + switch skippedTimePolicy { + case .former: + icuSkippedTime = UCAL_TZ_LOCAL_FORMER + case .latter: + icuSkippedTime = UCAL_TZ_LOCAL_LATTER } + + uatimezone_getOffsetFromLocal(t, icuSkippedTime, icuDuplicatedTime, date.udate, &rawOffset, &dstOffset, &status) + return (Int(rawOffset / 1000), TimeInterval(dstOffset / 1000)) } func localizedName(for style: TimeZone.NameStyle, locale: Locale?) -> String? { From d2b065d594ae7e494a66d712f05f23f215bfd00c Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Tue, 21 Oct 2025 14:05:32 -0700 Subject: [PATCH 3/6] Store the internal ICU timezone object as a non-optional value --- .../TimeZone/TimeZone_ICU.swift | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift index 510651333..1c698a553 100644 --- a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift +++ b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift @@ -45,7 +45,7 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } // This is safe because it's only mutated at deinit time - nonisolated(unsafe) private let _timeZone : UnsafePointer? + nonisolated(unsafe) private let _timeZone : UnsafePointer // 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 struct State : @unchecked Sendable { @@ -86,13 +86,8 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { ucal_close(c) } - guard let t = _timeZone else { - return - } - - let mutableT = UnsafeMutablePointer(mutating: t) + let mutableT = UnsafeMutablePointer(mutating: _timeZone) uatimezone_close(mutableT) - } required init?(identifier: String) { @@ -119,12 +114,11 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { return uatimezone } - if let timeZone { - self._timeZone = UnsafePointer(timeZone) - } else { - self._timeZone = nil + guard let timeZone else { + return nil } + self._timeZone = UnsafePointer(timeZone) self.name = name lock = LockedState(initialState: State()) } @@ -139,13 +133,10 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } func secondsFromGMT(for date: Date) -> Int { - guard let t = _timeZone else { - return 0 - } var rawOffset: Int32 = 0 var dstOffset: Int32 = 0 var status: UErrorCode = U_ZERO_ERROR - uatimezone_getOffset(t, date.udate, 0, &rawOffset, &dstOffset, &status) + uatimezone_getOffset(_timeZone, date.udate, 0, &rawOffset, &dstOffset, &status) guard status.checkSuccessAndLogError("error getting uatimezone offset") else { return 0 } @@ -166,12 +157,10 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } func daylightSavingTimeOffset(for date: Date) -> TimeInterval { - guard let t = _timeZone else { return 0.0 } - var rawOffset_unused: Int32 = 0 var dstOffset: Int32 = 0 var status = U_ZERO_ERROR - uatimezone_getOffset(t, date.udate, 0, &rawOffset_unused, &dstOffset, &status) + uatimezone_getOffset(_timeZone, date.udate, 0, &rawOffset_unused, &dstOffset, &status) if status.isSuccess { return TimeInterval(Double(dstOffset) / 1000.0) } else { @@ -180,13 +169,9 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } func nextDaylightSavingTimeTransition(after date: Date) -> Date? { - guard let t = _timeZone else { - return nil - } - var status = U_ZERO_ERROR var answer = UDate(0.0) - let success = uatimezone_getTimeZoneTransitionDate(t, date.udate, UCAL_TZ_TRANSITION_NEXT, &answer, &status) + let success = uatimezone_getTimeZoneTransitionDate(_timeZone, date.udate, UCAL_TZ_TRANSITION_NEXT, &answer, &status) let limit = Date.validCalendarRange.upperBound guard (success != 0) && status.isSuccess && answer < limit.udate else { @@ -196,7 +181,6 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } func rawAndDaylightSavingTimeOffset(for date: Date, repeatedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former, skippedTimePolicy: TimeZone.DaylightSavingTimePolicy = .former) -> (rawOffset: Int, daylightSavingOffset: TimeInterval) { - guard let t = _timeZone else { return (0, 0) } var rawOffset: Int32 = 0 var dstOffset: Int32 = 0 var status = U_ZERO_ERROR @@ -216,7 +200,7 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { icuSkippedTime = UCAL_TZ_LOCAL_LATTER } - uatimezone_getOffsetFromLocal(t, icuSkippedTime, icuDuplicatedTime, date.udate, &rawOffset, &dstOffset, &status) + uatimezone_getOffsetFromLocal(_timeZone, icuSkippedTime, icuDuplicatedTime, date.udate, &rawOffset, &dstOffset, &status) return (Int(rawOffset / 1000), TimeInterval(dstOffset / 1000)) } From 6845505d3c028978f1a26acbb19f35d1946e2253 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Wed, 22 Oct 2025 10:43:17 -0700 Subject: [PATCH 4/6] Add memory to TimeZone benchmark --- .../Benchmarks/Internationalization/BenchmarkTimeZone.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift index 6c930ac86..c74021346 100644 --- a/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift @@ -41,7 +41,7 @@ func timeZoneBenchmarks() { Benchmark.defaultConfiguration.maxIterations = 1_000 Benchmark.defaultConfiguration.maxDuration = .seconds(3) Benchmark.defaultConfiguration.scalingFactor = .kilo - Benchmark.defaultConfiguration.metrics = [.cpuTotal, .mallocCountTotal, .throughput] + Benchmark.defaultConfiguration.metrics = [.cpuTotal, .mallocCountTotal, .throughput, .peakMemoryResident] guard let t = TimeZone(identifier: "America/Los_Angeles") else { fatalError("unexpected failure when creating time zone") From aae2dfcf947a8ab3e70aa018d07750dfdb793bfe Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Wed, 22 Oct 2025 11:00:29 -0700 Subject: [PATCH 5/6] Add a benchmark for initiating time zone --- .../Internationalization/BenchmarkTimeZone.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift index c74021346..7410f945d 100644 --- a/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift @@ -53,6 +53,13 @@ func timeZoneBenchmarks() { blackHole(s) } } + + Benchmark("creatingTimeZones", configuration: .init(scalingFactor: .mega)) { benchmark in + for name in NSTimeZone.knownTimeZoneNames { + let t = TimeZone(identifier: name) + blackHole(t) + } + } } From 02ba18878404ed13bc214936d2d61a35bbb68e46 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Wed, 22 Oct 2025 11:00:29 -0700 Subject: [PATCH 6/6] add a benchmark for creation and secondsFromGMT: --- .../Internationalization/BenchmarkTimeZone.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift index 7410f945d..85f372750 100644 --- a/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift @@ -60,6 +60,17 @@ func timeZoneBenchmarks() { blackHole(t) } } + + Benchmark("secondsFromGMT_manyTimeZones", configuration: .init(scalingFactor: .mega)) { benchmark in + for name in NSTimeZone.knownTimeZoneNames { + let t = TimeZone(identifier: name)! + for d in testDates { + let s = t.secondsFromGMT(for: d) + blackHole(s) + } + blackHole(t) + } + } }