Skip to content

Instantly share code, notes, and snippets.

@eerhardt
Last active December 1, 2022 02:08
Show Gist options
  • Save eerhardt/8f4f6cd58e3c4ec2879ed3d4e464ec81 to your computer and use it in GitHub Desktop.
Save eerhardt/8f4f6cd58e3c4ec2879ed3d4e464ec81 to your computer and use it in GitHub Desktop.
App size report for NativeAOT ASP.NET app

Key Takeaways

By making changes in the following areas, I believe we can cut somewhere between 6 - 8 MB of size on disk of the 32 MB Albums Linux NativeAOT app.

  1. Compression - 1.1 MB
  2. System.Text.Json - ~2 MB
  3. Regex - 1.2 MB
  4. Linq.Expresions - 1.1 MB
  5. Non-Required Dependencies - ~2 MB

Report

Cataloging the size on disk of the Albums app, shows there are a few places where large dependencies are brought into the app. The following diagram lists the largest dependencies and their respective sizes.

Note: interior boxes are dependencies that are pulled in by the outer boxes.

image

1. HTTPS Support

A large portion of the app (~22%) is taken up by supporting HTTPS/SSL/TLS. This is caused by chained dependencies.

In order to support HTTPS, we need System.Security.Cryptography. Cryptography needs to validate certificates, thus it needs to make outgoing HTTP requests - pulling in HttpClient. HTTP requests can be compressed, so HttpClient pulls in System.IO.Compression - both Brotli and Deflate.

Options

  1. Remove HTTPS support in the web server
    • Using a feature switch set in the .csproj
    • Create a barebones ASP.NET API that doesn't enable HTTPS support by default. Users would need to explicitly ".AddHttps()" to bring it in.
  2. Remove Compression dependency from HttpClient
    • We could do some internal refactoring that allowed the DecompressionHandler to be trimmed if no one set the AutomaticDecompression property (which defaults to None)
    • Another possibility is using a feature switch in the .csproj.
    • Note that this size savings comes from C/C++ code, so removing this dependency only really helps app size of NativeAOT apps, and not "regular" trimmed apps. Regular trimming doesn't trim native code.
    • This savings would also accrue to any NativeAOT app that uses HttpClient without enabling compression.
  3. Remove HttpClient dependency from Cryptography
    • The HTTP needs of Crypto are small. It could be possible to write a minimal HTTP implementation on top of Sockets to meet the needs of Cryptography. This would allow the whole HttpClient implementation to be trimmed from the server - but only if the application doesn't make other outbound HTTP requests.

2. System.Text.Json

System.Text.Json's design pulls in all the features by default, whether the app uses them or not. One example of this is the "converter" design where all converters are registered up-front. The JsonSerializer then uses Reflection to inspect the Types that get passed to it.

The way around this design is to use the JSON Source Generator. However, the source generator needs to be used everywhere in the app in order to get unused JSON features to be trimmed. There can't be any calls to the JsonSerializer without passing the source generated information. Once there is, all the JSON features are brought back into the app, and the size reduction is eliminated.

One proposal here that may help is Expose instance methods in JsonSerializer. This would allow for ASP.NET's internal code to get an instance of JsonSerializer and call Serialize/Deserialize as instance methods. The implementation of how to serialize and deserialize would depend on how the JsonSerializer instance was created. If it was created to only use source generated information, the unused JSON features could be trimmed. This also has the side effect of reducing trimming warnings, because the instance methods to Serialize/Deserialize wouldn't be marked as unsafe. Instead, it depends on how the JsonSerializer instance was constructed.

3. Regex

Routing has a feature named "Route Constraints". One of the options to make a constraint is to add a regular expression to the route, for example: app.MapGet("/posts/{id:regex(^[a-z0-9]+$)}", …). Because these route constraints are inline in the route string, the Regex code needs to always be in the application, in case any of the routes happen to use a regex constraint.

In .NET 7, we added a new feature to Regex: NonBacktracking. This added a considerable amount of code. Depending on the Regex constructor overload used (the one that takes RegexOptions, which ASP.NET Routing uses), this new feature's code will be left in the app, even if the NonBacktracking engine isn't being used.

ASP.NET Routing uses the CultureInvariant and IgnoreCase options when constructing Regex route constraints.

Testing locally, being able to remove the NonBacktracking engine can cut about 1.2 MB of the 1.6 MB of Regex code out of the app size.

UPDATE 11/30/2022

With the latest NativeAOT compiler changes, here are updated numbers for linux-x64 NativeAOT:

Hello World 3.22 MB (3,381,112 bytes)
new Regex("").IsMatch 3.50 MB (3,680,200 bytes)
new Regex("", RegexOptions).IsMatch 4.33 MB (4,545,960 bytes)

Options

  1. Add new Regex construction APIs that allow for some RegexOptions to be used, but also allows for the NonBacktracking engine to be trimmed. For example: Regex.CreateCompiled(pattern, RegexOptions). This API would throw an exception if RegexOptions.NonBacktracking was passed.
  2. Remove the use of RegexOptions. The IgnoreCase option can be specified as part of the pattern as a Pattern Modifier: (?i). However, CultureInvariant cannot be specified this way.
    • One option is to drop support for CultureInvariant from regex route constraints. This affects the Turkish 'i' handling.
    • Another option could be to add a CultureInvariant Pattern Modifier to .NET Regex, so this could be specified without using RegexOptions.
  3. We could remove the Regex route constraints feature either with an API that adds it back, or a feature switch set in the .csproj
  4. Add a feature to the linker/NativeAOT compiler that can see which RegexOptions values are used in the app. And for the enum values that aren't used, it can trim the code branches behind those values (i.e. the NonBacktracking code). This would accrue more size savings elsewhere as well.

4. System.Linq.Expressions

There are 2 places that use System.Linq.Expressions to create dynamic methods:

  1. Minimal APIs to wrap the user's provided Delegate into a RequestDelegate
    • This usage should be eliminated by the proposed Minimal APIs source generator
  2. UseMiddleware<TMiddleware>
    • We could potentially use a source generator here as well, to avoid the SLE usages.
    • We could manually write code in our internal usages of UseMIddleware to remove the SLE usages

UPDATE 11/30/2022

With the latest NativeAOT compiler changes, here are updated numbers for linux-x64 NativeAOT:

Hello World 3.22 MB (3,381,112 bytes)
System.Linq.Expressions.Expression<Func<int, bool>> expr = i => i < 5;
Func<int, bool> deleg = expr.Compile();
4.40 MB (4,615,096 bytes)

5. Non-Required Dependencies

Using the new WebHostBuilder() (v1.0) API instead WebApplication.CreateBuilder() eliminates many dependencies that aren't strictly required. The size savings is about 4.2 MB out of the 32.7 MB above. The assemblies eliminated are:

  • Microsoft.AspNetCore.Authentication.Abstractions.dll
  • Microsoft.AspNetCore.Authentication.Core.dll
  • Microsoft.AspNetCore.Authentication.dll
  • Microsoft.AspNetCore.Authorization.dll
  • Microsoft.AspNetCore.Authorization.Policy.dll
  • Microsoft.AspNetCore.Diagnostics.Abstractions.dll
  • Microsoft.AspNetCore.Diagnostics.dll
  • Microsoft.AspNetCore.dll
  • Microsoft.AspNetCore.HostFiltering.dll
  • Microsoft.AspNetCore.HttpOverrides.dll
  • Microsoft.AspNetCore.Server.IIS.dll
  • Microsoft.AspNetCore.Server.IISIntegration.dll
  • Microsoft.Extensions.Configuration.Binder.dll
  • Microsoft.Extensions.Configuration.CommandLine.dll
  • Microsoft.Extensions.Configuration.Json.dll
  • Microsoft.Extensions.Configuration.UserSecrets.dll
  • Microsoft.Extensions.FileProviders.Composite.dll
  • Microsoft.Extensions.Hosting.dll
  • Microsoft.Extensions.Logging.Configuration.dll
  • Microsoft.Extensions.Logging.Console.dll
  • Microsoft.Extensions.Logging.Debug.dll
  • Microsoft.Extensions.Logging.EventLog.dll
  • Microsoft.Extensions.Logging.EventSource.dll
  • Microsoft.Extensions.Options.ConfigurationExtensions.dll
  • System.Collections.Specialized.dll
  • System.Diagnostics.EventLog.dll
  • System.Security.Principal.Windows.dll

Out of this list, the more critical ones are Authentication/Authorization, Diagnostics, and Console Logging. The Json Configuration (i.e. appsettings.json file) is also very useful to many apps, but not always required.

The work here is to prototype creating a new "Application Builder" API that depends on only the most critical features. Cloud developers using this API would opt-in to the extra features on top of that in their Program.cs file. This allows microservice apps to start off as small as possible, and only when the app needs a feature does it get brought into the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment