Skip to content

Reduce allocations in DependencyHelper.GetExtensionRequirements #11022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions release_notes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### Release notes

<!-- Please add your release notes in the following format:
- My change description (#PR)
-->
### Release notes

<!-- Please add your release notes in the following format:
- My change description (#PR)
-->
- Memory allocation optimizations in `DependencyHelper.GetExtensionRequirements` (#11022)
- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012)
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,11 @@ private bool IsDotnetIsolatedApp(IEnumerable<FunctionMetadata> functions, IEnvir
private ExtensionRequirementsInfo GetExtensionRequirementsInfo()
{
ExtensionRequirementsInfo requirementsInfo = _extensionRequirementOptions.Value.Bundles != null || _extensionRequirementOptions.Value.Extensions != null
? new ExtensionRequirementsInfo(_extensionRequirementOptions.Value.Bundles, _extensionRequirementOptions.Value.Extensions)
? new ExtensionRequirementsInfo
{
Bundles = _extensionRequirementOptions.Value.Bundles?.ToArray() ?? [],
Types = _extensionRequirementOptions.Value.Extensions?.ToArray() ?? []
}
: DependencyHelper.GetExtensionRequirements();
return requirementsInfo;
}
Expand Down
78 changes: 43 additions & 35 deletions src/WebJobs.Script/Description/DotNet/DependencyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dictionary<string, string[]>> _ridGraph = new Lazy<Dictionary<string, string[]>>(BuildRuntimesGraph);
private static readonly Assembly ThisAssembly = typeof(DependencyHelper).Assembly;
private static readonly string ThisAssemblyName = ThisAssembly.GetName().Name;
private static readonly Lazy<Dictionary<string, string[]>> RidGraph = new Lazy<Dictionary<string, string[]>>(BuildRuntimesGraph);

private static string _runtimeIdentifier;

private static Dictionary<string, string[]> BuildRuntimesGraph()
{
var ridGraph = new Dictionary<string, string[]>();
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<string>()
.ToArray();
throw new InvalidOperationException("Failed to deserialize runtimes graph JSON or runtimes section is empty.");
}

var ridGraph = new Dictionary<string, string[]>(runtimeGraph.Runtimes.Count, StringComparer.OrdinalIgnoreCase);

ridGraph.Add(runtime.Key, imports);
foreach (var (rid, info) in runtimeGraph.Runtimes)
{
ridGraph[rid] = info.Imports ?? [];
}

return ridGraph;
Expand Down Expand Up @@ -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<string, ScriptRuntimeAssembly> 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<string, ScriptRuntimeAssembly>(assemblies.Count, StringComparer.OrdinalIgnoreCase);

foreach (var assembly in assemblies)
{
dictionary[assembly.Name] = assembly;
}

return assemblies["runtimeAssemblies"]
.ToObject<ScriptRuntimeAssembly[]>()
.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<BundleRequirement[]>();
using var stream = GetEmbeddedResourceStream(fileName);
var extensionRequirementsInfo = JsonSerializer.Deserialize(stream, ExtensionRequirementsJsonContext.Default.ExtensionRequirementsInfo);

var extensionRequirements = requirements["types"]
.ToObject<ExtensionStartupTypeRequirement[]>();
if (extensionRequirementsInfo is null)
{
throw new InvalidOperationException($"Failed to deserialize extension requirements from embedded resource '{fileName}'.");
}

return new ExtensionRequirementsInfo(bundleRequirements, extensionRequirements);
return extensionRequirementsInfo;
}

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

var runtimeFallbacks = new RuntimeFallbacks(rid);
var fallbacks = new List<string>();
Expand Down Expand Up @@ -184,7 +192,7 @@ public static List<string> GetRuntimeFallbacks(string rid)
/// <returns> bool if string in was in proper assembly representation format. </returns>
public static bool IsAssemblyReferenceFormat(string assemblyFormatString)
{
return assemblyFormatString != null && assemblyFormatString.StartsWith(AssemblyNamePrefix);
return assemblyFormatString != null && assemblyFormatString.StartsWith(AssemblyNamePrefix);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents the configuration that lists runtime assemblies and their resolution policies.
/// </summary>
internal sealed class RuntimeAssembliesConfig
{
public List<ScriptRuntimeAssembly> RuntimeAssemblies { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
15 changes: 15 additions & 0 deletions src/WebJobs.Script/Description/DotNet/RuntimeGraph.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents the runtime graph configuration defined in runtimes.json.
/// </summary>
internal sealed class RuntimeGraph
{
public Dictionary<string, RuntimeInfo> Runtimes { get; set; } = [];
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions src/WebJobs.Script/Description/DotNet/RuntimeInfo.cs
Original file line number Diff line number Diff line change
@@ -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; } = [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,35 @@ namespace Microsoft.Azure.WebJobs.Script.ExtensionRequirements
{
internal sealed class ExtensionRequirementsInfo
{
public ExtensionRequirementsInfo(IEnumerable<BundleRequirement> bundleRequirements, IEnumerable<ExtensionStartupTypeRequirement> extensionRequirements)
private Dictionary<string, BundleRequirement> _bundleRequirementsById;
private Dictionary<string, ExtensionStartupTypeRequirement> _extensionRequirementsByStartupType;
private BundleRequirement[] _bundles = [];
private ExtensionStartupTypeRequirement[] _types = [];

public BundleRequirement[] Bundles
{
BundleRequirementsByBundleId = bundleRequirements?.ToDictionary(a => a.Id, StringComparer.OrdinalIgnoreCase);
get => _bundles;
set
{
_bundles = value ?? [];
_bundleRequirementsById = null;
}
}

ExtensionRequirementsByStartupType = extensionRequirements?.ToDictionary(a => a.Name, StringComparer.OrdinalIgnoreCase);
public ExtensionStartupTypeRequirement[] Types
{
get => _types;
set
{
_types = value ?? [];
_extensionRequirementsByStartupType = null;
}
}

public Dictionary<string, BundleRequirement> BundleRequirementsByBundleId { get; private set; }
public Dictionary<string, BundleRequirement> BundleRequirementsByBundleId =>
_bundleRequirementsById ??= Bundles.ToDictionary(b => b.Id, StringComparer.OrdinalIgnoreCase);

public Dictionary<string, ExtensionStartupTypeRequirement> ExtensionRequirementsByStartupType { get; private set; }
public Dictionary<string, ExtensionStartupTypeRequirement> ExtensionRequirementsByStartupType =>
_extensionRequirementsByStartupType ??= Types.ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +12,23 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Description
{
public class DependencyHelperTests
{
[Fact]
public void GetExtensionRequirementsReturnsEmbededManifestContent()
{
var extensionRequirements = DependencyHelper.GetExtensionRequirements();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a new test to ensure JSON deserialization works as expected.


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()
{
Expand Down