Skip to content

Commit fbe239b

Browse files
authored
Reduce allocations in DependencyHelper.GetExtensionRequirements (#11022)
* GetExtensionRequirements optimizations * Add release notes * Optimize BuildRuntimesGraph method * null check. * Minor cleanup. * Simplify ExtensionRequirementsInfo type.
1 parent 4d50a92 commit fbe239b

File tree

11 files changed

+151
-45
lines changed

11 files changed

+151
-45
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- Memory allocation optimizations in `ReadLanguageWorkerFile` by reading files in buffered chunks, preventing LOH allocations (#11069)
1010
- Enhancing the capability to send startup failure logs to AppInsights/Otel. (#11055)
1111
- Added support for collecting cross-platform perf traces and generating PGO JIT traces (#11062)
12+
- Memory allocation optimizations in `DependencyHelper.GetExtensionRequirements` (#11022)

src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ void CollectError(Type extensionType, Version minimumVersion, ExtensionStartupTy
343343
private ExtensionRequirementsInfo GetExtensionRequirementsInfo()
344344
{
345345
ExtensionRequirementsInfo requirementsInfo = _extensionRequirementOptions.Value.Bundles != null || _extensionRequirementOptions.Value.Extensions != null
346-
? new ExtensionRequirementsInfo(_extensionRequirementOptions.Value.Bundles, _extensionRequirementOptions.Value.Extensions)
346+
? new ExtensionRequirementsInfo(_extensionRequirementOptions.Value.Bundles?.ToArray() ?? [], _extensionRequirementOptions.Value.Extensions?.ToArray() ?? [])
347347
: DependencyHelper.GetExtensionRequirements();
348348
return requirementsInfo;
349349
}

src/WebJobs.Script/Description/DotNet/DependencyHelper.cs

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,40 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Reflection;
89
using System.Runtime.InteropServices;
10+
using System.Text.Json;
11+
using Microsoft.Azure.WebJobs.Script.Description.DotNet;
912
using Microsoft.Azure.WebJobs.Script.ExtensionRequirements;
1013
using Microsoft.Extensions.DependencyModel;
11-
using Newtonsoft.Json.Linq;
1214

1315
namespace Microsoft.Azure.WebJobs.Script.Description
1416
{
1517
public static class DependencyHelper
1618
{
1719
private const string AssemblyNamePrefix = "assembly:";
18-
private static readonly Lazy<Dictionary<string, string[]>> _ridGraph = new Lazy<Dictionary<string, string[]>>(BuildRuntimesGraph);
20+
private static readonly Assembly ThisAssembly = typeof(DependencyHelper).Assembly;
21+
private static readonly string ThisAssemblyName = ThisAssembly.GetName().Name;
22+
private static readonly Lazy<Dictionary<string, string[]>> RidGraph = new Lazy<Dictionary<string, string[]>>(BuildRuntimesGraph);
23+
1924
private static string _runtimeIdentifier;
2025

2126
private static Dictionary<string, string[]> BuildRuntimesGraph()
2227
{
23-
var ridGraph = new Dictionary<string, string[]>();
24-
string runtimesJson = GetRuntimesGraphJson();
25-
var runtimes = (JObject)JObject.Parse(runtimesJson)["runtimes"];
28+
using var stream = GetEmbeddedResourceStream("runtimes.json");
29+
30+
var runtimeGraph = JsonSerializer.Deserialize(stream, RuntimeGraphJsonContext.Default.RuntimeGraph);
2631

27-
foreach (var runtime in runtimes)
32+
if (runtimeGraph is not { Runtimes.Count: > 0 })
2833
{
29-
string[] imports = ((JObject)runtime.Value)["#import"]
30-
?.Values<string>()
31-
.ToArray();
34+
throw new InvalidOperationException("Failed to deserialize runtimes graph JSON or runtimes section is empty.");
35+
}
36+
37+
var ridGraph = new Dictionary<string, string[]>(runtimeGraph.Runtimes.Count, StringComparer.OrdinalIgnoreCase);
3238

33-
ridGraph.Add(runtime.Key, imports);
39+
foreach (var (rid, info) in runtimeGraph.Runtimes)
40+
{
41+
ridGraph[rid] = info.Imports ?? [];
3442
}
3543

3644
return ridGraph;
@@ -66,43 +74,43 @@ private static string GetDefaultPlatformRid()
6674
return rid;
6775
}
6876

69-
private static string GetRuntimesGraphJson()
77+
private static Stream GetEmbeddedResourceStream(string fileName)
7078
{
71-
return GetResourceFileContents("runtimes.json");
72-
}
79+
var stream = ThisAssembly.GetManifestResourceStream($"{ThisAssemblyName}.{fileName}");
7380

74-
private static string GetResourceFileContents(string fileName)
75-
{
76-
var assembly = typeof(DependencyHelper).Assembly;
77-
using (Stream resource = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.{fileName}"))
78-
using (var reader = new StreamReader(resource))
79-
{
80-
return reader.ReadToEnd();
81-
}
81+
return stream ?? throw new InvalidOperationException($"The embedded resource '{ThisAssemblyName}.{fileName}' could not be found.");
8282
}
8383

8484
internal static Dictionary<string, ScriptRuntimeAssembly> GetRuntimeAssemblies(string assemblyManifestName)
8585
{
86-
string assembliesJson = GetResourceFileContents(assemblyManifestName);
87-
JObject assemblies = JObject.Parse(assembliesJson);
86+
using var stream = GetEmbeddedResourceStream(assemblyManifestName);
87+
var runtimeAssemblies = JsonSerializer.Deserialize(stream, RuntimeAssembliesJsonContext.Default.RuntimeAssembliesConfig);
88+
89+
var assemblies = runtimeAssemblies?.RuntimeAssemblies ?? throw new InvalidOperationException($"Failed to retrieve runtime assemblies from the embedded resource '{assemblyManifestName}'.");
90+
91+
var dictionary = new Dictionary<string, ScriptRuntimeAssembly>(assemblies.Count, StringComparer.OrdinalIgnoreCase);
92+
93+
foreach (var assembly in assemblies)
94+
{
95+
dictionary[assembly.Name] = assembly;
96+
}
8897

89-
return assemblies["runtimeAssemblies"]
90-
.ToObject<ScriptRuntimeAssembly[]>()
91-
.ToDictionary(a => a.Name, StringComparer.OrdinalIgnoreCase);
98+
return dictionary;
9299
}
93100

94101
internal static ExtensionRequirementsInfo GetExtensionRequirements()
95102
{
96-
string requirementsJson = GetResourceFileContents("extensionrequirements.json");
97-
JObject requirements = JObject.Parse(requirementsJson);
103+
const string fileName = "extensionrequirements.json";
98104

99-
var bundleRequirements = requirements["bundles"]
100-
.ToObject<BundleRequirement[]>();
105+
using var stream = GetEmbeddedResourceStream(fileName);
106+
var extensionRequirementsInfo = JsonSerializer.Deserialize(stream, ExtensionRequirementsJsonContext.Default.ExtensionRequirementsInfo);
101107

102-
var extensionRequirements = requirements["types"]
103-
.ToObject<ExtensionStartupTypeRequirement[]>();
108+
if (extensionRequirementsInfo is null)
109+
{
110+
throw new InvalidOperationException($"Failed to deserialize extension requirements from embedded resource '{fileName}'.");
111+
}
104112

105-
return new ExtensionRequirementsInfo(bundleRequirements, extensionRequirements);
113+
return extensionRequirementsInfo;
106114
}
107115

108116
/// <summary>
@@ -115,7 +123,7 @@ internal static ExtensionRequirementsInfo GetExtensionRequirements()
115123
/// <returns>The runtime fallbacks for the provided identifier.</returns>
116124
public static RuntimeFallbacks GetDefaultRuntimeFallbacks(string rid)
117125
{
118-
var ridGraph = _ridGraph.Value;
126+
var ridGraph = RidGraph.Value;
119127

120128
var runtimeFallbacks = new RuntimeFallbacks(rid);
121129
var fallbacks = new List<string>();
@@ -184,7 +192,7 @@ public static List<string> GetRuntimeFallbacks(string rid)
184192
/// <returns> bool if string in was in proper assembly representation format. </returns>
185193
public static bool IsAssemblyReferenceFormat(string assemblyFormatString)
186194
{
187-
return assemblyFormatString != null && assemblyFormatString.StartsWith(AssemblyNamePrefix);
195+
return assemblyFormatString != null && assemblyFormatString.StartsWith(AssemblyNamePrefix);
188196
}
189197

190198
/// <summary>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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.Collections.Generic;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Description
7+
{
8+
/// <summary>
9+
/// Represents the configuration that lists runtime assemblies and their resolution policies.
10+
/// </summary>
11+
internal sealed class RuntimeAssembliesConfig
12+
{
13+
public List<ScriptRuntimeAssembly> RuntimeAssemblies { get; set; }
14+
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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;
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.Azure.WebJobs.Script.Description.DotNet
8+
{
9+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)]
10+
[JsonSerializable(typeof(RuntimeAssembliesConfig))]
11+
internal partial class RuntimeAssembliesJsonContext : JsonSerializerContext;
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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.Collections.Generic;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Description.DotNet
7+
{
8+
/// <summary>
9+
/// Represents the runtime graph configuration defined in runtimes.json.
10+
/// </summary>
11+
internal sealed class RuntimeGraph
12+
{
13+
public Dictionary<string, RuntimeInfo> Runtimes { get; set; } = [];
14+
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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;
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.Azure.WebJobs.Script.Description.DotNet
8+
{
9+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)]
10+
[JsonSerializable(typeof(RuntimeGraph))]
11+
internal partial class RuntimeGraphJsonContext : JsonSerializerContext;
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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.Description.DotNet
7+
{
8+
internal sealed class RuntimeInfo
9+
{
10+
[JsonPropertyName("#import")]
11+
public string[] Imports { get; set; } = [];
12+
}
13+
}

src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsInfo.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77

88
namespace Microsoft.Azure.WebJobs.Script.ExtensionRequirements
99
{
10-
internal sealed class ExtensionRequirementsInfo
10+
internal sealed class ExtensionRequirementsInfo(BundleRequirement[] bundles, ExtensionStartupTypeRequirement[] types)
1111
{
12-
public ExtensionRequirementsInfo(IEnumerable<BundleRequirement> bundleRequirements, IEnumerable<ExtensionStartupTypeRequirement> extensionRequirements)
13-
{
14-
BundleRequirementsByBundleId = bundleRequirements?.ToDictionary(a => a.Id, StringComparer.OrdinalIgnoreCase);
12+
private Dictionary<string, BundleRequirement> _bundleRequirementsById;
13+
private Dictionary<string, ExtensionStartupTypeRequirement> _extensionRequirementsByStartupType;
1514

16-
ExtensionRequirementsByStartupType = extensionRequirements?.ToDictionary(a => a.Name, StringComparer.OrdinalIgnoreCase);
17-
}
15+
public BundleRequirement[] Bundles { get; } = bundles;
1816

19-
public Dictionary<string, BundleRequirement> BundleRequirementsByBundleId { get; private set; }
17+
public ExtensionStartupTypeRequirement[] Types { get; } = types;
2018

21-
public Dictionary<string, ExtensionStartupTypeRequirement> ExtensionRequirementsByStartupType { get; private set; }
19+
internal Dictionary<string, BundleRequirement> BundleRequirementsByBundleId =>
20+
_bundleRequirementsById ??= Bundles.ToDictionary(b => b.Id, StringComparer.OrdinalIgnoreCase);
21+
22+
internal Dictionary<string, ExtensionStartupTypeRequirement> ExtensionRequirementsByStartupType =>
23+
_extensionRequirementsByStartupType ??= Types.ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase);
2224
}
2325
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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;
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.Azure.WebJobs.Script.ExtensionRequirements
8+
{
9+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)]
10+
[JsonSerializable(typeof(ExtensionRequirementsInfo))]
11+
internal partial class ExtensionRequirementsJsonContext : JsonSerializerContext;
12+
}

test/WebJobs.Script.Tests/Description/DotNet/DependencyHelperTests.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7-
using System.Text;
87
using Microsoft.Azure.WebJobs.Script.Description;
98
using Microsoft.Extensions.DependencyModel;
109
using Xunit;
@@ -13,6 +12,23 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Description
1312
{
1413
public class DependencyHelperTests
1514
{
15+
[Fact]
16+
public void GetExtensionRequirementsReturnsEmbededManifestContent()
17+
{
18+
var extensionRequirements = DependencyHelper.GetExtensionRequirements();
19+
20+
Assert.NotNull(extensionRequirements);
21+
Assert.NotNull(extensionRequirements.BundleRequirementsByBundleId);
22+
Assert.NotNull(extensionRequirements.ExtensionRequirementsByStartupType);
23+
24+
// Ensure properties are populated for an item in the collection.
25+
Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.AssemblyName);
26+
Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.Name);
27+
Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.PackageName);
28+
Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.MinimumAssemblyVersion);
29+
Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.MinimumPackageVersion);
30+
}
31+
1632
[Fact]
1733
public void GetDefaultRuntimeFallbacks_MatchesCurrentRuntimeFallbacks()
1834
{

0 commit comments

Comments
 (0)