Skip to content

Commit

Permalink
Improve performance of FormatStyle formatting when used in multithrea…
Browse files Browse the repository at this point in the history
…ded context (swiftlang#719)

* Improve performance of FormatStyle formatting when used in multithreaded context

* Change canImport statement to support newer SDK on older macOS
  • Loading branch information
parkera authored Jul 9, 2024
1 parent 9899514 commit 6ee772b
Show file tree
Hide file tree
Showing 16 changed files with 379 additions and 492 deletions.
33 changes: 33 additions & 0 deletions Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@

import Benchmark
import func Benchmark.blackHole
import Dispatch

#if FOUNDATION_FRAMEWORK
import Foundation
#else
import FoundationEssentials
import FoundationInternationalization
#endif

let benchmarks = {
Expand Down Expand Up @@ -57,4 +59,35 @@ let benchmarks = {
}
}
}

Benchmark("parallel-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
for _ in benchmark.scaledIterations {
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
let result = 10.123.formatted()
blackHole(result)
}
}
}

Benchmark("parallel-and-serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
for _ in benchmark.scaledIterations {
DispatchQueue.concurrentPerform(iterations: 10) { _ in
// Reuse the values on this thread a bunch
for _ in 0..<100 {
let result = 10.123.formatted()
blackHole(result)
}
}
}
}

Benchmark("serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
for _ in benchmark.scaledIterations {
for _ in 0..<1000 {
let result = 10.123.formatted()
blackHole(result)
}
}
}

}
2 changes: 1 addition & 1 deletion Sources/FoundationEssentials/Calendar/Calendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ public struct Calendar : Hashable, Equatable, Sendable {
///
/// - note: The autoupdating Calendar will only compare equal to another autoupdating Calendar.
public static var autoupdatingCurrent : Calendar {
Calendar(inner: CalendarCache.cache.autoupdatingCurrent)
Calendar(inner: CalendarCache.autoupdatingCurrent)
}

// MARK: -
Expand Down
138 changes: 53 additions & 85 deletions Sources/FoundationEssentials/Calendar/Calendar_Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import CoreFoundation
#endif

/// Singleton which listens for notifications about preference changes for Calendar and holds cached singletons for the current locale, calendar, and time zone.
struct CalendarCache : Sendable {
struct CalendarCache : Sendable, ~Copyable {

// MARK: - Concrete Classes

Expand All @@ -38,103 +38,71 @@ struct CalendarCache : Sendable {
}
#endif
}

// MARK: - State

struct State : Sendable {
// If nil, the calendar has been invalidated and will be created next time State.current() is called
private var currentCalendar: (any _CalendarProtocol)?
private var autoupdatingCurrentCalendar: _CalendarAutoupdating?
private var fixedCalendars: [Calendar.Identifier: any _CalendarProtocol] = [:]

private var noteCount = -1
private var wasResetManually = false

mutating func check() {
#if FOUNDATION_FRAMEWORK
// On Darwin we listen for certain distributed notifications to reset the current Calendar.
let newNoteCount = _CFLocaleGetNoteCount() + _CFTimeZoneGetNoteCount() + Int(_CFCalendarGetMidnightNoteCount())
#else
let newNoteCount = 1
#endif
if newNoteCount != noteCount || wasResetManually {
// rdar://102017659
// Don't create `currentCalendar` here to avoid deadlocking when retrieving a fixed
// calendar. Creating the current calendar gets the current locale, decodes a plist
// from CFPreferences, and may call +[NSDate initialize] on a separate thread. This
// leads to a deadlock if we are also initializing a class on the current thread
currentCalendar = nil
fixedCalendars = [:]

noteCount = newNoteCount
wasResetManually = false
}
}

mutating func current() -> any _CalendarProtocol {
check()
if let currentCalendar {
return currentCalendar
} else {
let id = Locale.current._calendarIdentifier
// If we cannot create the right kind of class, we fail immediately here
let calendarClass = CalendarCache.calendarICUClass(identifier: id, useGregorian: true)!
let calendar = calendarClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
currentCalendar = calendar
return calendar
}
static let cache = CalendarCache()

// 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.

private let _current = LockedState<(any _CalendarProtocol)?>(initialState: nil)
private let _fixed = LockedState<[Calendar.Identifier: any _CalendarProtocol]>(initialState: [:])

fileprivate init() {
}

var current: any _CalendarProtocol {
if let result = _current.withLock({ $0 }) {
return result
}

let id = Locale.current._calendarIdentifier
// If we cannot create the right kind of class, we fail immediately here
let calendarClass = CalendarCache.calendarICUClass(identifier: id, useGregorian: true)!
let calendar = calendarClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)

mutating func autoupdatingCurrent() -> any _CalendarProtocol {
if let autoupdatingCurrentCalendar {
return autoupdatingCurrentCalendar
return _current.withLock {
if let current = $0 {
// Someone beat us to setting it - use the existing one
return current
} else {
let calendar = _CalendarAutoupdating()
autoupdatingCurrentCalendar = calendar
$0 = calendar
return calendar
}
}

mutating func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol {
check()
if let cached = fixedCalendars[id] {
return cached
} else {
// If we cannot create the right kind of class, we fail immediately here
let calendarClass = CalendarCache.calendarICUClass(identifier: id, useGregorian: true)!
let new = calendarClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
fixedCalendars[id] = new
return new
}
}

mutating func reset() {
wasResetManually = true
}
}

let lock: LockedState<State>

static let cache = CalendarCache()

fileprivate init() {
lock = LockedState(initialState: State())
}


func reset() {
lock.withLock { $0.reset() }
}

var current: any _CalendarProtocol {
lock.withLock { $0.current() }
// rdar://102017659
// Don't create `currentCalendar` here to avoid deadlocking when retrieving a fixed
// calendar. Creating the current calendar gets the current locale, decodes a plist
// from CFPreferences, and may call +[NSDate initialize] on a separate thread. This
// leads to a deadlock if we are also initializing a class on the current thread
_current.withLock { $0 = nil }
_fixed.withLock { $0 = [:] }
}

var autoupdatingCurrent: any _CalendarProtocol {
lock.withLock { $0.autoupdatingCurrent() }
}
// MARK: Singletons

static let autoupdatingCurrent = _CalendarAutoupdating()

// MARK: -

func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol {
lock.withLock { $0.fixed(id) }
if let existing = _fixed.withLock({ $0[id] }) {
return existing
}

// If we cannot create the right kind of class, we fail immediately here
let calendarClass = CalendarCache.calendarICUClass(identifier: id, useGregorian: true)!
let new = calendarClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)

return _fixed.withLock {
if let existing = $0[id] {
return existing
} else {
$0[id] = new
return new
}
}
}

func fixed(identifier: Calendar.Identifier, locale: Locale?, timeZone: TimeZone?, firstWeekday: Int?, minimumDaysInFirstWeek: Int?, gregorianStartDate: Date?) -> any _CalendarProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
//===----------------------------------------------------------------------===//

package struct FormatterCache<Format : Hashable & Sendable, FormattingType: Sendable>: Sendable {

let countLimit = 100

private let _lock: LockedState<[Format: FormattingType]>
Expand Down
6 changes: 3 additions & 3 deletions Sources/FoundationEssentials/Locale/Locale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public struct Locale : Hashable, Equatable, Sendable {
///
/// - note: The autoupdating Locale will only compare equal to another autoupdating Locale.
public static var autoupdatingCurrent : Locale {
Locale(inner: LocaleCache.cache.autoupdatingCurrent)
Locale(inner: LocaleCache.autoupdatingCurrent)
}

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

/// System locale.
internal static var system : Locale {
Locale(inner: LocaleCache.cache.system)
Locale(inner: LocaleCache.system)
}

/// Unlocalized locale (`en_001`).
internal static var unlocalized : Locale {
Locale(inner: LocaleCache.cache.unlocalized)
Locale(inner: LocaleCache.unlocalized)
}

#if FOUNDATION_FRAMEWORK && canImport(_FoundationICU)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ internal final class _LocaleAutoupdating : _LocaleProtocol, @unchecked Sendable
}

func bridgeToNSLocale() -> NSLocale {
LocaleCache.cache.autoupdatingCurrentNSLocale()
LocaleCache.autoupdatingCurrentNSLocale
}
#endif

Expand Down
Loading

0 comments on commit 6ee772b

Please sign in to comment.