Skip to content

Conversation

davidfowl
Copy link
Member

@davidfowl davidfowl commented Nov 12, 2020

  • Today there's lots of indirection to figure out which invoker is responsible for executing a particular request based on the ActionContext. The IActionInvokerFactory calls through IActionInvokerProviders and an IActionInvoker is picked as a result of this. The default implementations based this decision on the type of action descriptor (though that's not required) which means its possible to avoid this per request indirection and select the invoker based on the descriptor. The ControllerRequestDelegateFactory creates a RequestDelegate for the endpoint don't go through indirection to find the ControllerActionInvoker.

API

public class MvcOptions
{
+   /// <summary>
+   /// Gets or sets the flag that determines if MVC should use action invoker extensibility. This will allow
+   /// custom <see cref="IActionInvokerFactory"/> and <see cref="IActionInvokerProvider"/> execute during the request pipeline.
+   /// </summary>
+   /// <remarks>This only applies when <see cref="EnableEndpointRouting"/> is true.</remarks>
+   public bool EnableActionInvokers { get; set; }
}

@ghost ghost added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Nov 12, 2020
@davidfowl davidfowl changed the base branch from davidfowl/request-delegate-factory to master November 13, 2020 01:30
- Today there's lots of indirection to figure out which invoker is
responsible for executing a particular request based on the `ActionContext`. The `IActionInvokerFactory` calls through
`IActionInvokerProviders` and an `IActionInvoker` is picked as a result of this. The default implementations based
this decision on the type of action descriptor (though that's not required) which means its possible to
avoid this per request indirection and select the invoker based on the descriptor.
The `ControllerRequestDelegateFactory` creates a `RequestDelegate` for the endpoint don't go
through indirection to find the ControllerActionInvoker.

PS: This code path breaks extensibility for IActionInvoker* types that want
to take over invocation of existing ControllerActionDescriptor since it no longer
calls them in the endpoint routing code path. This may warrant a compatibility switch
@davidfowl davidfowl force-pushed the davidfowl/controller-delegate-factory branch from 29ebb0c to e1bedc9 Compare November 13, 2020 01:57
- This will allow people to turn off the request delegate factory if they were using that extensiblity point before.
@davidfowl davidfowl added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Nov 13, 2020
@ghost
Copy link

ghost commented Nov 13, 2020

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@davidfowl davidfowl marked this pull request as ready for review November 13, 2020 12:04
@davidfowl davidfowl requested a review from halter73 November 13, 2020 17:46
@rynowak
Copy link
Member

rynowak commented Nov 16, 2020

PS: This code path breaks extensibility for IActionInvoker* types that want to take over invocation of existing ControllerActionDescriptor since it no longer calls them in the endpoint routing code path. This may warrant a compatibility switch (this is why it's a draft)

FWIW - I'm not aware of anyone using this for extensiblity for anything real (along with filter providers). There were some projects using it back when our implementation of the invoker was public. Now that this requires some really complicated work, it's just not realistic for someone to extend.


controllerContext.ModelState.MaxAllowedErrors = _maxModelValidationErrors;

var (cacheEntry, filters) = _controllerActionInvokerCache.GetCachedResult(controllerContext);
Copy link
Member

Choose a reason for hiding this comment

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

Another thing to investigate. This is a cache lookup that hits a concurrent dictionary (keyed on ActionDescriptor). Since you're creating a request delegate per-action there's no need for additional caches and all that stuff can get closed over here.

Copy link
Member Author

Choose a reason for hiding this comment

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


namespace Microsoft.AspNetCore.Mvc.Routing
{
internal class ControllerRequestDelegateFactory : IRequestDelegateFactory
Copy link
Member

Choose a reason for hiding this comment

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

Have you tried implementing this yet for Pages? I suspect it's more complex

Copy link
Member Author

Choose a reason for hiding this comment

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

I have a stash for pages and it is more complex.

@pranavkm pranavkm added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Nov 23, 2020
@ghost
Copy link

ghost commented Nov 23, 2020

Hello @davidfowl!

Because this pull request has the auto-merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@davidfowl davidfowl added this to the 6.0-preview1 milestone Nov 24, 2020
@davidfowl davidfowl closed this Nov 24, 2020
@davidfowl davidfowl reopened this Nov 24, 2020
@davidfowl
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@halter73
Copy link
Member

You said you were close to having an IRequestDelegateFactory implementation ready for Razor Pages. Can that be included in this PR?

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

After some offline discussion, I'm fine with looking at the Razor Page change in a separate PR.

@ghost ghost merged commit 584d928 into master Nov 25, 2020
@ghost ghost deleted the davidfowl/controller-delegate-factory branch November 25, 2020 00:32
@halter73
Copy link
Member

FWIW, here are the benchmark results for an early version of this PR hitting the following action using this crank config:

    public class Person
    {
        public string Name { get; set; }
    }

    [ApiController]
    public class EchoController : ControllerBase
    {
        [HttpPost("/EchoController")]
        public Person Post(Person person) => person;
### MapController (master)

`> crank --config .\echo.yml --scenario mapcontroller --profile aspnet-perf-lin --application.options.outputFiles C:\dev\dotnet\aspnetcore\artifacts\bin\Microsoft.AspNetCore.Mvc.Core\Release\net6.0\`
| application           |                       |
| --------------------- | --------------------- |
| CPU Usage (%)         |                    98 |
| Raw CPU Usage (%)     |              1,176.68 |
| Working Set (MB)      |                   190 |
| Build Time (ms)       |                 3,614 |
| Start Time (ms)       |                   380 |
| Published Size (KB)   |                94,141 |
| .NET Core SDK Version | 5.0.100-rc.2.20479.15 |

| load                   |           |
| ---------------------- | --------- |
| CPU Usage (%)          |        39 |
| Raw CPU Usage (%)      |    471.89 |
| Working Set (MB)       |        51 |
| Build Time (ms)        |     4,072 |
| Start Time (ms)        |       107 |
| Published Size (KB)    |    77,080 |
| .NET Core SDK Version  | 3.1.403   |
| First Request (ms)     |       189 |
| Requests               | 1,827,246 |
| Bad responses          |         0 |
| Mean latency (us)      |     2,096 |
| Max latency (us)       |    52,615 |
| Requests/sec           |   122,043 |
| Requests/sec (max)     |   132,081 |
| Read throughput (MB/s) |     20.56 |

### MapControler (davidfowl/fast-path)

`> crank --config .\echo.yml --scenario mapcontroller --profile aspnet-perf-lin --application.options.outputFiles C:\dev\dotnet\aspnetcore\artifacts\bin\Microsoft.AspNetCore.Mvc.Core\Release\net6.0\`

| application           |                       |
| --------------------- | --------------------- |
| CPU Usage (%)         |                    99 |
| Raw CPU Usage (%)     |              1,185.04 |
| Working Set (MB)      |                   186 |
| Build Time (ms)       |                 3,578 |
| Start Time (ms)       |                   372 |
| Published Size (KB)   |                94,141 |
| .NET Core SDK Version | 5.0.100-rc.2.20479.15 |

| load                   |           |
| ---------------------- | --------- |
| CPU Usage (%)          |        42 |
| Raw CPU Usage (%)      |    507.19 |
| Working Set (MB)       |        51 |
| Build Time (ms)        |     4,105 |
| Start Time (ms)        |       106 |
| Published Size (KB)    |    77,080 |
| .NET Core SDK Version  | 3.1.403   |
| First Request (ms)     |       213 |
| Requests               | 2,061,249 |
| Bad responses          |         0 |
| Mean latency (us)      |     1,857 |
| Max latency (us)       |    63,930 |
| Requests/sec           |   137,556 |
| Requests/sec (max)     |   148,030 |
| Read throughput (MB/s) |     23.20 |

A ~7.5% RPS improvement for this action with a small JSON payload.

@halter73
Copy link
Member

@davidfowl pointed out what I measured before removed a lot more stuff than this PR ultimately did. Here are the numbers for the actual PR:

### post-echo-controller (pre-PR)

| application           |          |
| --------------------- | -------- |
| CPU Usage (%)         | 99       |
| Raw CPU Usage (%)     | 1,186.47 |
| Working Set (MB)      | 187      |
| Build Time (ms)       | 3,596    |
| Start Time (ms)       | 383      |
| Published Size (KB)   | 94,119   |
| .NET Core SDK Version | 5.0.100  |


| load                   |           |
| ---------------------- | --------- |
| CPU Usage (%)          | 39        |
| Raw CPU Usage (%)      | 468.89    |
| Working Set (MB)       | 51        |
| Build Time (ms)        | 4,118     |
| Start Time (ms)        | 108       |
| Published Size (KB)    | 77,077    |
| .NET Core SDK Version  | 3.1.404   |
| First Request (ms)     | 249       |
| Requests               | 1,845,112 |
| Bad responses          | 0         |
| Mean latency (us)      | 2,075     |
| Max latency (us)       | 79,749    |
| Requests/sec           | 123,451   |
| Requests/sec (max)     | 133,755   |
| Read throughput (MB/s) | 20.76     |

### post-echo-controller (post-PR)

| application           |          |
| --------------------- | -------- |
| CPU Usage (%)         | 99       |
| Raw CPU Usage (%)     | 1,182.20 |
| Working Set (MB)      | 188      |
| Build Time (ms)       | 3,617    |
| Start Time (ms)       | 384      |
| Published Size (KB)   | 94,119   |
| .NET Core SDK Version | 5.0.100  |


| load                   |           |
| ---------------------- | --------- |
| CPU Usage (%)          | 39        |
| Raw CPU Usage (%)      | 472.26    |
| Working Set (MB)       | 50        |
| Build Time (ms)        | 4,104     |
| Start Time (ms)        | 106       |
| Published Size (KB)    | 77,077    |
| .NET Core SDK Version  | 3.1.404   |
| First Request (ms)     | 242       |
| Requests               | 1,873,259 |
| Bad responses          | 0         |
| Mean latency (us)      | 2,043     |
| Max latency (us)       | 62,319    |
| Requests/sec           | 125,211   |
| Requests/sec (max)     | 134,867   |
| Read throughput (MB/s) | 21.08     |

It looks like there might be a small perf improvement, but it's within the margin of error.

@davidfowl davidfowl added the breaking-change This issue / pr will introduce a breaking change, when resolved / merged. label Dec 14, 2022
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates breaking-change This issue / pr will introduce a breaking change, when resolved / merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants