Optimize ToLowerCase, PrependArticleSuffix, and ToQuantity for .NET 8+ performance and fix thread safety issues#1635
Conversation
Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
…ocations Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
…ng methods to avoid CA warnings Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
|
@copilot make sure the entire thing is threadsafe |
… add locking to Configurator, add XML doc warnings Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Completed comprehensive thread safety review of the entire codebase. Fixed in commit f57d329: Critical fixes:
Verified thread-safe:
All 15,307 tests pass. The registries now use FrozenDictionary for optimal read performance in multi-threaded scenarios. |
|
@copilot Are there other cases where string.Create will improve perf? |
After comprehensive analysis, the main opportunities for ToQuantityExtensions (lines 79, 82, 109) - These use However, most other string operations are already optimized:
The Would you like me to optimize |
|
@copilot yes, and add a benchmark for it |
Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Done in commit 725ce3d. Added:
All 15,307 tests pass. The optimization applies to both |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull Request Overview
This PR implements performance optimizations across string transformation and registry initialization operations. The changes focus on reducing allocations and improving lookup performance in hot paths.
- Optimized string transformations with more efficient culture handling and expression-bodied members
- Introduced
FrozenDictionaryfor faster read-only lookups inLocaliserRegistry - Added
string.Createoptimization for string concatenation inToQuantityExtensionsandArticlePrefixSort
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Humanizer/Transformer/ToUpperCase.cs | Simplified to use string.ToUpper() directly instead of TextInfo.ToUpper() and converted to expression-bodied members |
| src/Humanizer/Transformer/ToLowerCase.cs | Simplified to use string.ToLower() directly instead of TextInfo.ToLower() and converted to expression-bodied members |
| src/Humanizer/Transformer/ToTitleCase.cs | Updated to pass CultureInfo.CurrentCulture instead of null for consistency |
| src/Humanizer/Transformer/ToSentenceCase.cs | Updated to pass CultureInfo.CurrentCulture instead of null for consistency |
| src/Humanizer/ToQuantityExtensions.cs | Added ConcatWithSpace helper with string.Create optimization for NET6+ |
| src/Humanizer/Configuration/LocaliserRegistry.cs | Introduced lazy FrozenDictionary initialization for better read performance with registration locking |
| src/Humanizer/Configuration/Configurator.cs | Added thread-safety with volatile fields, locks, and documentation warnings for strategy properties |
| src/Humanizer/ArticlePrefixSort.cs | Refactored article detection logic with RearrangeArticle helper using string.Create for NET6+ |
| src/Benchmarks/ToQuantityBenchmarks.cs | Added benchmark suite to measure ToQuantity performance improvements |
| public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy(); | ||
|
|
||
| /// <summary> | ||
| /// The strategy to be used for DateTimeOffset.Humanize | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This property should be set only once during application startup before any humanization operations occur. | ||
| /// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior. | ||
| /// </remarks> | ||
| public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy { get; set; } = new DefaultDateTimeOffsetHumanizeStrategy(); |
There was a problem hiding this comment.
The documentation warns about thread-safety issues but the property setter on line 97 doesn't prevent concurrent writes. If thread-safety is a concern, consider either making these properties immutable after first use (similar to the LocaliserRegistry pattern) or documenting that the application is responsible for synchronization during configuration.
| public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy(); | |
| /// <summary> | |
| /// The strategy to be used for DateTimeOffset.Humanize | |
| /// </summary> | |
| /// <remarks> | |
| /// This property should be set only once during application startup before any humanization operations occur. | |
| /// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior. | |
| /// </remarks> | |
| public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy { get; set; } = new DefaultDateTimeOffsetHumanizeStrategy(); | |
| private static volatile IDateTimeHumanizeStrategy _dateTimeHumanizeStrategy = new DefaultDateTimeHumanizeStrategy(); | |
| /// <summary> | |
| /// The strategy to be used for DateTime.Humanize | |
| /// </summary> | |
| /// <remarks> | |
| /// This property can only be set once during application startup before any humanization operations occur. | |
| /// Any attempt to set it again will result in an InvalidOperationException. | |
| /// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior. | |
| /// </remarks> | |
| public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy | |
| { | |
| get => _dateTimeHumanizeStrategy; | |
| set | |
| { | |
| if (_dateTimeHumanizeStrategy != null && !ReferenceEquals(_dateTimeHumanizeStrategy, new DefaultDateTimeHumanizeStrategy())) | |
| throw new InvalidOperationException("DateTimeHumanizeStrategy can only be set once during application startup."); | |
| _dateTimeHumanizeStrategy = value ?? throw new ArgumentNullException(nameof(value)); | |
| } | |
| } | |
| private static volatile IDateTimeOffsetHumanizeStrategy _dateTimeOffsetHumanizeStrategy = new DefaultDateTimeOffsetHumanizeStrategy(); | |
| /// <summary> | |
| /// The strategy to be used for DateTimeOffset.Humanize | |
| /// </summary> | |
| /// <remarks> | |
| /// This property can only be set once during application startup before any humanization operations occur. | |
| /// Any attempt to set it again will result in an InvalidOperationException. | |
| /// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior. | |
| /// </remarks> | |
| public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy | |
| { | |
| get => _dateTimeOffsetHumanizeStrategy; | |
| set | |
| { | |
| if (_dateTimeOffsetHumanizeStrategy != null && !ReferenceEquals(_dateTimeOffsetHumanizeStrategy, new DefaultDateTimeOffsetHumanizeStrategy())) | |
| throw new InvalidOperationException("DateTimeOffsetHumanizeStrategy can only be set once during application startup."); | |
| _dateTimeOffsetHumanizeStrategy = value ?? throw new ArgumentNullException(nameof(value)); | |
| } | |
| } |
|
@copilot Address the thread-safety issues found in review while maintaining performance goals |
… double-checked pattern Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Fixed in commit faca477. Addressed all thread-safety issues identified in review: LocaliserRegistry improvements:
Configurator strategy properties:
The double-checked locking pattern provides optimal performance (lock-free reads after initialization) while ensuring thread-safety. All 15,307 tests pass. |
Performance Optimization - Complete ✓
Issues Identified
Implemented Changes
Performance Optimizations
string.Create(NET6+)string.Create(NET6+) and comprehensive benchmarksThread Safety Fixes (Enhanced)
volatilekeyword tofrozenLocalisersfield for memory visibilityFindLocaliserto prevent race conditionsRegistermethods with locks to prevent concurrent registrationlocalisersBuilderis never accessed concurrently with freezing operationBenchmark Results ✓
Thread Safety Implementation Details
LocaliserRegistry Double-Checked Locking
This pattern ensures:
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.