Skip to content

Commit 67b0782

Browse files
authored
Snapstart Minimal API Performance Improvements (#2010)
* Add AddAWSLambdaBeforeSnapshotRequest to support warming up the asp.net/lambda pipelines automatically during BeforeSnapshot callback. * inline SnapstartHelperLambdaRequest * add auto version change * add support for additional LambdaEventSource * update documentation * Fix bugs in LambdaSnapstartExecuteRequestsBeforeSnapshotHelper and add unit tests * removed duplicate reference to snk * update AddAWSLambdaBeforeSnapshotRequest() to support AOT by optimizing json serialization, adjust #if blocks and fix simple warnings * remove requirement that Amazon.Lambda.RuntimeSupport make InternalsVisibileTo Amazon.Lambda.AspNetCorServer.Hosting * migrate extension method to use HttpRequestMethods instead of collecting calls to HttpClient * Add Snapstart helper to AbstractAspNetCoreFunction * rename RegisterBeforeSnapshotRequest to GetBeforeSnapshotRequests * optimize HttpRequestMessageSerializer and remove AspNetCoreServers's dependency on RuntimeSupport * Update the Minimal API extension method to use GetBeforeSnapshotRequests. * Add additional test for asp.net workloads * improve documentation * impvoe unit tests
1 parent 9df1fc3 commit 67b0782

16 files changed

+616
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Amazon.Lambda.AspNetCoreServer.Hosting",
5+
"Type": "Patch",
6+
"ChangelogMessages": [
7+
"Add overrideable method GetBeforeSnapshotRequests() and AddAWSLambdaBeforeSnapshotRequest() extension method to support warming up the asp.net/lambda pipelines automatically during BeforeSnapshot callback."
8+
]
9+
}
10+
]
11+
}

Libraries/Libraries.sln

+7
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DynamoDBEvent
139139
EndProject
140140
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests", "test\Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests\Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests.csproj", "{074DB940-82BA-47D4-B888-C213D4220A82}"
141141
EndProject
142+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.AspNetCoreServer.Hosting.Tests", "test\Amazon.Lambda.AspNetCoreServer.Hosting.Tests\Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj", "{D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}"
143+
EndProject
142144
Global
143145
GlobalSection(SolutionConfigurationPlatforms) = preSolution
144146
Debug|Any CPU = Debug|Any CPU
@@ -381,6 +383,10 @@ Global
381383
{074DB940-82BA-47D4-B888-C213D4220A82}.Debug|Any CPU.Build.0 = Debug|Any CPU
382384
{074DB940-82BA-47D4-B888-C213D4220A82}.Release|Any CPU.ActiveCfg = Release|Any CPU
383385
{074DB940-82BA-47D4-B888-C213D4220A82}.Release|Any CPU.Build.0 = Release|Any CPU
386+
{D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
387+
{D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Debug|Any CPU.Build.0 = Debug|Any CPU
388+
{D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Release|Any CPU.ActiveCfg = Release|Any CPU
389+
{D61CBB71-17AB-4EC2-8C6A-70E9D7C60526}.Release|Any CPU.Build.0 = Release|Any CPU
384390
EndGlobalSection
385391
GlobalSection(SolutionProperties) = preSolution
386392
HideSolutionNode = FALSE
@@ -449,6 +455,7 @@ Global
449455
{A699E183-D0D4-4F26-A0A7-88DA5607F455} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
450456
{3400F4E9-BA12-4D3D-9BA1-2798AA8D0AFC} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
451457
{074DB940-82BA-47D4-B888-C213D4220A82} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
458+
{D61CBB71-17AB-4EC2-8C6A-70E9D7C60526} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
452459
EndGlobalSection
453460
GlobalSection(ExtensibilityGlobals) = postSolution
454461
SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal;
7+
8+
#if NET8_0_OR_GREATER
9+
/// <summary>
10+
/// Helper class for storing Requests for
11+
/// <see cref="ServiceCollectionExtensions.AddAWSLambdaBeforeSnapshotRequest"/>
12+
/// </summary>
13+
internal class GetBeforeSnapshotRequestsCollector
14+
{
15+
public HttpRequestMessage? Request { get; set; }
16+
}
17+
#endif

Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs

+55-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
using Amazon.Lambda.AspNetCoreServer.Internal;
1+
using System.Diagnostics.CodeAnalysis;
2+
using Amazon.Lambda.AspNetCoreServer.Internal;
23
using Amazon.Lambda.Core;
34
using Amazon.Lambda.RuntimeSupport;
4-
using Amazon.Lambda.Serialization.SystemTextJson;
55
using Microsoft.AspNetCore.Hosting.Server;
6-
using Microsoft.AspNetCore.Mvc.ApplicationParts;
76
using Microsoft.Extensions.DependencyInjection;
87

98
namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal
@@ -16,7 +15,8 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal
1615
/// </summary>
1716
public abstract class LambdaRuntimeSupportServer : LambdaServer
1817
{
19-
IServiceProvider _serviceProvider;
18+
private readonly IServiceProvider _serviceProvider;
19+
2020
internal ILambdaSerializer Serializer;
2121

2222
/// <summary>
@@ -26,6 +26,7 @@ public abstract class LambdaRuntimeSupportServer : LambdaServer
2626
public LambdaRuntimeSupportServer(IServiceProvider serviceProvider)
2727
{
2828
_serviceProvider = serviceProvider;
29+
2930
Serializer = serviceProvider.GetRequiredService<ILambdaSerializer>();
3031
}
3132

@@ -41,6 +42,7 @@ public override Task StartAsync<TContext>(IHttpApplication<TContext> application
4142
base.StartAsync(application, cancellationToken);
4243

4344
var handlerWrapper = CreateHandlerWrapper(_serviceProvider);
45+
4446
var bootStrap = new LambdaBootstrap(handlerWrapper);
4547
return bootStrap.RunAsync();
4648
}
@@ -83,14 +85,30 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP
8385
/// </summary>
8486
public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction
8587
{
88+
#if NET8_0_OR_GREATER
89+
private readonly IEnumerable<GetBeforeSnapshotRequestsCollector> _beforeSnapshotRequestsCollectors;
90+
#endif
91+
8692
/// <summary>
8793
/// Create instances
8894
/// </summary>
8995
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
9096
public APIGatewayHttpApiV2MinimalApi(IServiceProvider serviceProvider)
9197
: base(serviceProvider)
9298
{
99+
#if NET8_0_OR_GREATER
100+
_beforeSnapshotRequestsCollectors = serviceProvider.GetServices<GetBeforeSnapshotRequestsCollector>();
101+
#endif
93102
}
103+
104+
#if NET8_0_OR_GREATER
105+
protected override IEnumerable<HttpRequestMessage> GetBeforeSnapshotRequests()
106+
{
107+
foreach (var collector in _beforeSnapshotRequestsCollectors)
108+
if (collector.Request != null)
109+
yield return collector.Request;
110+
}
111+
#endif
94112
}
95113
}
96114

@@ -124,14 +142,30 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP
124142
/// </summary>
125143
public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction
126144
{
145+
#if NET8_0_OR_GREATER
146+
private readonly IEnumerable<GetBeforeSnapshotRequestsCollector> _beforeSnapshotRequestsCollectors;
147+
#endif
148+
127149
/// <summary>
128150
/// Create instances
129151
/// </summary>
130152
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
131153
public APIGatewayRestApiMinimalApi(IServiceProvider serviceProvider)
132154
: base(serviceProvider)
133155
{
156+
#if NET8_0_OR_GREATER
157+
_beforeSnapshotRequestsCollectors = serviceProvider.GetServices<GetBeforeSnapshotRequestsCollector>();
158+
#endif
134159
}
160+
161+
#if NET8_0_OR_GREATER
162+
protected override IEnumerable<HttpRequestMessage> GetBeforeSnapshotRequests()
163+
{
164+
foreach (var collector in _beforeSnapshotRequestsCollectors)
165+
if (collector.Request != null)
166+
yield return collector.Request;
167+
}
168+
#endif
135169
}
136170
}
137171

@@ -165,14 +199,30 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP
165199
/// </summary>
166200
public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction
167201
{
202+
#if NET8_0_OR_GREATER
203+
private readonly IEnumerable<GetBeforeSnapshotRequestsCollector> _beforeSnapshotRequestsCollectors;
204+
#endif
205+
168206
/// <summary>
169207
/// Create instances
170208
/// </summary>
171209
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
172210
public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider)
173211
: base(serviceProvider)
174212
{
213+
#if NET8_0_OR_GREATER
214+
_beforeSnapshotRequestsCollectors = serviceProvider.GetServices<GetBeforeSnapshotRequestsCollector>();
215+
#endif
216+
}
217+
218+
#if NET8_0_OR_GREATER
219+
protected override IEnumerable<HttpRequestMessage> GetBeforeSnapshotRequests()
220+
{
221+
foreach (var collector in _beforeSnapshotRequestsCollectors)
222+
if (collector.Request != null)
223+
yield return collector.Request;
175224
}
225+
#endif
176226
}
177227
}
178-
}
228+
}

Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Amazon.Lambda.AspNetCoreServer.Hosting;
1+
using Amazon.Lambda.AspNetCoreServer.Hosting;
22
using Amazon.Lambda.AspNetCoreServer.Internal;
33
using Amazon.Lambda.AspNetCoreServer.Hosting.Internal;
44
using Amazon.Lambda.Core;
@@ -88,6 +88,62 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser
8888
return services;
8989
}
9090

91+
#if NET8_0_OR_GREATER
92+
/// <summary>
93+
/// Adds a <see cref="HttpRequestMessage"/>> that will be used to invoke
94+
/// Routes in your lambda function in order to initialize the ASP.NET Core and Lambda pipelines
95+
/// during <see cref="SnapshotRestore.RegisterBeforeSnapshot"/>. This improves the performance gains
96+
/// offered by SnapStart.
97+
/// <para />
98+
/// <paramref name="beforeSnapStartRequest"/> must have a relative
99+
/// <see cref="HttpRequestMessage.RequestUri"/> and the <see cref="HttpRequestMessage.Content"/> only supports
100+
/// text based payload.
101+
/// <para />.
102+
/// Be aware that this will invoke your applications function handler code
103+
/// multiple times so that .NET runtime sees this code is a hot path and should be optimized.
104+
/// <para />
105+
/// When the function handler is called as part of SnapStart warm up, the instance will use a
106+
/// mock <see cref="ILambdaContext"/>, which will not be fully populated.
107+
/// <para />
108+
/// This method automatically registers with <see cref="SnapshotRestore.RegisterBeforeSnapshot"/>.
109+
/// <para />
110+
/// This method can be called multiple times to register additional urls.
111+
/// <para />
112+
/// Example:
113+
/// <para />
114+
/// <code>
115+
/// <![CDATA[
116+
/// // Example Minimal Api
117+
/// var builder = WebApplication.CreateSlimBuilder(args);
118+
///
119+
/// builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);
120+
///
121+
/// // Initialize asp.net pipeline before Snapshot
122+
/// builder.Services.AddAWSLambdaBeforeSnapshotRequest(
123+
/// new HttpRequestMessage(HttpMethod.Get, "/test")
124+
/// );
125+
///
126+
/// var app = builder.Build();
127+
///
128+
/// app.MapGet("/test", () => "Success");
129+
///
130+
/// app.Run();
131+
/// ]]>
132+
/// </code>
133+
/// </summary>
134+
/// <param name="services"></param>
135+
/// <param name="beforeSnapStartRequest"></param>
136+
public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IServiceCollection services, HttpRequestMessage beforeSnapStartRequest)
137+
{
138+
services.AddSingleton(new GetBeforeSnapshotRequestsCollector
139+
{
140+
Request = beforeSnapStartRequest
141+
});
142+
143+
return services;
144+
}
145+
#endif
146+
91147
private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSource eventSource, Action<HostingOptions>? configure, out HostingOptions? hostingOptions)
92148
{
93149
hostingOptions = null;

Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs

+81
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System;
99
using System.Collections.Generic;
1010
using System.IO;
11+
using System.Linq;
12+
using System.Net.Http;
1113
using System.Reflection;
1214
using System.Text;
1315
using System.Threading.Tasks;
@@ -120,6 +122,8 @@ protected AbstractAspNetCoreFunction(IServiceProvider hostedServices)
120122
_hostServices = hostedServices;
121123
_server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer;
122124
_logger = ActivatorUtilities.CreateInstance<Logger<AbstractAspNetCoreFunction<TREQUEST, TRESPONSE>>>(this._hostServices);
125+
126+
AddRegisterBeforeSnapshot();
123127
}
124128

125129
/// <summary>
@@ -251,6 +255,47 @@ protected virtual IHostBuilder CreateHostBuilder()
251255
return builder;
252256
}
253257

258+
#if NET8_0_OR_GREATER
259+
/// <summary>
260+
/// Return one or more <see cref="HttpRequestMessage"/>s that will be used to invoke
261+
/// Routes in your lambda function in order to initialize the ASP.NET Core and Lambda pipelines
262+
/// during <see cref="SnapshotRestore.RegisterBeforeSnapshot"/>,
263+
/// improving the performance gains offered by SnapStart.
264+
/// <para />
265+
/// The returned <see cref="HttpRequestMessage"/>s must have a relative
266+
/// <see cref="HttpRequestMessage.RequestUri"/> and the <see cref="HttpRequestMessage.Content"/> only supports
267+
/// text based payload.
268+
/// <para />.
269+
/// Be aware that this will invoke your applications function handler code
270+
/// multiple times. Additionally, it uses a mock <see cref="ILambdaContext"/>
271+
/// which may not be fully populated.
272+
/// <para />
273+
/// This method automatically registers with <see cref="SnapshotRestore.RegisterBeforeSnapshot"/>.
274+
/// <para />
275+
/// If SnapStart is not enabled, then this method is never invoked.
276+
/// <para />
277+
/// Example:
278+
/// <para />
279+
/// <code>
280+
/// <![CDATA[
281+
/// public class HttpV2LambdaFunction : APIGatewayHttpApiV2ProxyFunction<Startup>
282+
/// {
283+
/// protected override IEnumerable<HttpRequestMessage> RegisterBeforeSnapshotRequest() =>
284+
/// [
285+
/// new HttpRequestMessage
286+
/// {
287+
/// RequestUri = new Uri("/api/ExampleSnapstartInit"),
288+
/// Method = HttpMethod.Get
289+
/// }
290+
/// ];
291+
/// }
292+
/// ]]>
293+
/// </code>
294+
/// </summary>
295+
protected virtual IEnumerable<HttpRequestMessage> GetBeforeSnapshotRequests() =>
296+
Enumerable.Empty<HttpRequestMessage>();
297+
#endif
298+
254299
private protected bool IsStarted
255300
{
256301
get
@@ -259,6 +304,40 @@ private protected bool IsStarted
259304
}
260305
}
261306

307+
private void AddRegisterBeforeSnapshot()
308+
{
309+
#if NET8_0_OR_GREATER
310+
311+
Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () =>
312+
{
313+
var beforeSnapstartRequests = GetBeforeSnapshotRequests();
314+
315+
foreach (var httpRequest in beforeSnapstartRequests)
316+
{
317+
var invokeTimes = 5;
318+
319+
var request = await HttpRequestMessageConverter.ConvertToLambdaRequest<TREQUEST>(httpRequest);
320+
321+
InvokeFeatures features = new InvokeFeatures();
322+
(features as IItemsFeature).Items = new Dictionary<object, object>();
323+
(features as IServiceProvidersFeature).RequestServices = _hostServices;
324+
325+
MarshallRequest(features, request, new SnapStartEmptyLambdaContext());
326+
327+
var context = CreateContext(features);
328+
329+
for (var i = 0; i < invokeTimes; i++)
330+
{
331+
var lambdaContext = new SnapStartEmptyLambdaContext();
332+
333+
await ProcessRequest(lambdaContext, context, features);
334+
}
335+
}
336+
});
337+
338+
#endif
339+
}
340+
262341
/// <summary>
263342
/// Should be called in the derived constructor
264343
/// </summary>
@@ -284,6 +363,8 @@ protected void Start()
284363
"instead of ConfigureWebHostDefaults to make sure the property Lambda services are registered.");
285364
}
286365
_logger = ActivatorUtilities.CreateInstance<Logger<AbstractAspNetCoreFunction<TREQUEST, TRESPONSE>>>(this._hostServices);
366+
367+
AddRegisterBeforeSnapshot();
287368
}
288369

289370
/// <summary>

0 commit comments

Comments
 (0)