-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce allocations in MediaTokenService and improve performance #10941
Reduce allocations in MediaTokenService and improve performance #10941
Conversation
@@ -15,7 +18,7 @@ public class MediaTokenService : IMediaTokenService | |||
private const string TokenCacheKeyPrefix = "MediaToken:"; | |||
private readonly IMemoryCache _memoryCache; | |||
|
|||
private readonly HashSet<string> _knownCommands = new HashSet<string>(); | |||
private readonly HashSet<string> _knownCommands = new(12); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
12 is what default Orchard setup seems to have
} | ||
|
||
// Using the command values as a key retrieve from cache | ||
var queryStringTokenKey = CreateQueryStringTokenKey(processingCommands.Values); | ||
var queryStringTokenKey = CreateQueryStringTokenKey(processingCommands); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.Values
allocates new ValueCollection
, no need for that so let's just pass the dictionary which has efficient enumeration
return AddQueryString(path.AsSpan(0, pathIndex), processingCommands); | ||
} | ||
|
||
private void ParseQuery( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as in QueryHelpers
, but produces two dictionaries based on whether known commands matches
/// <summary> | ||
/// Specialized version with fast enumeration and no StringValues string allocation. | ||
/// </summary> | ||
private static string CreateQueryStringTokenKey(Dictionary<string, StringValues> values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a bit of duplication, but with concrete type overload we get fast struct enumerator instead of one wrapped via interface
{ | ||
entry.SlidingExpiration = TimeSpan.FromHours(5); | ||
{ | ||
if (!_memoryCache.TryGetValue(queryStringTokenKey, out var result)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
old code had capturing closure as extension method took a Func which needed outside context, now using methods actually present in IMemoryCache
to prevent that
/// Custom version of <see cref="QueryHelpers.AddQueryString(string,string,string)"/> that takes our pre-built | ||
/// dictionary, uri as ReadOnlySpan<char> and uses ZString. Otherwise same logic. | ||
/// </summary> | ||
private static string AddQueryString( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this ones copes with ReadOnlySpan<char>
and uses ZString
instead of allocating StringBuilder
Indeed :) We aren't quite there with .NET 6 only yet, still multi targetting. Due to drop 3.1 and 5 after the next release (due shortly)
No, it's just Scoped until you find value in making it a Singleton. |
8b8676d
to
8a0e8b2
Compare
I did a workaround with #if/#else that's the slower path without the zero-allocating query params traversal.
Thanks for the confirmation. I changed it to singleton and tested in PR with singleton instance, which gave 930ns -> 700ns change. Quite big difference I'd say just by singletoning the instance. |
out Dictionary<string, StringValues> processingCommands, | ||
out Dictionary<string, StringValues> otherCommands) | ||
{ | ||
var parsed = QueryHelpers.ParseQuery(queryString); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
basically old logic which generates at least two dictionaries
* keep query StringValues as-is instead of ToString as string * use single pass to handle known commands and other commands * allocate static command arrays for processors * use spans and stack alloc for hash calculation * make MediaTokenService singleton
8a0e8b2
to
9756671
Compare
I tweaked existing test case to test both with known and unknown commands and it now checks for complete generated string, results didn't change between main and this branch. |
One has to have hobbies, right? Felt like a fun exercise as Orchard can use all .NET 6 goodness.
MediaTokenService
does some unnecessary allocations and is called on every request which has images served.tweak constructor to iterate array, DI gives array and it's fastest to traverse - constructor takes considerable amount of time (see scope note below)ToString
StringValues
, keep them as-is as in dictionary once builtQueryHelpers
usesGetHash
not to allocation anonymous closure each time by usingTry
/Set
construct, uses same logic that extension methodGetOrCreate
usesAddQueryString
method that understands spans and concrete dictionary typeMediaTokenService
seems to be scoped service and thus known commands need to be initialized for every request, is this a tenancy things and cannot be made singleton?Benchmarks
Before
After
Now testing with singleton - unlike main branch!