# Diagnostics Allocations in ASP.NET Core

## Object Allocations

ASP.NET Core's Kestrel is optimized for high performance and scale. To achieve high performance, it reduces heap allocations by either pooling large objects or by allocating `struct`s on the stack. By reducing allocations, the GC has less work to do. As pooled objects get promoted to Gen2 generation, the most common collections (Gen0 and Gen1) become cheaper as they contain fewer objects to handle. However, pooling is not entirely free:

- it increases the Gen2 size and its corresponding collections
- cross-generation references may require tracking references from Gen2 regions pointing to lower generation regions (for example, when a pooled object contains a reference to a newly allocated object).

One example is type `Http2Stream` (or `Http3Stream`), which corresponds to a request-response pair in a connection. As such objects are large, they are typically pooled. These objects may have a reference to the corresponding `HttpContext`, which should be also pooled, otherwise it incurs an allocation or a cross-generation reference.

## Measuring Allocations per Request

We can measure allocations that correspond to a single request-response pair. The simplest way to correlate allocations to requests is by sending a large number of requests (say 20,000) and measuring each allocation. The most commonly allocated types are `string`, `byte[]`, or `Task`, which should not be surprising in a web application. When objects of a given type are allocated at positive integer multiples of the requests, it reveals that at least one such object is allocated while serving each request.

While reviewing allocation metrics, `string`s can be easily overlooked:

- Applications and frameworks typically use many `string`s (for example, HTTP protocol is `string`-based)
- When `string`s are overlooked, one might miss Large Object Heap (LOH) allocations too, and those can cause performance issues.

## HexConverter

During one such measurement, I noticed an interesting allocation in ASP.NET Core that surprised me, given the general efficiency of the framework. For each HTTP/2 request, a `string` object gets allocated by `HexConverter.ToString`.

The allocation happens in the stack trace shown below, which reveals that it belongs to a diagnostics/metrics/logging code path.

```
System.Private.CoreLib.dll!System.HexConverter.ToString(System.ReadOnlySpan<byte> bytes, System.HexConverter.Casing casing) Line 116	C#
System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.ActivityTraceId.CreateFromBytes(System.ReadOnlySpan<byte> idData)
System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.ActivityTraceId.CreateRandom()
System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.Activity.GenerateW3CId()
System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.Activity.Start()
Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.HostingApplicationDiagnostics.StartActivity(Microsoft.AspNetCore.Http.HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener)
Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.HostingApplicationDiagnostics.BeginRequest(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Hosting.HostingApplication.Context context)
Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.HostingApplication.CreateContext(Microsoft.AspNetCore.Http.Features.IFeatureCollection contextFeatures)
```

A `string` is allocated for diagnostic traces for an object typed as `Activity`. 

With a closer look at the [code](https://github.com/dotnet/aspnetcore/blob/177c938deb9f9ef5593d42401682c3ae6feca2d8/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L107), it turns out that the `Activity` object is allocated, if the `_activitySource` is subscribed to, logging is enabled, or `_diagnosticListener` is subscribed to (in .NET 10 Preview 7).

## Disable HexConverter Allocation

Both with the regular `WebApplicationBuilder` or Slim builder, the allocation occurs by default. The stacktrace reveals that the allocation is made by the host, rather than the server itself. To remove this allocation, all 3 above conditions should be disabled.

In a default application this can be done by disabling the logging with the services in **Program.cs**:

```csharp
builder.Services.AddLogging(logBuilder =>
{
    logBuilder.ClearProviders();
});
```

However, this solution has a downside: there are no more log messages output by the application. A different approach could be to only disable the diagnostics corresponding to these log messages. This can be done by setting `"Microsoft.AspNetCore.Hosting.Diagnostics": "None"` in the **appsettings.json**:

```json
"Logging": {
  "LogLevel": {
    "Microsoft.AspNetCore.Hosting.Diagnostics": "None"
  },
  "EventLog": {
    "LogLevel": {
      "Microsoft.AspNetCore.Hosting.Diagnostics": "None"
    }
  }
}
```

## Conclusion

This investigation reveals how even small diagnostic operations — like creating an `Activity` and using `System.HexConverter.ToString` — can lead to per-request `string` allocations. These are not a problem for most applications (as they likely allocate significantly more objects), moreover it is probably the preferred setting for good telemetry and observability. However, while rich diagnostics and automatic tracing enhance system observability, they come at the cost of increased allocations and potential cross-generation overhead. These are more apparent for performance-critical applications. This post showed, how such allocations may be disabled in ASP.NET Core at the cost of reduced observability.