src/WebJobs.Script.WebHost/Middleware/ClrOptimizationMiddleware.cs (74 lines of code) (raw):

// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; using System.Runtime; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Microsoft.Azure.WebJobs.Script.WebHost.Middleware { /// <summary> /// A middleware responsible for Optimizing CLR settings like GC to help with cold start /// </summary> internal sealed class ClrOptimizationMiddleware { // This value is calculated based on prod profiles across all languages observed for an extended period of time. // This value is just a best effort and if for any reason CLR needs to allocate more memory then it will ignore this value. private const long AllocationBudgetForGCDuringSpecialization = 24 * 1024 * 1024; private readonly ILogger _logger; private readonly RequestDelegate _next; private readonly IScriptWebHostEnvironment _webHostEnvironment; private readonly IEnvironment _environment; private RequestDelegate _invoke; private double _specialized = 0; public ClrOptimizationMiddleware(RequestDelegate next, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, ILogger<ClrOptimizationMiddleware> logger) { _webHostEnvironment = webHostEnvironment; _environment = environment; _logger = logger; _next = next; _invoke = _environment.IsAnyLinuxConsumption() ? next : InvokeClrOptimizationCheck; } public Task Invoke(HttpContext context) { return _invoke(context); } private Task InvokeClrOptimizationCheck(HttpContext context) { var task = _next.Invoke(context).ContinueWith(task => { // We are tweaking GC behavior in ClrOptimizationMiddleware as this is one of the last call stacks that get executed during standby mode as well as function exection. // We force a GC and enter no GC region in standby mode and exit no GC region after first function execution during specialization. StartStopGCAsBestEffort(); }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion); return task; } private void StartStopGCAsBestEffort() { try { // optimization not intended for single core VMs if (_webHostEnvironment.InStandbyMode && _environment.GetEffectiveCoresCount() > 1 && !_environment.IsAnyLinuxConsumption()) { // If in placeholder mode and already in NoGCRegion, let's end it then start NoGCRegion again. // This may happen if there are multiple warmup calls(few minutes apart) during placeholder mode and before specialization. if (GCSettings.LatencyMode == GCLatencyMode.NoGCRegion) { GC.EndNoGCRegion(); } // In standby mode, we enter NoGCRegion mode as best effort. // This is to try to avoid GC during cold start specialization. if (!GC.TryStartNoGCRegion(AllocationBudgetForGCDuringSpecialization, disallowFullBlockingGC: false)) { _logger.LogError($"CLR runtime GC failed to commit the requested amount of memory: {AllocationBudgetForGCDuringSpecialization}"); } _logger.LogInformation($"GC Collection count for gen 0: {GC.CollectionCount(0)}, gen 1: {GC.CollectionCount(1)}, gen 2: {GC.CollectionCount(2)}"); } else { // if not in standby mode and we are in NoGCRegion then we end NoGCRegion. if (GCSettings.LatencyMode == GCLatencyMode.NoGCRegion) { GC.EndNoGCRegion(); _logger.LogInformation($"GC Collection count for gen 0: {GC.CollectionCount(0)}, gen 1: {GC.CollectionCount(1)}, gen 2: {GC.CollectionCount(2)}"); } // This logic needs to run only once during specialization, so replacing the RequestDelegate after specialization if (Interlocked.CompareExchange(ref _specialized, 1, 0) == 0) { Interlocked.Exchange(ref _invoke, _next); } } } catch (Exception ex) { // Just logging it at informational. _logger.LogInformation(ex, "GC optimization will not get applied."); } } } }