Skip to content
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

perf: improve allocations in OwinEnvironment #58917

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

DeagleGross
Copy link
Contributor

@DeagleGross DeagleGross commented Nov 13, 2024

OwinEnvironment allocates a Dictionary<string, FeatureMap> with at least 23 entries of string and FeatureMap objects per request.

In most cases, Owin abstraction is used only to get the data in the specific format (i.e. access the HTTP method via OwinConstants.RequestMethod key), but not to remove \ add entries or rebuild the whole FeatureMap dictionary.

Therefore I am introducing the OwinEntries class (not visible to users), which allocates the static readonly entries dictionary (similar to what existed before) and uses that for the key-value access (so a single allocation per app lifetime against the per-request allocation). However, OwinEnvironment has a rich API to modify the dictionary (i.e. remove entries or clear them completely). Therefore I am doing the following to fully support existing API and dont introduce breaking changes:

  1. for API OwinEnvironment.FeatureMaps returning IDictionary<string, FeatureMap> there is no way to securely determine if the dictionary instance will be changed, and because of that we can't avoid performing the deep-copy of static _entries (same perf loss as existed). Next interaction with OwinEnvironment will be using _contextEntries (request-lifetime) instead of static _entries.
  2. for API OwinEnvironment.Remove(string key) I am using a separate HashSet<string> _deletedKeys to keep track of deleted entries per request lifetime. Even if all original entries are deleted, this is still a more lightweight flow than existed before
  3. for API OwinEnvironment.Clear() I am falling back to _contextEntries usage (request-lifetime)

Note: there are some FeatureMap objects, which are dependent on the HttpContext passed into OwinEnvironment, so I keep them separately in a dedicated Dictionary<string, FeatureMap>. It's contains a single entry so far.

I have added the microbenchmark (see PR), with a code that performs multiple requests using the default HttpContext, and used the new OwinEnvironment implementation against the old one:

    [Benchmark]
    public async Task ProcessMultipleRequests()
    {
        foreach (var i in Enumerable.Range(0, 10000))
        {
            await _requestDelegate(_httpContext);
        }
    }

Benchmark results:

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
OwinRequest_NoOperation (old) 27.46 ms 0.508 ms 0.821 ms 2750.0000 62.5000 - 133 MB
OwinRequest_AccessPorts (old) 29.45 ms 0.577 ms 0.931 ms 2750.0000 62.5000 - 133 MB
OwinRequest_AccessHeaders (old) 28.52 ms 0.565 ms 1.344 ms 2781.2500 93.7500 - 133 MB
OwinRequest_NoOperation (fix) 6.162 ms 0.1208 ms 0.1984 ms 234.3750 - - 12 MB
OwinRequest_AccessPorts (fix) 6.388 ms 0.1219 ms 0.1304 ms 234.3750 - - 12 MB
OwinRequest_AccessHeaders (fix) 6.478 ms 0.0921 ms 0.0817 ms 250.0000 - - 12 MB

(thanks to @deanward81 for the idea)

Closes #58916

@DeagleGross DeagleGross self-assigned this Nov 13, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions label Nov 13, 2024
@DeagleGross
Copy link
Contributor Author

DeagleGross commented Nov 13, 2024

TODO:

  • include DictionaryStringValuesWrapper and DictionaryStringArrayWrapper enumerator improvements
  • improve allocations for port string conversion

@DeagleGross DeagleGross requested review from wtgodbe and a team as code owners November 14, 2024 11:30
@DeagleGross DeagleGross force-pushed the dmkorolev/owin/environment-allocations branch from ebd529e to ebc7e4d Compare November 14, 2024 11:35
@mgravell
Copy link
Member

concept looks solid, nice; added some thoughts

@danmoseley
Copy link
Member

could you please resolve conflict @DeagleGross

@@ -119,4 +121,40 @@ bool IDictionary<string, StringValues>.TryGetValue(string key, out StringValues
value = default(StringValues);
return false;
}

public struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>>, IEnumerator
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this not share as public struct Enumerator : IEnumerator<KeyValuePair<string, T>>, IEnumerator with the one above? ConvertingEnumerator or something

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to ConvertingEnumerator. I dont think I can make a generic impl for inner enumerator of DictionaryStringArrayWrapper and DictionaryStringValuesWrapper. That was your idea, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd have to pass in a Convert delegate to reuse the same one. Maybe not worth the bother.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions Perf
Projects
None yet
Development

Successfully merging this pull request may close these issues.

perf: improve allocations in OwinEnvironment
5 participants