Ready To Run (R2R) to improve the performance of Azure Functions

2024, Jan 11

How can ReadyToRun (R2R) improve the performance of Azure Functions? Are there any visible/quantifiable gains by enabling it?

What is ReadyToRun (R2R)?

ReadyToRun (R2R) is a compilation option that improves the startup performance of .NET Core applications. It is an alternative to the Just-In-Time (JIT) compilation that is enabled by default. R2R compiles the application and all of its dependencies ahead of time, which means that the application doesn't have to be compiled on the fly by the JIT compiler.

Reference: ReadyToRun (R2R)

How to enable ReadyToRun (R2R) in Azure Functions?

To enable ReadyToRun (R2R) in Azure Functions, we can simply modify the default project settings to include:

<PropertyGroup>
    <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>

R2R is only available when you publish an app that targets specific runtime environments (RID) such as Linux x64 or Windows x64, which you can do in 2 ways.

Either via command:

dotnet publish -c Release -r linux-x64

or updating the project settings (this is more common to see it in Azure Functions):

<PropertyGroup>
    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>

Cold Starts improvements with ReadyToRun (R2R)

Cold starts are a common problem in serverless applications. When a function app is running on a consumption plan, the Azure Functions host monitors the functions that are running and keeps idle instances warm. However, if all instances are busy, the host may have to start a new instance to run a function. This is called a cold start.

What R2R does is improve the startup performance of .NET Core applications, which means that the application doesn't have to be compiled on the fly by the JIT compiler. This is a great improvement for cold starts, as the application is already compiled and ready to run.

There are caveats though, as the R2R compilation is done at the time of publishing, so if you have a large number of dependencies, it will take longer to publish the application. This is because the file size is larger as it contains the native code in addition to the IL (Intermediate Language) code.

Comparing Cold Starts with and without ReadyToRun (R2R)

I have created a sample solution that contains 2 projects: ready2run and regular. For both projects, we have the OOTB HTTP Trigger Function that returns the "Welcome to Azure Functions!" message.

[Function("Function1")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
{
    _logger.LogInformation("C# HTTP trigger function processed a request.");

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

    response.WriteString("Welcome to Azure Functions!");

    return response;
}

The difference is on the *.csproj file for ready2run project, where we have R2R enabled:

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
  <PublishReadyToRun>true</PublishReadyToRun>
  <OutputType>Exe</OutputType>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
</PropertyGroup>

The sample is available on PlayGoKids repository

File size

File sizes are slightly different, as the ready2run project is larger due to the R2R compilation.

filesizes

When looking at dotpeek (a decompiler tool), the assembly contains R2R attributes produced by crossgen2 tool. The crossgen2 tool is a .NET Core tool that generates ReadyToRun native images from managed assemblies. More details can be found here.

dotpeek

Testing

I have used the Azure Load Testing service, which is a cloud-based load testing service that enables you to measure the performance of your web application under load. The service can either simulate virtual users accessing your application at the same time or starting multiple Requests per Second (RPS).

Cold starts were simulated on 3 different days, leveraging load testing with the following settings:

load-testing-settings

Both projects were tested with 100 requests per second, with a ramp-up of 1 minute, and a total duration of 1 minute.

The test was executed for each project, and the results are below:

R2R project

ready2run-test1

ready2run-test2

ready2run-test3

Regular project

regular-test1

regular-test2

regular-test3

When comparing the charts, we can see that the response times are very similar, the R2R project has a slightly better performance on the first request, on a cold start. However, afterwards, the performance varies between both projects.

Conclusion

Both function apps that were tested are simple with no project dependencies, and still, R2R brought some gains on cold starts. Would the same happen with a more complex project? I will leave it up to you to test it out, but in theory yes. What do you have to lose?

IMHO It is worth a shot to enable R2R in your Azure Functions.