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? {