From 777ef9bd79f02fe4e1ca1b14680e1eb6b6d57174 Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Thu, 13 Feb 2025 10:14:35 +0100 Subject: [PATCH 1/3] Cache Category to avoid doing expensive Assembly.GetName() calls --- src/DynamoCore/Library/FunctionDescriptor.cs | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/DynamoCore/Library/FunctionDescriptor.cs b/src/DynamoCore/Library/FunctionDescriptor.cs index 15d3dc83ef4..c26647ae23b 100644 --- a/src/DynamoCore/Library/FunctionDescriptor.cs +++ b/src/DynamoCore/Library/FunctionDescriptor.cs @@ -318,6 +318,8 @@ public IEnumerable> InputParameters private set; } + private string category; + /// /// The category of this function. /// @@ -325,6 +327,11 @@ public string Category { get { + if (category != null) + { + return category; + } + var categoryBuf = new StringBuilder(); categoryBuf.Append(GetRootCategory()); @@ -334,13 +341,11 @@ public string Category //get function assembly var asm = AppDomain.CurrentDomain.GetAssemblies() .Where(x => x.GetName().Name == Path.GetFileNameWithoutExtension(Assembly)) - .ToArray(); + .FirstOrDefault(); - if (asm.Any() && asm.First().GetType(ClassName) != null) + //get class type of function + if (asm?.GetType(ClassName) is System.Type type) { - //get class type of function - var type = asm.First().GetType(ClassName); - //get NodeCategoryAttribute for this function if it was been defined var nodeCat = type.GetMethods().Where(x => x.Name == FunctionName) .Select(x => x.GetCustomAttribute(typeof(NodeCategoryAttribute))) @@ -356,7 +361,8 @@ public string Category || nodeCat == LibraryServices.Categories.MemberFunctions)) { categoryBuf.Append("." + UnqualifedClassName + "." + nodeCat); - return categoryBuf.ToString(); + category = categoryBuf.ToString(); + return category; } } } @@ -380,7 +386,9 @@ public string Category "." + UnqualifedClassName + "." + LibraryServices.Categories.Properties); break; } - return categoryBuf.ToString(); + + category = categoryBuf.ToString(); + return category; } } From 9c8ee4b9af64d79c7bfc05ade1cbd039e9667564 Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 14:16:46 +0100 Subject: [PATCH 2/3] Skip WhereIterator, avoid string manipulation in loop --- src/DynamoCore/Library/FunctionDescriptor.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/DynamoCore/Library/FunctionDescriptor.cs b/src/DynamoCore/Library/FunctionDescriptor.cs index c26647ae23b..91a32569430 100644 --- a/src/DynamoCore/Library/FunctionDescriptor.cs +++ b/src/DynamoCore/Library/FunctionDescriptor.cs @@ -339,9 +339,8 @@ public string Category if (ClassName != null) { //get function assembly - var asm = AppDomain.CurrentDomain.GetAssemblies() - .Where(x => x.GetName().Name == Path.GetFileNameWithoutExtension(Assembly)) - .FirstOrDefault(); + var asmName = Path.GetFileNameWithoutExtension(Assembly); + var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == asmName); //get class type of function if (asm?.GetType(ClassName) is System.Type type) From f0e08361cdabc82229c123a57c35f9e13a4da783 Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Fri, 21 Feb 2025 14:10:35 +0100 Subject: [PATCH 3/3] FunctionDescriptor category accelerator --- src/DynamoCore/Library/FunctionDescriptor.cs | 66 +++++++++++++++++++- src/DynamoCore/Models/DynamoModel.cs | 6 ++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/DynamoCore/Library/FunctionDescriptor.cs b/src/DynamoCore/Library/FunctionDescriptor.cs index 91a32569430..2c471a3c7c2 100644 --- a/src/DynamoCore/Library/FunctionDescriptor.cs +++ b/src/DynamoCore/Library/FunctionDescriptor.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Text; +using System.Threading; using Dynamo.Configuration; using Dynamo.Graph.Nodes; using Dynamo.Interfaces; @@ -159,6 +160,16 @@ public FunctionDescriptorParams() /// public class FunctionDescriptor : IFunctionDescriptor { + /// + /// A dictionary of loaded assemblies by assembly name to speed up Dynamo loading + /// + private static Dictionary assembliesByName; + + /// + /// Ensure the assembly cache is kept around until all callers have finished using it + /// + private static int assemblyCachingRequests = 0; + /// /// A comment describing the Function /// @@ -340,10 +351,9 @@ public string Category { //get function assembly var asmName = Path.GetFileNameWithoutExtension(Assembly); - var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == asmName); //get class type of function - if (asm?.GetType(ClassName) is System.Type type) + if (TryGetAssembly(asmName, out var asm) && asm.GetType(ClassName) is System.Type type) { //get NodeCategoryAttribute for this function if it was been defined var nodeCat = type.GetMethods().Where(x => x.Name == FunctionName) @@ -591,6 +601,56 @@ private bool CheckIfFunctionIsMarkedExperimentalByPrefs(FunctionDescriptor fd) return false; } internal bool IsExperimental { get;} - } + /// + /// Try to get an by name + /// + /// Name of the assembly to load + /// The assembly + /// if a matching assembly was found + private static bool TryGetAssembly(string assemblyName, out Assembly assembly) + { + // Use the lookup dictionary if it exists, to avoid doing .GetName calls. + if (assembliesByName != null) + { + return assembliesByName.TryGetValue(assemblyName, out assembly); + } + + assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(x => x.GetName().Name == assemblyName); + + return assembly != null; + } + + + /// + /// Load all assemblies by name in the current domain for faster lookup. + /// + /// An that removes the assembly dictionary on + internal static IDisposable CacheAssemblyNamesForZeroTouchNodeSearch() + { + return Scheduler.Disposable.Create(() => { + // If in a nested call, the assembliesByName cache should already exist, + // and 'assemblyCachingRequests' should be higher than 0 + if (Interlocked.Increment(ref assemblyCachingRequests) == 1) + { + assembliesByName = new(); + foreach(var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + // Only add the first occurence of an assembly name, to match the + // functionality of TryGetAssembly + assembliesByName.TryAdd(asm.GetName().Name, asm); + } + } + }, + () => { + // If in a nested call, the count should be larger than 1, and the outer + // caller should be responsible for deleting the cache + if (Interlocked.Decrement(ref assemblyCachingRequests) == 0) + { + assembliesByName = null; + } + }); + } + } } diff --git a/src/DynamoCore/Models/DynamoModel.cs b/src/DynamoCore/Models/DynamoModel.cs index 1e45473b1a9..525d2e1f5cf 100644 --- a/src/DynamoCore/Models/DynamoModel.cs +++ b/src/DynamoCore/Models/DynamoModel.cs @@ -1342,6 +1342,9 @@ public virtual void PostTraceReconciliation(Dictionary> orpha /// private void LibraryLoaded(object sender, LibraryServices.LibraryLoadedEventArgs e) { + if (!e.LibraryPaths.Any()) return; + + using (var cachedAssemblies = FunctionDescriptor.CacheAssemblyNamesForZeroTouchNodeSearch()) foreach (var newLibrary in e.LibraryPaths) { // Load all functions defined in that library. @@ -3478,6 +3481,9 @@ internal void AddZeroTouchNodesToSearch(IEnumerable functionGroup { var iDoc = LuceneUtility.InitializeIndexDocumentForNodes(); List nodes = new(); + + // Preload the assembly names for faster FunctionDescriptor.Category resolution + using (var cachedAssemblies = FunctionDescriptor.CacheAssemblyNamesForZeroTouchNodeSearch()) foreach (var funcGroup in functionGroups) { foreach (var functionDescriptor in funcGroup.Functions)