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

Adds TodosApi app (native AOT "goldilocks" stage 2) #1828

Merged
merged 26 commits into from
Mar 31, 2023

Conversation

DamianEdwards
Copy link
Member

@DamianEdwards DamianEdwards commented Mar 30, 2023

This adds a refactored version of the TrimmedTodo app configured to align with our goals for the native AOT "stage 2" app.

The app uses:

  • Minimal APIs
  • JSON serialization
  • Configuration & logging
  • JWT bearer authentication
  • Npgsql for data access
  • Exception handler middleware with problem details
  • Health checks (with a check for database access and another for JWT configuration)

When published native AOT with the -p:EnableLogging=true property, the exe size is currently 20,135 KB on Windows. Everything except the JWT authentication and data access works in native AOT right now, the latter will start working once the JSON support for IAsyncEnumerable lands.

Native AOT published sizes from Crank runs:

Platform exe size (KB)
win-x64 28,316
linux-x64 31,121

src/BenchmarksApps/TodosApi/appsettings.json Show resolved Hide resolved
src/BenchmarksApps/TodosApi/TodosApi.csproj Outdated Show resolved Hide resolved
src/BenchmarksApps/TodosApi/TodosApi.csproj Outdated Show resolved Hide resolved
src/BenchmarksApps/TodosApi/DataExtensions.cs Outdated Show resolved Hide resolved
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0-preview.3.23177.8" />
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a way to keep this version up to date with daily builds?

cc @sebastienros

Copy link
Member Author

Choose a reason for hiding this comment

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

We can use a wildcard, e.g. 8.0.0-preview.*?

Copy link
Member Author

Choose a reason for hiding this comment

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

Seems there's a $(MicrosoftAspNetCoreAppPackageVersion) property defined, I'll try using that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh it's hard-coded to a preview.1 version 😱

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that might get replaced by crank.... @sebastienros ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I've noticed the projects aren't always set up for ease of local use so I made some tweaks to improve that as part of this PR now, i.e. ensuring there are appropriate package versions set.

Copy link
Member

Choose a reason for hiding this comment

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

You can have a default version set for local development, and fallback to $(MicrosoftAspNetCoreAppPackageVersion) if it's defined. Crank will set it on builds. There is the same one for the runtime.

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 humbly suggest we shouldn't be relying on anything local being manually configured to get a good dev experience. The solution/projects should be openable locally after cloning without getting warnings.

Copy link
Member Author

Choose a reason for hiding this comment

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

@sebastienros are you suggesting we should change all the package version references in the app project files to hard-code a default version (can include a wildcard) but prefer $(MicrosoftAspNetCoreAppPackageVersion) if it's set? It's a shame we can't easily make that more elegant in MSBuild but it will certainly achieve what I think we're looking for.

Copy link
Member

@sebastienros sebastienros Apr 3, 2023

Choose a reason for hiding this comment

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

I think you can have something like

<MicrosoftAspNetCoreAuthenticationJwtBearerVersion>8.0.0-preview.3.23177.8</MicrosoftAspNetCoreAuthenticationJwtBearerVersion>
<MicrosoftAspNetCoreAuthenticationJwtBearerVersion Condition="'$(MicrosoftAspNetCoreAppPackageVersion)' != ''">$(MicrosoftAspNetCoreAppPackageVersion)</MicrosoftAspNetCoreAuthenticationJwtBearerVersion>

This way it will build locally, but use the version aligned with aspnet when using crank.

Copy link
Contributor

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

Looking good!

Just need the yaml stuff and the NuGet package version figured out and I think we are golden

src/BenchmarksApps/TodosApi/appsettings.json Outdated Show resolved Hide resolved
src/BenchmarksApps/TodosApi/appsettings.Development.json Outdated Show resolved Hide resolved
src/BenchmarksApps/TodosApi/TodosApi.csproj Outdated Show resolved Hide resolved
src/BenchmarksApps/TodosApi/Program.cs Outdated Show resolved Hide resolved
src/BenchmarksApps/TodosApi/Program.cs Outdated Show resolved Hide resolved
src/BenchmarksApps/TodosApi/DatabaseHealthCheck.cs Outdated Show resolved Hide resolved
<PropertyGroup Condition="$(TargetFramework) == 'net8.0'">
<MicrosoftAspNetCoreAppPackageVersion>8.0.0-preview.1.23112.2</MicrosoftAspNetCoreAppPackageVersion>
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
<MicrosoftAspNetCoreAppPackageVersion>$(MicrosoftAspNetCoreAppPackageVersion80)</MicrosoftAspNetCoreAppPackageVersion>
Copy link
Member

Choose a reason for hiding this comment

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

Works, but why treat net8 differently with a separate constant?

Copy link
Member Author

@DamianEdwards DamianEdwards Mar 30, 2023

Choose a reason for hiding this comment

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

I added a net8 variant so it could be referenced elsewhere when a TargetFramework is not passed in from the outside (MSBuild order of evaluation, etc.).

Copy link
Contributor

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

LGTM. This looks like a great starting app to work off.

var group = routes.MapGroup("/api/todos");

group.MapGet("/", (NpgsqlDataSource db, CancellationToken ct) => db.QueryAsync<Todo>("SELECT * FROM Todos", ct))
.WithName("GetAllTodos");
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the WithName do? Do we need it?

Copy link
Member Author

Choose a reason for hiding this comment

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

It sets the route name and endpoint name metadata which can be used for route resolution, URL generation, Open API metadata, etc. Not strictly required in this app at the moment as we aren't doing any of those things.

Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
@JamesNK
Copy link
Member

JamesNK commented Mar 31, 2023

LGTM. Interested in getting this merged and seeing RPS with workstation GC on APIs that do some work (auth and/or data access)

@DamianEdwards
Copy link
Member Author

Numbers from a manual run on aspnet-perf environment:

db todosapi_vanilla todosapi_trimmedr2r todosapi_aot todosapi_aot_win
CPU Usage (%) 55 0 0 1 -98.18%
Cores usage (%) 660 6 -99.09% 6 -99.09% 13 -98.03%
Working Set (MB) 457 22 -95.19% 23 -94.97% 23 -94.97%
Build Time (ms) 1,462 1,433 -1.98% 1,438 -1.64% 10,887 +644.66%
Start Time (ms) 6,667 6,642 -0.37% 6,564 -1.54% 6,471 -2.94%
Published Size (KB) 369,845 369,845 0.00% 369,845 0.00% 369,845 0.00%
application todosapi_vanilla todosapi_trimmedr2r todosapi_aot todosapi_aot_win
CPU Usage (%) 66 89 +34.85% 73 +10.61% 25 -62.12%
Cores usage (%) 798 1,067 +33.71% 877 +9.90% 305 -61.78%
Working Set (MB) 143 92 -35.66% 64 -55.24% 64 -55.24%
Private Memory (MB) 404 354 -12.38% 303 -25.00% 48 -88.12%
Build Time (ms) 2,193 19,467 +787.69% 41,259 +1,781.40% 42,156 +1,822.30%
Start Time (ms) 250 169 -32.40% 57 -77.20% 128 -48.80%
Published Size (KB) 96,256 55,266 -42.58% 126,380 +31.30% 159,640 +65.85%
Symbols Size (KB) 56 71 +26.79% 95,259 +170,005.36% 131,324 +234,407.14%
.NET Core SDK Version 8.0.100-preview.3.23178.7 8.0.100-preview.3.23178.7 8.0.100-preview.3.23178.7 8.0.100-preview.3.23178.7
Max CPU Usage (%) 66 89 +34.53% 0 0
Max Working Set (MB) 150 96 -35.96% 0 0
Max GC Heap Size (MB) 32 11 -64.85% 0 0
Size of committed memory by the GC (MB) 46 17 -62.99% 0 0
Max Number of Gen 0 GCs / sec 146.00 54.00 -63.01% 0.00 0.00
Max Number of Gen 1 GCs / sec 84.00 3.00 -96.43% 0.00 0.00
Max Number of Gen 2 GCs / sec 16.00 1.00 -93.75% 0.00 0.00
Max Time in GC (%) 71.00 9.00 -87.32% 0.00 0.00
Max Gen 0 Size (B) 851,600 368,736 -56.70% 0 0
Max Gen 1 Size (B) 9,666,784 1,080,224 -88.83% 0 0
Max Gen 2 Size (B) 27,504,240 4,934,360 -82.06% 0 0
Max LOH Size (B) 87,464 0 0 0
Max POH Size (B) 1,182,944 1,144,816 -3.22% 0 0
Max Allocation Rate (B/sec) 766,292,824 330,571,720 -56.86% 0 0
Max GC Heap Fragmentation 50 36 -27.54% 0 0
# of Assemblies Loaded 115 98 -14.78% 0 0
Max Exceptions (#/s) 470 119,722 +25,372.77% 0 0
Max Lock Contention (#/s) 55 27 -50.91% 0 0
Max ThreadPool Threads Count 26 24 -7.69% 0 0
Max ThreadPool Queue Length 140 215 +53.57% 0 0
Max ThreadPool Items (#/s) 104,825 33,330 -68.20% 0 0
Max Active Timers 232 2 -99.14% 0 0
IL Jitted (B) 566,912 317,286 -44.03% 0 0
Methods Jitted 6,052 2,857 -52.79% 0 0
load todosapi_vanilla todosapi_trimmedr2r todosapi_aot todosapi_aot_win
CPU Usage (%) 25 15 -40.00% 32 +28.00% 24 -4.00%
Cores usage (%) 295 183 -37.97% 385 +30.51% 290 -1.69%
Working Set (MB) 58 48 -17.24% 49 -15.52% 58 0.00%
Private Memory (MB) 358 358 0.00% 358 0.00% 358 0.00%
Build Time (ms) 9,129 0 0 9,313 +2.02%
Start Time (ms) 0 0 0 0
Published Size (KB) 73,161 0 0 73,161 0.00%
Symbols Size (KB) 0 0 0 0
.NET Core SDK Version 7.0.202 0 0 7.0.202
First Request (ms) 166 100 -39.76% 57 -65.66% 70 -57.83%
Requests/sec 64,310 29,647 -53.90% 106,569 +65.71% 70,403 +9.47%
Requests 969,473 446,886 -53.90% 1,606,633 +65.72% 1,062,730 +9.62%
Mean latency (ms) 3.97 8.58 +116.12% 2.39 -39.80% 5.09 +28.21%
Max latency (ms) 24.11 20.11 -16.59% 20.88 -13.40% 66.99 +177.85%
Bad responses 0 446,886 +∞% 1,606,633 +∞% 1,062,730 +∞%
Socket errors 0 0 0 0
Read throughput (MB/s) 32.69 11.31 -65.40% 40.65 +24.35% 24.78 -24.20%
Latency 50th (ms) 4.57 8.47 +85.34% 1.92 -57.99% 2.55 -44.20%
Latency 75th (ms) 5.88 9.51 +61.73% 3.36 -42.86% 8.81 +49.83%
Latency 90th (ms) 6.79 10.11 +48.90% 3.71 -45.36% 13.82 +103.53%
Latency 99th (ms) 8.57 12.65 +47.61% 5.22 -39.09% 21.23 +147.72%

@vonzshik
Copy link

Judging by requests being equal to bad responses, it doesn't seem to work.

@DamianEdwards
Copy link
Member Author

Judging by requests being equal to bad responses, it doesn't seem to work.

For the non-vanilla runs you're right 😞 Hopefully something straightforward.

@DamianEdwards
Copy link
Member Author

@vonzshik ah that's expected actually! Right now JSON serialization doesn't work with IAsyncEnumerable when trimming is enabled. That's going to be fixed by dotnet/runtime#82457

@DamianEdwards
Copy link
Member Author

The JSON support for IAsyncEnumerable is actually in the latest runtime and installer builds, but Npgsql is currently broken until this runtime fix lands in a daily SDK installer build. Once that happens this app and the crank runs should hopefully start working for the timmed and native AOT scenarios.

@DamianEdwards DamianEdwards merged commit b0da256 into main Mar 31, 2023
@DamianEdwards DamianEdwards deleted the damianedwards/stage2-TodosApi branch March 31, 2023 18:11
@vonzshik
Copy link

vonzshik commented Apr 2, 2023

Once that happens this app and the crank runs should hopefully start working for the timmed and native AOT scenarios.

If that was indeed the case, wouldn't non-trimmed also fail? + judging by the first published results (on powerbi), crank does use the runtime with the fix, but there are still errors.

@DamianEdwards
Copy link
Member Author

@vonzshik there are two issues. The first is that when trimming or native AOT is enabled, the JSON source generator doesn't support IAsyncEnumerable. The second is the runtime issue with impacting Npgsql itself which I understand is fixed in latest runtime builds. I'll dig in a bit more tomorrow to get a clearer picture of which versions are being used and to confirm the actual exceptions occurring in the runs.

@DamianEdwards
Copy link
Member Author

Seems there's an issue with the runtime support for JSON serializing IAsyncEnumerable that was added in preview.3. Issue at dotnet/runtime#84260

@vonzshik
Copy link

vonzshik commented Apr 3, 2023

I suppose we change to IEnnumerable for now and hopefully everything works?

@DamianEdwards
Copy link
Member Author

@vonzshik I'm inclined to leave it as is for now and focus on getting the underlying issue fixed (seems it's actually ASP.NET Core that needs a code change to fix this).

@vonzshik
Copy link

vonzshik commented Apr 3, 2023

Well then, let's pray that there are no other issues and fixing that isn't going to take long...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants