Skip to content

Commit 446af5c

Browse files
authored
Formatting performance improvements (#884)
1 parent 9c71a4a commit 446af5c

File tree

16 files changed

+376
-489
lines changed

16 files changed

+376
-489
lines changed

Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import Benchmark
1414
import func Benchmark.blackHole
15+
import Dispatch
1516

1617
#if os(macOS) && USE_PACKAGE
1718
import FoundationEssentials
@@ -61,5 +62,35 @@ let benchmarks = {
6162
}
6263
}
6364

65+
Benchmark("parallel-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
66+
for _ in benchmark.scaledIterations {
67+
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
68+
let result = 10.123.formatted()
69+
blackHole(result)
70+
}
71+
}
72+
}
73+
74+
Benchmark("parallel-and-serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
75+
for _ in benchmark.scaledIterations {
76+
DispatchQueue.concurrentPerform(iterations: 10) { _ in
77+
// Reuse the values on this thread a bunch
78+
for _ in 0..<100 {
79+
let result = 10.123.formatted()
80+
blackHole(result)
81+
}
82+
}
83+
}
84+
}
85+
86+
Benchmark("serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
87+
for _ in benchmark.scaledIterations {
88+
for _ in 0..<1000 {
89+
let result = 10.123.formatted()
90+
blackHole(result)
91+
}
92+
}
93+
}
94+
6495
#endif // swift(>=6)
6596
}

Sources/FoundationEssentials/Calendar/Calendar.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ public struct Calendar : Hashable, Equatable, Sendable {
323323
///
324324
/// - note: The autoupdating Calendar will only compare equal to another autoupdating Calendar.
325325
public static var autoupdatingCurrent : Calendar {
326-
Calendar(inner: CalendarCache.cache.autoupdatingCurrent)
326+
Calendar(inner: CalendarCache.autoupdatingCurrent)
327327
}
328328

329329
// MARK: -

Sources/FoundationEssentials/Calendar/Calendar_Cache.swift

Lines changed: 53 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -34,103 +34,73 @@ func _calendarClass(identifier: Calendar.Identifier, useGregorian: Bool) -> _Cal
3434
}
3535

3636
/// Singleton which listens for notifications about preference changes for Calendar and holds cached singletons for the current locale, calendar, and time zone.
37-
struct CalendarCache : Sendable {
37+
struct CalendarCache : Sendable, ~Copyable {
3838
// MARK: - State
3939

40-
struct State : Sendable {
41-
// If nil, the calendar has been invalidated and will be created next time State.current() is called
42-
private var currentCalendar: (any _CalendarProtocol)?
43-
private var autoupdatingCurrentCalendar: _CalendarAutoupdating?
44-
private var fixedCalendars: [Calendar.Identifier: any _CalendarProtocol] = [:]
45-
46-
private var noteCount = -1
47-
private var wasResetManually = false
48-
49-
mutating func check() {
50-
#if FOUNDATION_FRAMEWORK
51-
// On Darwin we listen for certain distributed notifications to reset the current Calendar.
52-
let newNoteCount = _CFLocaleGetNoteCount() + _CFTimeZoneGetNoteCount() + Int(_CFCalendarGetMidnightNoteCount())
53-
#else
54-
let newNoteCount = 1
55-
#endif
56-
if newNoteCount != noteCount || wasResetManually {
57-
// rdar://102017659
58-
// Don't create `currentCalendar` here to avoid deadlocking when retrieving a fixed
59-
// calendar. Creating the current calendar gets the current locale, decodes a plist
60-
// from CFPreferences, and may call +[NSDate initialize] on a separate thread. This
61-
// leads to a deadlock if we are also initializing a class on the current thread
62-
currentCalendar = nil
63-
fixedCalendars = [:]
64-
65-
noteCount = newNoteCount
66-
wasResetManually = false
67-
}
68-
}
69-
70-
mutating func current() -> any _CalendarProtocol {
71-
check()
72-
if let currentCalendar {
73-
return currentCalendar
74-
} else {
75-
let id = Locale.current._calendarIdentifier
76-
// If we cannot create the right kind of class, we fail immediately here
77-
let calendarClass = _calendarClass(identifier: id, useGregorian: true)!
78-
let calendar = calendarClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
79-
currentCalendar = calendar
80-
return calendar
81-
}
40+
static let cache = CalendarCache()
41+
42+
// The values stored in these two locks do not depend upon each other, so it is safe to access them with separate locks. This helps avoids contention on a single lock.
43+
44+
private let _current = LockedState<(any _CalendarProtocol)?>(initialState: nil)
45+
private let _fixed = LockedState<[Calendar.Identifier: any _CalendarProtocol]>(initialState: [:])
46+
47+
fileprivate init() {
48+
}
49+
50+
var current: any _CalendarProtocol {
51+
if let result = _current.withLock({ $0 }) {
52+
return result
8253
}
54+
55+
let id = Locale.current._calendarIdentifier
56+
// If we cannot create the right kind of class, we fail immediately here
57+
let calendarClass = _calendarClass(identifier: id, useGregorian: true)!
58+
let calendar = calendarClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
8359

84-
mutating func autoupdatingCurrent() -> any _CalendarProtocol {
85-
if let autoupdatingCurrentCalendar {
86-
return autoupdatingCurrentCalendar
60+
return _current.withLock {
61+
if let current = $0 {
62+
// Someone beat us to setting it - use the existing one
63+
return current
8764
} else {
88-
let calendar = _CalendarAutoupdating()
89-
autoupdatingCurrentCalendar = calendar
65+
$0 = calendar
9066
return calendar
9167
}
9268
}
93-
94-
mutating func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol {
95-
check()
96-
if let cached = fixedCalendars[id] {
97-
return cached
98-
} else {
99-
// If we cannot create the right kind of class, we fail immediately here
100-
let calendarClass = _calendarClass(identifier: id, useGregorian: true)!
101-
let new = calendarClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
102-
fixedCalendars[id] = new
103-
return new
104-
}
105-
}
106-
107-
mutating func reset() {
108-
wasResetManually = true
109-
}
110-
}
111-
112-
let lock: LockedState<State>
113-
114-
static let cache = CalendarCache()
115-
116-
fileprivate init() {
117-
lock = LockedState(initialState: State())
11869
}
119-
70+
12071
func reset() {
121-
lock.withLock { $0.reset() }
122-
}
123-
124-
var current: any _CalendarProtocol {
125-
lock.withLock { $0.current() }
72+
// rdar://102017659
73+
// Don't create `currentCalendar` here to avoid deadlocking when retrieving a fixed
74+
// calendar. Creating the current calendar gets the current locale, decodes a plist
75+
// from CFPreferences, and may call +[NSDate initialize] on a separate thread. This
76+
// leads to a deadlock if we are also initializing a class on the current thread
77+
_current.withLock { $0 = nil }
78+
_fixed.withLock { $0 = [:] }
12679
}
12780

128-
var autoupdatingCurrent: any _CalendarProtocol {
129-
lock.withLock { $0.autoupdatingCurrent() }
130-
}
81+
// MARK: Singletons
82+
83+
static let autoupdatingCurrent = _CalendarAutoupdating()
84+
85+
// MARK: -
13186

13287
func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol {
133-
lock.withLock { $0.fixed(id) }
88+
if let existing = _fixed.withLock({ $0[id] }) {
89+
return existing
90+
}
91+
92+
// If we cannot create the right kind of class, we fail immediately here
93+
let calendarClass = _calendarClass(identifier: id, useGregorian: true)!
94+
let new = calendarClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
95+
96+
return _fixed.withLock {
97+
if let existing = $0[id] {
98+
return existing
99+
} else {
100+
$0[id] = new
101+
return new
102+
}
103+
}
134104
}
135105

136106
func fixed(identifier: Calendar.Identifier, locale: Locale?, timeZone: TimeZone?, firstWeekday: Int?, minimumDaysInFirstWeek: Int?, gregorianStartDate: Date?) -> any _CalendarProtocol {

Sources/FoundationEssentials/Formatting/FormatterCache.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
package struct FormatterCache<Format : Hashable & Sendable, FormattingType: Sendable>: Sendable {
14-
1514
let countLimit = 100
1615

1716
private let _lock: LockedState<[Format: FormattingType]>

Sources/FoundationEssentials/Locale/Locale.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public struct Locale : Hashable, Equatable, Sendable {
6565
///
6666
/// - note: The autoupdating Locale will only compare equal to another autoupdating Locale.
6767
public static var autoupdatingCurrent : Locale {
68-
Locale(inner: LocaleCache.cache.autoupdatingCurrent)
68+
Locale(inner: LocaleCache.autoupdatingCurrent)
6969
}
7070

7171
/// Returns the user's current locale.
@@ -75,12 +75,12 @@ public struct Locale : Hashable, Equatable, Sendable {
7575

7676
/// System locale.
7777
internal static var system : Locale {
78-
Locale(inner: LocaleCache.cache.system)
78+
Locale(inner: LocaleCache.system)
7979
}
8080

8181
/// Unlocalized locale (`en_001`).
8282
internal static var unlocalized : Locale {
83-
Locale(inner: LocaleCache.cache.unlocalized)
83+
Locale(inner: LocaleCache.unlocalized)
8484
}
8585

8686
#if FOUNDATION_FRAMEWORK && canImport(_FoundationICU)

Sources/FoundationEssentials/Locale/Locale_Autoupdating.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ internal final class _LocaleAutoupdating : _LocaleProtocol, @unchecked Sendable
263263
}
264264

265265
func bridgeToNSLocale() -> NSLocale {
266-
LocaleCache.cache.autoupdatingCurrentNSLocale()
266+
LocaleCache.autoupdatingCurrentNSLocale
267267
}
268268
#endif
269269

0 commit comments

Comments
 (0)