Skip to content

Commit 7adf9e4

Browse files
authored
Reduce allocations in ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync (#11012)
* Reduce allocations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` * Fix release notes. * Adding null check on Bindings property. * Added test to simulate no "Bindings" in extension json. * Using different cases for extension JSON entries.
1 parent da3e3f9 commit 7adf9e4

File tree

6 files changed

+111
-35
lines changed

6 files changed

+111
-35
lines changed

release_notes.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
### Release notes
2-
3-
<!-- Please add your release notes in the following format:
4-
- My change description (#PR)
5-
-->
1+
### Release notes
2+
3+
<!-- Please add your release notes in the following format:
4+
- My change description (#PR)
5+
-->
6+
- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012)

src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Linq;
99
using System.Reflection;
10+
using System.Text.Json;
1011
using System.Threading.Tasks;
1112
using Microsoft.Azure.WebJobs.Hosting;
1213
using Microsoft.Azure.WebJobs.Script.Config;
@@ -19,16 +20,14 @@
1920
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
2021
using Microsoft.Extensions.Logging;
2122
using Microsoft.Extensions.Options;
22-
using Newtonsoft.Json;
23-
using Newtonsoft.Json.Linq;
2423

2524
namespace Microsoft.Azure.WebJobs.Script.DependencyInjection
2625
{
2726
/// <summary>
2827
/// An implementation of an <see cref="IWebJobsStartupTypeLocator"/> that locates startup types
2928
/// from extension registrations.
3029
/// </summary>
31-
public class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator
30+
public sealed class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator
3231
{
3332
private const string ApplicationInsightsStartupType = "Microsoft.Azure.WebJobs.Extensions.ApplicationInsights.ApplicationInsightsWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.ApplicationInsights, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9475d07f10cb09df";
3433

@@ -146,7 +145,7 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
146145
string metadataFilePath = Path.Combine(extensionsMetadataPath, ScriptConstants.ExtensionsMetadataFileName);
147146

148147
// parse the extensions file to get declared startup extensions
149-
ExtensionReference[] extensionItems = ParseExtensions(metadataFilePath);
148+
ExtensionReference[] extensionItems = await ParseExtensionsAsync(metadataFilePath);
150149

151150
var startupTypes = new List<Type>();
152151

@@ -160,7 +159,7 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
160159
}
161160

162161
if (!bundleConfigured
163-
|| extensionItem.Bindings.Count == 0
162+
|| extensionItem.Bindings is null || extensionItem.Bindings.Count == 0
164163
|| extensionItem.Bindings.Intersect(bindingsSet, StringComparer.OrdinalIgnoreCase).Any())
165164
{
166165
string startupExtensionName = extensionItem.Name ?? extensionItem.TypeName;
@@ -222,33 +221,32 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
222221
return startupTypes;
223222
}
224223

225-
private ExtensionReference[] ParseExtensions(string metadataFilePath)
224+
private async Task<ExtensionReference[]> ParseExtensionsAsync(string metadataFilePath)
226225
{
227226
using (_metricsLogger.LatencyEvent(MetricEventNames.ParseExtensions))
228227
{
229228
if (!File.Exists(metadataFilePath))
230229
{
231-
return Array.Empty<ExtensionReference>();
230+
return [];
232231
}
233232

234233
try
235234
{
236-
var extensionMetadata = JObject.Parse(File.ReadAllText(metadataFilePath));
235+
await using var stream = File.OpenRead(metadataFilePath);
236+
var extensionReferences = await JsonSerializer.DeserializeAsync(stream, ExtensionReferencesJsonContext.Default.ExtensionReferences);
237237

238-
var extensionItems = extensionMetadata["extensions"]?.ToObject<List<ExtensionReference>>();
239-
if (extensionItems == null)
238+
if (extensionReferences?.Extensions == null || extensionReferences.Extensions.Length == 0)
240239
{
241240
_logger.ScriptStartUpUnableParseMetadataMissingProperty(metadataFilePath);
242-
return Array.Empty<ExtensionReference>();
241+
return [];
243242
}
244243

245-
return extensionItems.ToArray();
244+
return extensionReferences.Extensions;
246245
}
247-
catch (JsonReaderException exc)
246+
catch (JsonException exc)
248247
{
249248
_logger.ScriptStartUpUnableParseMetadata(exc, metadataFilePath);
250-
251-
return Array.Empty<ExtensionReference>();
249+
return [];
252250
}
253251
}
254252
}
@@ -275,12 +273,14 @@ private void ValidateExtensionRequirements(List<Type> startupTypes, ExtensionReq
275273
{
276274
return;
277275
}
278-
var errors = new List<string>();
276+
277+
List<string> errors = null;
279278

280279
void CollectError(Type extensionType, Version minimumVersion, ExtensionStartupTypeRequirement requirement)
281280
{
282281
_logger.MinimumExtensionVersionNotSatisfied(extensionType.Name, extensionType.Assembly.FullName, minimumVersion, requirement.PackageName, requirement.MinimumPackageVersion);
283282
string requirementNotMetError = $"ExtensionStartupType {extensionType.Name} from assembly '{extensionType.Assembly.FullName}' does not meet the required minimum version of {minimumVersion}. Update your NuGet package reference for {requirement.PackageName} to {requirement.MinimumPackageVersion} or later.";
283+
errors ??= [];
284284
errors.Add(requirementNotMetError);
285285
}
286286

@@ -327,7 +327,7 @@ void CollectError(Type extensionType, Version minimumVersion, ExtensionStartupTy
327327
}
328328
}
329329

330-
if (errors.Count > 0)
330+
if (errors != null && errors.Count > 0)
331331
{
332332
var builder = new System.Text.StringBuilder();
333333
builder.AppendLine("One or more loaded extensions do not meet the minimum requirements. For more information see https://aka.ms/func-min-extension-versions.");
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,34 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
using System;
5-
using System.Collections;
64
using System.Collections.Generic;
7-
using System.Collections.ObjectModel;
8-
using System.Text;
95

106
namespace Microsoft.Azure.WebJobs.Script.Models
117
{
128
/// <summary>
139
/// Represents a binding extension reference.
1410
/// </summary>
15-
public class ExtensionReference
11+
public sealed class ExtensionReference
1612
{
1713
/// <summary>
18-
/// Gets or sets the extension name.
14+
/// Gets the extension name.
1915
/// </summary>
20-
public string Name { get; set; }
16+
public string Name { get; init; }
2117

2218
/// <summary>
23-
/// Gets or sets the assembly-qualified name of the type.
19+
/// Gets the assembly-qualified name of the type.
2420
/// </summary>
25-
public string TypeName { get; set; }
21+
public string TypeName { get; init; }
2622

2723
/// <summary>
28-
/// Gets or sets a hit path that may be used when loading the assembly containing the extension
24+
/// Gets a hit path that may be used when loading the assembly containing the extension.
2925
/// implementation.
3026
/// </summary>
31-
public string HintPath { get; set; }
27+
public string HintPath { get; init; }
3228

3329
/// <summary>
34-
/// Gets the binding exposed by the extension
30+
/// Gets the binding exposed by the extension.
3531
/// </summary>
36-
public ICollection<string> Bindings { get; } = new Collection<string>();
32+
public ICollection<string> Bindings { get; init; } = [];
3733
}
3834
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Azure.WebJobs.Script.Models
5+
{
6+
public sealed class ExtensionReferences
7+
{
8+
public ExtensionReference[] Extensions { get; init; } = [];
9+
}
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Models
7+
{
8+
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
9+
[JsonSerializable(typeof(ExtensionReferences))]
10+
public partial class ExtensionReferencesJsonContext : JsonSerializerContext;
11+
}

test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,64 @@ void CopyToBin(string path)
952952
}
953953
}
954954

955+
[Fact]
956+
public async Task GetExtensionsStartupTypes_NoBindings_In_ExtensionJson()
957+
{
958+
TestMetricsLogger testMetricsLogger = new TestMetricsLogger();
959+
960+
using var directory = new TempDirectory();
961+
var binPath = Path.Combine(directory.Path, "bin");
962+
Directory.CreateDirectory(binPath);
963+
964+
void CopyToBin(string path)
965+
{
966+
File.Copy(path, Path.Combine(binPath, Path.GetFileName(path)));
967+
}
968+
969+
CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location);
970+
971+
string extensionJson = $$"""
972+
{
973+
"extensions": [
974+
{
975+
"name": "Storage",
976+
"typeName": "{{typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName}}",
977+
"hintPath": "Microsoft.Azure.WebJobs.Extensions.Storage.dll"
978+
},
979+
{
980+
"Name": "AzureStorageBlobs",
981+
"TypeName": "{{typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName}}"
982+
}
983+
]
984+
}
985+
""";
986+
987+
File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensionJson);
988+
989+
TestLoggerProvider testLoggerProvider = new TestLoggerProvider();
990+
LoggerFactory factory = new LoggerFactory();
991+
factory.AddProvider(testLoggerProvider);
992+
var testLogger = factory.CreateLogger<ScriptStartupTypeLocator>();
993+
994+
var mockExtensionBundleManager = new Mock<IExtensionBundleManager>();
995+
mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true);
996+
mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(new ExtensionBundleDetails() { Id = "bundleID", Version = "1.0.0" }));
997+
mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath));
998+
999+
var languageWorkerOptions = new TestOptionsMonitor<LanguageWorkerOptions>(new LanguageWorkerOptions());
1000+
var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions);
1001+
OptionsWrapper<ExtensionRequirementOptions> optionsWrapper = new(new ExtensionRequirementOptions());
1002+
var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper);
1003+
1004+
// Act
1005+
var types = await discoverer.GetExtensionsStartupTypesAsync();
1006+
1007+
// Assert
1008+
AreExpectedMetricsGenerated(testMetricsLogger);
1009+
Assert.Equal(types.Count(), 2);
1010+
Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.FirstOrDefault().FullName);
1011+
}
1012+
9551013
[Fact]
9561014
public async Task GetExtensionsStartupTypes_RejectsBundleBelowMinimumVersion()
9571015
{

0 commit comments

Comments
 (0)