From 1763514e40cceb9326c467bee8126003ca7dc8a6 Mon Sep 17 00:00:00 2001 From: Tina L <49205802+itingliu@users.noreply.github.com> Date: Tue, 4 Nov 2025 08:58:12 -0800 Subject: [PATCH] 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: --- .../BenchmarkCalendar.swift | 9 +- .../BenchmarkLocale.swift | 8 +- .../BenchmarkTimeZone.swift | 76 ++++++++++ .../InternationalizationBenchmark.swift | 4 + .../TimeZone/TimeZone_ICU.swift | 133 +++++++++--------- 5 files changed, 160 insertions(+), 70 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..85f372750 --- /dev/null +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// 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, .peakMemoryResident] + + 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) + } + } + + Benchmark("creatingTimeZones", configuration: .init(scalingFactor: .mega)) { benchmark in + for name in NSTimeZone.knownTimeZoneNames { + let t = TimeZone(identifier: name) + 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) + } + } +} + + 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 diff --git a/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift b/Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift index 931807868..1c698a553 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,9 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { guard let c = $0.calendar(identifier) else { return } ucal_close(c) } + + let mutableT = UnsafeMutablePointer(mutating: _timeZone) + uatimezone_close(mutableT) } required init?(identifier: String) { @@ -99,6 +105,20 @@ 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 + } + + guard let timeZone else { + return nil + } + + self._timeZone = UnsafePointer(timeZone) self.name = name lock = LockedState(initialState: State()) } @@ -113,27 +133,15 @@ 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) + var rawOffset: Int32 = 0 + var dstOffset: Int32 = 0 + var status: UErrorCode = U_ZERO_ERROR + uatimezone_getOffset(_timeZone, 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 +157,51 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable { } func daylightSavingTimeOffset(for date: Date) -> TimeInterval { - lock.withLock { - let udate = date.udate - - 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(_timeZone, 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) - } - } + var status = U_ZERO_ERROR + var answer = UDate(0.0) + let success = uatimezone_getTimeZoneTransitionDate(_timeZone, date.udate, UCAL_TZ_TRANSITION_NEXT, &answer, &status) - 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 - } + let limit = Date.validCalendarRange.upperBound + guard (success != 0) && status.isSuccess && answer < limit.udate else { + return nil + } + return Date(udate: answer) + } - let icuSkippedTime: UTimeZoneLocalOption - switch skippedTimePolicy { - case .former: - icuSkippedTime = UCAL_TZ_LOCAL_FORMER - case .latter: - icuSkippedTime = UCAL_TZ_LOCAL_LATTER - } - - 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) { + 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(_timeZone, icuSkippedTime, icuDuplicatedTime, date.udate, &rawOffset, &dstOffset, &status) + return (Int(rawOffset / 1000), TimeInterval(dstOffset / 1000)) } func localizedName(for style: TimeZone.NameStyle, locale: Locale?) -> String? {