From f99e2defc23f5a773b898287536bcba53310d613 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Wed, 28 May 2025 14:01:55 -0700 Subject: [PATCH 1/2] Reduce size of `cachedFixedComponentsLocales` Previously we maintain a cache `private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol]`, and every call to `Locale(components: Locale.Components)` reads and writes to the cache. This results in a cache with non trivial size of keys. Optimize this by replacing the key with a String instead of the full `Locale.Components` instance. Using this as the baseline: ```swift for identifier in Locale.availableIdentifiers { let locale = Locale(identifier: identifier) } ``` With the following as the testing code: ```swift for identifier in Locale.availableIdentifiers { let components = Locale.Components(identifier: identifier) let locale = Locale(components: components) let components2 = Locale.Components(locale: locale) let locale2 = Locale(components: components2) // cache hit } ``` Previously, this incurred +2.6 MB of footprint comparing to the above baseline. After the fix, it incurred only +0.9MB increase. Also benchmarked that we have not regressed the time now that we are using the string-based identifier as the key. Fixes 144642686 --- .../BenchmarkLocale.swift | 28 +++++++++++++-- .../Locale/Locale+Components.swift | 35 +++++++++++++++++++ .../Locale/Locale_Cache.swift | 9 ++--- .../Locale/Locale+Components_ICU.swift | 29 +-------------- 4 files changed, 66 insertions(+), 35 deletions(-) diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift index 5aadcc5d1..16a6b82d4 100644 --- a/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift @@ -13,15 +13,20 @@ import Benchmark import func Benchmark.blackHole -#if FOUNDATION_FRAMEWORK // This test uses CFString +#if os(macOS) && USE_PACKAGE +import FoundationEssentials +import FoundationInternationalization +#else import Foundation +#endif let benchmarks = { Benchmark.defaultConfiguration.maxIterations = 1_000 Benchmark.defaultConfiguration.maxDuration = .seconds(3) Benchmark.defaultConfiguration.scalingFactor = .kilo - Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .mallocCountTotal, .throughput] + Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .throughput, .peakMemoryResident, .peakMemoryResidentDelta] +#if FOUNDATION_FRAMEWORK let string1 = "aaA" as CFString let string2 = "AAĆ " as CFString let range1 = CFRange(location: 0, length: CFStringGetLength(string1)) @@ -34,5 +39,22 @@ let benchmarks = { CFStringCompareWithOptionsAndLocale(string1, string2, range1, .init(rawValue: 0), nsLocale) } } -} #endif + + let identifiers = Locale.availableIdentifiers + let allComponents = identifiers.map { Locale.Components(identifier: $0) } + Benchmark("LocaleInitFromComponents") { benchmark in + for components in allComponents { + let locale = Locale(components: components) + let components2 = Locale.Components(locale: locale) + let locale2 = Locale(components: components2) // cache hit + } + } + + Benchmark("LocaleComponentsInitIdentifer") { benchmark in + for identifier in identifiers { + let components = Locale.Components(identifier: identifier) + } + } +} + diff --git a/Sources/FoundationEssentials/Locale/Locale+Components.swift b/Sources/FoundationEssentials/Locale/Locale+Components.swift index a093b21ff..c6a0cd4e9 100644 --- a/Sources/FoundationEssentials/Locale/Locale+Components.swift +++ b/Sources/FoundationEssentials/Locale/Locale+Components.swift @@ -85,6 +85,41 @@ extension Locale { public init(languageCode: Locale.LanguageCode? = nil, script: Locale.Script? = nil, languageRegion: Locale.Region? = nil) { self.languageComponents = Language.Components(languageCode: languageCode, script: script, region: languageRegion) } + + // Returns an ICU-style identifier like "de_DE@calendar=gregorian" + package var icuIdentifier: String { + + var keywords = [(ICULegacyKey, String)]() + if let id = calendar?.cldrIdentifier { keywords.append((Calendar.Identifier.legacyKeywordKey, id)) } + if let id = collation?._normalizedIdentifier { keywords.append((Locale.Collation.legacyKeywordKey, id)) } + if let id = currency?._normalizedIdentifier { keywords.append((Locale.Currency.legacyKeywordKey, id)) } + if let id = numberingSystem?._normalizedIdentifier { keywords.append((Locale.NumberingSystem.legacyKeywordKey, id)) } + if let id = firstDayOfWeek?.rawValue { keywords.append((Locale.Weekday.legacyKeywordKey, id)) } + if let id = hourCycle?.rawValue { keywords.append((Locale.HourCycle.legacyKeywordKey, id)) } + if let id = measurementSystem?._normalizedIdentifier { keywords.append((Locale.MeasurementSystem.legacyKeywordKey, id)) } + // No need for redundant region keyword + if let region = region, region != languageComponents.region { + // rg keyword value is actually a subdivision code + keywords.append((Locale.Region.legacyKeywordKey, Locale.Subdivision.subdivision(for: region)._normalizedIdentifier)) + } + if let id = subdivision?._normalizedIdentifier { keywords.append((Locale.Subdivision.legacyKeywordKey, id)) } + if let id = timeZone?.identifier { keywords.append((TimeZone.legacyKeywordKey, id)) } + if let id = variant?._normalizedIdentifier { keywords.append((Locale.Variant.legacyKeywordKey, id)) } + + var locID = languageComponents.identifier + let keywordCounts = keywords.count + if keywordCounts > 0 { + locID.append("@") + } + + for (i, (key, val)) in keywords.enumerated() { + locID.append("\(key.key)=\(val)") + if i != keywordCounts - 1 { + locID.append(";") + } + } + return locID + } } } diff --git a/Sources/FoundationEssentials/Locale/Locale_Cache.swift b/Sources/FoundationEssentials/Locale/Locale_Cache.swift index 004ac1ae8..1d268ee12 100644 --- a/Sources/FoundationEssentials/Locale/Locale_Cache.swift +++ b/Sources/FoundationEssentials/Locale/Locale_Cache.swift @@ -45,7 +45,7 @@ struct LocaleCache : Sendable, ~Copyable { } private var cachedFixedLocales: [String : any _LocaleProtocol] = [:] - private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol] = [:] + private var cachedFixedComponentsLocales: [String : any _LocaleProtocol] = [:] #if FOUNDATION_FRAMEWORK private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:] @@ -99,17 +99,18 @@ struct LocaleCache : Sendable, ~Copyable { #endif // FOUNDATION_FRAMEWORK - func fixedComponents(_ comps: Locale.Components) -> (any _LocaleProtocol)? { + func fixedComponents(_ comps: String) -> (any _LocaleProtocol)? { cachedFixedComponentsLocales[comps] } mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol { - if let l = fixedComponents(comps) { + let identifier = comps.icuIdentifier + if let l = fixedComponents(identifier) { return l } else { let new = _localeICUClass().init(components: comps) - cachedFixedComponentsLocales[comps] = new + cachedFixedComponentsLocales[identifier] = new return new } } diff --git a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift index b4cc25488..6aaf8f5e9 100644 --- a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift @@ -22,34 +22,7 @@ internal import _FoundationICU @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) extension Locale.Components { - // Returns an ICU-style identifier like "de_DE@calendar=gregorian" - internal var icuIdentifier: String { - var keywords: [ICULegacyKey: String] = [:] - if let id = calendar?.cldrIdentifier { keywords[Calendar.Identifier.legacyKeywordKey] = id } - if let id = collation?._normalizedIdentifier { keywords[Locale.Collation.legacyKeywordKey] = id } - if let id = currency?._normalizedIdentifier { keywords[Locale.Currency.legacyKeywordKey] = id } - if let id = numberingSystem?._normalizedIdentifier { keywords[Locale.NumberingSystem.legacyKeywordKey] = id } - if let id = firstDayOfWeek?.rawValue { keywords[Locale.Weekday.legacyKeywordKey] = id } - if let id = hourCycle?.rawValue { keywords[Locale.HourCycle.legacyKeywordKey] = id } - if let id = measurementSystem?._normalizedIdentifier { keywords[Locale.MeasurementSystem.legacyKeywordKey] = id } - // No need for redundant region keyword - if let region = region, region != languageComponents.region { - // rg keyword value is actually a subdivision code - keywords[Locale.Region.legacyKeywordKey] = Locale.Subdivision.subdivision(for: region)._normalizedIdentifier - } - if let id = subdivision?._normalizedIdentifier { keywords[Locale.Subdivision.legacyKeywordKey] = id } - if let id = timeZone?.identifier { keywords[TimeZone.legacyKeywordKey] = id } - if let id = variant?._normalizedIdentifier { keywords[Locale.Variant.legacyKeywordKey] = id } - - var locID = languageComponents.identifier - for (key, val) in keywords { - // This uses legacy key-value pairs, like "collation=phonebook" instead of "-cu-phonebk", so be sure that the above values are `legacyKeywordKey` - // See Locale.Components.legacyKey(forKey:) for more info on performance costs - locID = Locale.identifierWithKeywordValue(locID, key: key, value: val) - } - return locID - } - + /// - Parameter identifier: Unicode language identifier such as "en-u-nu-thai-ca-buddhist-kk-true" public init(identifier: String) { let languageComponents = Locale.Language.Components(identifier: identifier) From 1f567d06f100b07c9a1e5fc654d4386a08be2fe7 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Fri, 30 May 2025 10:06:58 -0700 Subject: [PATCH 2/2] Add a comment about using Locale.Components.icuIdentifier as the cache key --- .../FoundationEssentials/Locale/Locale+Components.swift | 1 + Sources/FoundationEssentials/Locale/Locale_Cache.swift | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/Locale/Locale+Components.swift b/Sources/FoundationEssentials/Locale/Locale+Components.swift index c6a0cd4e9..918100a8c 100644 --- a/Sources/FoundationEssentials/Locale/Locale+Components.swift +++ b/Sources/FoundationEssentials/Locale/Locale+Components.swift @@ -87,6 +87,7 @@ extension Locale { } // Returns an ICU-style identifier like "de_DE@calendar=gregorian" + // Must include every component stored by a `Locale.Components`, and be kept in sync with `init(identifier:)`. package var icuIdentifier: String { var keywords = [(ICULegacyKey, String)]() diff --git a/Sources/FoundationEssentials/Locale/Locale_Cache.swift b/Sources/FoundationEssentials/Locale/Locale_Cache.swift index 1d268ee12..96b8c36e2 100644 --- a/Sources/FoundationEssentials/Locale/Locale_Cache.swift +++ b/Sources/FoundationEssentials/Locale/Locale_Cache.swift @@ -45,7 +45,7 @@ struct LocaleCache : Sendable, ~Copyable { } private var cachedFixedLocales: [String : any _LocaleProtocol] = [:] - private var cachedFixedComponentsLocales: [String : any _LocaleProtocol] = [:] + private var cachedFixedComponentsLocales: [String /*ICU identifier*/: any _LocaleProtocol] = [:] #if FOUNDATION_FRAMEWORK private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:] @@ -99,13 +99,9 @@ struct LocaleCache : Sendable, ~Copyable { #endif // FOUNDATION_FRAMEWORK - func fixedComponents(_ comps: String) -> (any _LocaleProtocol)? { - cachedFixedComponentsLocales[comps] - } - mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol { let identifier = comps.icuIdentifier - if let l = fixedComponents(identifier) { + if let l = cachedFixedComponentsLocales[identifier] { return l } else { let new = _localeICUClass().init(components: comps)