diff --git a/release_notes.md b/release_notes.md index e28696a801..663fbc17b6 100644 --- a/release_notes.md +++ b/release_notes.md @@ -8,3 +8,4 @@ - Memory allocation optimizations in `ReadLanguageWorkerFile` by reading files in buffered chunks, preventing LOH allocations (#11069) - Enhancing the capability to send startup failure logs to AppInsights/Otel. (#11055) - Added support for collecting cross-platform perf traces and generating PGO JIT traces (#11062) +- Memory allocation optimizations in `DependencyHelper.GetExtensionRequirements` (#11022) diff --git a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs index 43c3758c18..4e61811073 100644 --- a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs +++ b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs @@ -343,7 +343,7 @@ void CollectError(Type extensionType, Version minimumVersion, ExtensionStartupTy private ExtensionRequirementsInfo GetExtensionRequirementsInfo() { ExtensionRequirementsInfo requirementsInfo = _extensionRequirementOptions.Value.Bundles != null || _extensionRequirementOptions.Value.Extensions != null - ? new ExtensionRequirementsInfo(_extensionRequirementOptions.Value.Bundles, _extensionRequirementOptions.Value.Extensions) + ? new ExtensionRequirementsInfo(_extensionRequirementOptions.Value.Bundles?.ToArray() ?? [], _extensionRequirementOptions.Value.Extensions?.ToArray() ?? []) : DependencyHelper.GetExtensionRequirements(); return requirementsInfo; } diff --git a/src/WebJobs.Script/Description/DotNet/DependencyHelper.cs b/src/WebJobs.Script/Description/DotNet/DependencyHelper.cs index 3f9ed5e932..c6303984f6 100644 --- a/src/WebJobs.Script/Description/DotNet/DependencyHelper.cs +++ b/src/WebJobs.Script/Description/DotNet/DependencyHelper.cs @@ -5,32 +5,40 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; +using System.Text.Json; +using Microsoft.Azure.WebJobs.Script.Description.DotNet; using Microsoft.Azure.WebJobs.Script.ExtensionRequirements; using Microsoft.Extensions.DependencyModel; -using Newtonsoft.Json.Linq; namespace Microsoft.Azure.WebJobs.Script.Description { public static class DependencyHelper { private const string AssemblyNamePrefix = "assembly:"; - private static readonly Lazy> _ridGraph = new Lazy>(BuildRuntimesGraph); + private static readonly Assembly ThisAssembly = typeof(DependencyHelper).Assembly; + private static readonly string ThisAssemblyName = ThisAssembly.GetName().Name; + private static readonly Lazy> RidGraph = new Lazy>(BuildRuntimesGraph); + private static string _runtimeIdentifier; private static Dictionary BuildRuntimesGraph() { - var ridGraph = new Dictionary(); - string runtimesJson = GetRuntimesGraphJson(); - var runtimes = (JObject)JObject.Parse(runtimesJson)["runtimes"]; + using var stream = GetEmbeddedResourceStream("runtimes.json"); + + var runtimeGraph = JsonSerializer.Deserialize(stream, RuntimeGraphJsonContext.Default.RuntimeGraph); - foreach (var runtime in runtimes) + if (runtimeGraph is not { Runtimes.Count: > 0 }) { - string[] imports = ((JObject)runtime.Value)["#import"] - ?.Values() - .ToArray(); + throw new InvalidOperationException("Failed to deserialize runtimes graph JSON or runtimes section is empty."); + } + + var ridGraph = new Dictionary(runtimeGraph.Runtimes.Count, StringComparer.OrdinalIgnoreCase); - ridGraph.Add(runtime.Key, imports); + foreach (var (rid, info) in runtimeGraph.Runtimes) + { + ridGraph[rid] = info.Imports ?? []; } return ridGraph; @@ -66,43 +74,43 @@ private static string GetDefaultPlatformRid() return rid; } - private static string GetRuntimesGraphJson() + private static Stream GetEmbeddedResourceStream(string fileName) { - return GetResourceFileContents("runtimes.json"); - } + var stream = ThisAssembly.GetManifestResourceStream($"{ThisAssemblyName}.{fileName}"); - private static string GetResourceFileContents(string fileName) - { - var assembly = typeof(DependencyHelper).Assembly; - using (Stream resource = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.{fileName}")) - using (var reader = new StreamReader(resource)) - { - return reader.ReadToEnd(); - } + return stream ?? throw new InvalidOperationException($"The embedded resource '{ThisAssemblyName}.{fileName}' could not be found."); } internal static Dictionary GetRuntimeAssemblies(string assemblyManifestName) { - string assembliesJson = GetResourceFileContents(assemblyManifestName); - JObject assemblies = JObject.Parse(assembliesJson); + using var stream = GetEmbeddedResourceStream(assemblyManifestName); + var runtimeAssemblies = JsonSerializer.Deserialize(stream, RuntimeAssembliesJsonContext.Default.RuntimeAssembliesConfig); + + var assemblies = runtimeAssemblies?.RuntimeAssemblies ?? throw new InvalidOperationException($"Failed to retrieve runtime assemblies from the embedded resource '{assemblyManifestName}'."); + + var dictionary = new Dictionary(assemblies.Count, StringComparer.OrdinalIgnoreCase); + + foreach (var assembly in assemblies) + { + dictionary[assembly.Name] = assembly; + } - return assemblies["runtimeAssemblies"] - .ToObject() - .ToDictionary(a => a.Name, StringComparer.OrdinalIgnoreCase); + return dictionary; } internal static ExtensionRequirementsInfo GetExtensionRequirements() { - string requirementsJson = GetResourceFileContents("extensionrequirements.json"); - JObject requirements = JObject.Parse(requirementsJson); + const string fileName = "extensionrequirements.json"; - var bundleRequirements = requirements["bundles"] - .ToObject(); + using var stream = GetEmbeddedResourceStream(fileName); + var extensionRequirementsInfo = JsonSerializer.Deserialize(stream, ExtensionRequirementsJsonContext.Default.ExtensionRequirementsInfo); - var extensionRequirements = requirements["types"] - .ToObject(); + if (extensionRequirementsInfo is null) + { + throw new InvalidOperationException($"Failed to deserialize extension requirements from embedded resource '{fileName}'."); + } - return new ExtensionRequirementsInfo(bundleRequirements, extensionRequirements); + return extensionRequirementsInfo; } /// @@ -115,7 +123,7 @@ internal static ExtensionRequirementsInfo GetExtensionRequirements() /// The runtime fallbacks for the provided identifier. public static RuntimeFallbacks GetDefaultRuntimeFallbacks(string rid) { - var ridGraph = _ridGraph.Value; + var ridGraph = RidGraph.Value; var runtimeFallbacks = new RuntimeFallbacks(rid); var fallbacks = new List(); @@ -184,7 +192,7 @@ public static List GetRuntimeFallbacks(string rid) /// bool if string in was in proper assembly representation format. public static bool IsAssemblyReferenceFormat(string assemblyFormatString) { - return assemblyFormatString != null && assemblyFormatString.StartsWith(AssemblyNamePrefix); + return assemblyFormatString != null && assemblyFormatString.StartsWith(AssemblyNamePrefix); } /// diff --git a/src/WebJobs.Script/Description/DotNet/RuntimeAssembliesConfig.cs b/src/WebJobs.Script/Description/DotNet/RuntimeAssembliesConfig.cs new file mode 100644 index 0000000000..8508bf3965 --- /dev/null +++ b/src/WebJobs.Script/Description/DotNet/RuntimeAssembliesConfig.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Azure.WebJobs.Script.Description +{ + /// + /// Represents the configuration that lists runtime assemblies and their resolution policies. + /// + internal sealed class RuntimeAssembliesConfig + { + public List RuntimeAssemblies { get; set; } + } +} diff --git a/src/WebJobs.Script/Description/DotNet/RuntimeAssembliesJsonContext.cs b/src/WebJobs.Script/Description/DotNet/RuntimeAssembliesJsonContext.cs new file mode 100644 index 0000000000..a8a101aa69 --- /dev/null +++ b/src/WebJobs.Script/Description/DotNet/RuntimeAssembliesJsonContext.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Script.Description.DotNet +{ + [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)] + [JsonSerializable(typeof(RuntimeAssembliesConfig))] + internal partial class RuntimeAssembliesJsonContext : JsonSerializerContext; +} diff --git a/src/WebJobs.Script/Description/DotNet/RuntimeGraph.cs b/src/WebJobs.Script/Description/DotNet/RuntimeGraph.cs new file mode 100644 index 0000000000..1915596c46 --- /dev/null +++ b/src/WebJobs.Script/Description/DotNet/RuntimeGraph.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Azure.WebJobs.Script.Description.DotNet +{ + /// + /// Represents the runtime graph configuration defined in runtimes.json. + /// + internal sealed class RuntimeGraph + { + public Dictionary Runtimes { get; set; } = []; + } +} diff --git a/src/WebJobs.Script/Description/DotNet/RuntimeGraphJsonContext.cs b/src/WebJobs.Script/Description/DotNet/RuntimeGraphJsonContext.cs new file mode 100644 index 0000000000..9ec96afb2c --- /dev/null +++ b/src/WebJobs.Script/Description/DotNet/RuntimeGraphJsonContext.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Script.Description.DotNet +{ + [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)] + [JsonSerializable(typeof(RuntimeGraph))] + internal partial class RuntimeGraphJsonContext : JsonSerializerContext; +} diff --git a/src/WebJobs.Script/Description/DotNet/RuntimeInfo.cs b/src/WebJobs.Script/Description/DotNet/RuntimeInfo.cs new file mode 100644 index 0000000000..7d3d0cb3e4 --- /dev/null +++ b/src/WebJobs.Script/Description/DotNet/RuntimeInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Script.Description.DotNet +{ + internal sealed class RuntimeInfo + { + [JsonPropertyName("#import")] + public string[] Imports { get; set; } = []; + } +} diff --git a/src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsInfo.cs b/src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsInfo.cs index 51b9a7c1a6..a1f4f48d7b 100644 --- a/src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsInfo.cs +++ b/src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsInfo.cs @@ -7,17 +7,19 @@ namespace Microsoft.Azure.WebJobs.Script.ExtensionRequirements { - internal sealed class ExtensionRequirementsInfo + internal sealed class ExtensionRequirementsInfo(BundleRequirement[] bundles, ExtensionStartupTypeRequirement[] types) { - public ExtensionRequirementsInfo(IEnumerable bundleRequirements, IEnumerable extensionRequirements) - { - BundleRequirementsByBundleId = bundleRequirements?.ToDictionary(a => a.Id, StringComparer.OrdinalIgnoreCase); + private Dictionary _bundleRequirementsById; + private Dictionary _extensionRequirementsByStartupType; - ExtensionRequirementsByStartupType = extensionRequirements?.ToDictionary(a => a.Name, StringComparer.OrdinalIgnoreCase); - } + public BundleRequirement[] Bundles { get; } = bundles; - public Dictionary BundleRequirementsByBundleId { get; private set; } + public ExtensionStartupTypeRequirement[] Types { get; } = types; - public Dictionary ExtensionRequirementsByStartupType { get; private set; } + internal Dictionary BundleRequirementsByBundleId => + _bundleRequirementsById ??= Bundles.ToDictionary(b => b.Id, StringComparer.OrdinalIgnoreCase); + + internal Dictionary ExtensionRequirementsByStartupType => + _extensionRequirementsByStartupType ??= Types.ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase); } } diff --git a/src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsJsonContext.cs b/src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsJsonContext.cs new file mode 100644 index 0000000000..5d77a248c2 --- /dev/null +++ b/src/WebJobs.Script/ExtensionRequirements/ExtensionRequirementsJsonContext.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Script.ExtensionRequirements +{ + [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)] + [JsonSerializable(typeof(ExtensionRequirementsInfo))] + internal partial class ExtensionRequirementsJsonContext : JsonSerializerContext; +} diff --git a/test/WebJobs.Script.Tests/Description/DotNet/DependencyHelperTests.cs b/test/WebJobs.Script.Tests/Description/DotNet/DependencyHelperTests.cs index 113641b557..6f7236e47a 100644 --- a/test/WebJobs.Script.Tests/Description/DotNet/DependencyHelperTests.cs +++ b/test/WebJobs.Script.Tests/Description/DotNet/DependencyHelperTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Extensions.DependencyModel; using Xunit; @@ -13,6 +12,23 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Description { public class DependencyHelperTests { + [Fact] + public void GetExtensionRequirementsReturnsEmbededManifestContent() + { + var extensionRequirements = DependencyHelper.GetExtensionRequirements(); + + Assert.NotNull(extensionRequirements); + Assert.NotNull(extensionRequirements.BundleRequirementsByBundleId); + Assert.NotNull(extensionRequirements.ExtensionRequirementsByStartupType); + + // Ensure properties are populated for an item in the collection. + Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.AssemblyName); + Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.Name); + Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.PackageName); + Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.MinimumAssemblyVersion); + Assert.NotEmpty(extensionRequirements.ExtensionRequirementsByStartupType.First().Value.MinimumPackageVersion); + } + [Fact] public void GetDefaultRuntimeFallbacks_MatchesCurrentRuntimeFallbacks() {