Fix performance regressions vs v3.0.0-rc.6 in transformers and registry lookups#1637
Fix performance regressions vs v3.0.0-rc.6 in transformers and registry lookups#1637clairernovotny merged 5 commits intomainfrom
Conversation
…egressions Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
This PR refactors string case transformation implementations and removes caching from the LocaliserRegistry. The changes standardize how culture information is handled and optimize performance by using TextInfo methods directly instead of string extension methods.
Key changes:
- Unified culture parameter handling across case transformers by passing
nullinstead ofCultureInfo.CurrentCultureto overloads - Replaced
string.ToUpper(CultureInfo)andstring.ToLower(CultureInfo)withTextInfo.ToUpper(string)andTextInfo.ToLower(string)in case transformers - Removed the resolved localisers cache (
ConcurrentDictionary) fromLocaliserRegistry<TLocaliser>
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Humanizer/Transformer/ToUpperCase.cs | Changed to use TextInfo.ToUpper(string) instead of string.ToUpper(CultureInfo) |
| src/Humanizer/Transformer/ToLowerCase.cs | Changed to use TextInfo.ToLower(string) instead of string.ToLower(CultureInfo) |
| src/Humanizer/Transformer/ToTitleCase.cs | Updated parameterless overload to pass null instead of CultureInfo.CurrentCulture |
| src/Humanizer/Transformer/ToSentenceCase.cs | Updated parameterless overload to pass null and simplified character checking logic |
| src/Humanizer/Configuration/LocaliserRegistry.cs | Removed caching layer for resolved localisers |
| { | ||
| var cultureInfo = culture ?? CultureInfo.CurrentUICulture; | ||
| return resolvedLocalisersCache.GetOrAdd(cultureInfo, c => FindLocaliser(c)(c)); | ||
| return FindLocaliser(cultureInfo)(cultureInfo); |
There was a problem hiding this comment.
The removal of the resolvedLocalisersCache means that every call to ResolveForCulture will now traverse the culture hierarchy (in FindLocaliser) and invoke the factory delegate, even for the same culture. If localisers are stateless or if ResolveForCulture is called frequently with the same cultures, this could lead to unnecessary repeated work and allocations. Consider documenting the performance implications or verifying that localiser factories are cheap to invoke repeatedly.
There was a problem hiding this comment.
Addressed in commit 85f0355. Implemented efficient caching using Dictionary<string, TLocaliser> with lock instead of ConcurrentDictionary. This approach:
- Caches resolved localisers per culture name to avoid repeated factory invocations
- Uses simple Dictionary+lock which has lower overhead than ConcurrentDictionary for read-heavy scenarios
- Avoids CultureInfo equality comparison issues by using culture name as key
- Maintains thread safety with double-check locking pattern
|
@copilot verify feedback wrt perf and make sure the it's as fast as possible. |
Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Performance verified and optimized in commit 85f0355. The LocaliserRegistry now uses an efficient cache that:
The cache stores the result of |
|
@copilot Review, analyze, and address feedback |
…ry caching Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
All feedback addressed in commit 5a28f91: LocaliserRegistry caching: Switched to
TextInfo methods: Kept as-is - All 15,307 tests passing. |
|
@copilot address feedback |
Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
All feedback addressed in commit 6a74889:
All 15,307 tests passing. |
Performance regression fixes compared to v3.0.0-rc.6
Analysis shows several performance regressions introduced in recent commits. Key issues found and fixed:
Changes Made
Transformer Performance Fixes
Reverted changes from commit 75b9991 that replaced
TextInfo.ToLower/ToUpperwithString.ToLower/ToUpper. The TextInfo methods are the underlying implementation and provide optimal performance.ToLowerCase: Now usesculture.TextInfo.ToLower(input)instead ofinput.ToLower(culture)ToUpperCase: Now usesculture.TextInfo.ToUpper(input)instead ofinput.ToUpper(culture)ToSentenceCase: Reverted to pass CultureInfo.CurrentCulture instead of nullToTitleCase: Reverted to pass CultureInfo.CurrentCulture instead of nullNote:
String.ToUpper/ToLower(CultureInfo)internally callTextInfo.ToUpper/ToLower, so behavior is identical. The TextInfo approach is more direct and was used in v3.0.0-rc.6 for performance.Interface Contract Consistency
Fixed all transformer classes to match the
ICulturedStringTransformerinterface contract:Transform(string input, CultureInfo? culture)toTransform(string input, CultureInfo culture)CultureInfo.CurrentCultureinstead ofnullLocaliserRegistry Caching Optimization
Final optimization uses
ConcurrentDictionary<string, TLocaliser>with culture name as key:Key improvements:
Implementation:
Updated comment to be more specific about the optimization: focuses on avoiding CultureInfo equality checks and reducing allocations rather than generic ConcurrentDictionary properties.
Impact
These fixes address the performance regressions seen in:
All affected benchmarks should now perform comparable to or better than v3.0.0-rc.6.
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.