From f0b7882f6e7ad77e7565653ffde41c0bf94744fe Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Wed, 2 Nov 2022 09:07:22 +0100 Subject: [PATCH 01/14] ensure capacity of the arrays and dictionary in BeforeSerialize and AfterSerialize to reduce number of memory allocations --- Scripts/Node.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 704e99d..93396bf 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -396,6 +396,8 @@ namespace XNode { public void OnBeforeSerialize() { keys.Clear(); values.Clear(); + keys.Capacity = this.Count; + values.Capacity = this.Count; foreach (KeyValuePair pair in this) { keys.Add(pair.Key); values.Add(pair.Value); @@ -404,6 +406,7 @@ namespace XNode { public void OnAfterDeserialize() { this.Clear(); + this.EnsureCapacity(keys.Count); if (keys.Count != values.Count) throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); From a077ca136b59a1a2f2e178f37208fe6c3b072895 Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Wed, 2 Nov 2022 09:08:57 +0100 Subject: [PATCH 02/14] cache the AssemblyQualifiedName string, reduces number of allocations and garbage collection in projects with many nodes --- Scripts/NodeDataCache.cs | 12 ++++++++++++ Scripts/NodePort.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 3bc8478..027218a 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -8,8 +8,20 @@ namespace XNode { public static class NodeDataCache { private static PortDataCache portDataCache; private static Dictionary> formerlySerializedAsCache; + private static Dictionary typeQualifiedNameCache; private static bool Initialized { get { return portDataCache != null; } } + public static string GetTypeQualifiedName(System.Type type) { + if(typeQualifiedNameCache == null) typeQualifiedNameCache = new Dictionary(); + + string name; + if (!typeQualifiedNameCache.TryGetValue(type, out name)) { + name = type.AssemblyQualifiedName; + typeQualifiedNameCache.Add(type, name); + } + return name; + } + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. public static void UpdatePorts(Node node, Dictionary ports) { if (!Initialized) BuildCache(); diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 6bcc638..f8bca99 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -48,7 +48,7 @@ namespace XNode { } set { valueType = value; - if (value != null) _typeQualifiedName = value.AssemblyQualifiedName; + if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); } } private Type valueType; From 5201808eff39a4403b311317fd18cc1fd7302f8d Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Wed, 2 Nov 2022 09:10:15 +0100 Subject: [PATCH 03/14] reuse staticPorts dictionary to reduce number of allocations of memory, clear after every use and ensure capacity when we know we are going to add many entries --- Scripts/NodeDataCache.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 027218a..772478b 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -9,6 +9,7 @@ namespace XNode { private static PortDataCache portDataCache; private static Dictionary> formerlySerializedAsCache; private static Dictionary typeQualifiedNameCache; + private static Dictionary staticPorts; private static bool Initialized { get { return portDataCache != null; } } public static string GetTypeQualifiedName(System.Type type) { @@ -26,7 +27,6 @@ namespace XNode { public static void UpdatePorts(Node node, Dictionary ports) { if (!Initialized) BuildCache(); - Dictionary staticPorts = new Dictionary(); Dictionary> removedPorts = new Dictionary>(); System.Type nodeType = node.GetType(); @@ -37,6 +37,7 @@ namespace XNode { List typePortCache; if (portDataCache.TryGetValue(nodeType, out typePortCache)) { + staticPorts.EnsureCapacity(typePortCache.Count); for (int i = 0; i < typePortCache.Count; i++) { staticPorts.Add(typePortCache[i].fieldName, portDataCache[nodeType][i]); } @@ -105,6 +106,8 @@ namespace XNode { listPort.connectionType = backingPort.connectionType; listPort.typeConstraint = backingPort.typeConstraint; } + + staticPorts.Clear(); } /// @@ -145,6 +148,7 @@ namespace XNode { /// Cache node types private static void BuildCache() { portDataCache = new PortDataCache(); + staticPorts = new Dictionary(); System.Type baseType = typeof(Node); List nodeTypes = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); From bdbb287f00669ff47b4b5dc0949d15cea009db9b Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Wed, 2 Nov 2022 09:11:27 +0100 Subject: [PATCH 04/14] use array instead of list in a enumeration we wont change --- Scripts/NodeDataCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 772478b..966b18f 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -46,7 +46,7 @@ namespace XNode { // Cleanup port dict - Remove nonexisting static ports - update static port types // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. // Loop through current node ports - foreach (NodePort port in ports.Values.ToList()) { + foreach (NodePort port in ports.Values.ToArray()) { // If port still exists, check it it has been changed NodePort staticPort; if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { From f16731395714f3adf29e0ccd2a6b68a877c4c43c Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Wed, 2 Nov 2022 09:12:06 +0100 Subject: [PATCH 05/14] use string substring instead of allocating a array with the two strings in it. --- Scripts/NodeDataCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 966b18f..f97636e 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -97,7 +97,7 @@ namespace XNode { foreach (NodePort listPort in dynamicListPorts) { // At this point we know that ports here are dynamic list ports // which have passed name/"backing port" checks, ergo we can proceed more safely. - string backingPortName = listPort.fieldName.Split(' ')[0]; + string backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' ')); NodePort backingPort = staticPorts[backingPortName]; // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. From f77fa501f164110201402847ac45b50c9e29f941 Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Wed, 2 Nov 2022 09:30:49 +0100 Subject: [PATCH 06/14] removed lookup to list that the tryget is already doing --- Scripts/NodeDataCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index f97636e..f74ea71 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -39,7 +39,7 @@ namespace XNode { if (portDataCache.TryGetValue(nodeType, out typePortCache)) { staticPorts.EnsureCapacity(typePortCache.Count); for (int i = 0; i < typePortCache.Count; i++) { - staticPorts.Add(typePortCache[i].fieldName, portDataCache[nodeType][i]); + staticPorts.Add(typePortCache[i].fieldName, typePortCache[i]); } } From a75f13e7bdf1e3d0ce961ab27f2be744b8148c35 Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Sun, 6 Nov 2022 10:16:26 +0100 Subject: [PATCH 07/14] in most cases the data is already set and with this if check we can skip a dictionary lookup --- Scripts/NodePort.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index f8bca99..b8656ef 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -47,6 +47,7 @@ namespace XNode { return valueType; } set { + if (valueType == value) return; valueType = value; if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); } From 445af6e7031bf5630a1ccbc00650e12af5da9b04 Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Sun, 6 Nov 2022 10:17:07 +0100 Subject: [PATCH 08/14] most new unity packages starts with Unity.xx, this reduces the number of assemblies to look through by ~30 --- Scripts/NodeDataCache.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index f74ea71..08ac91b 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -163,6 +163,7 @@ namespace XNode { // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped case "UnityEditor": case "UnityEngine": + case "Unity": case "System": case "mscorlib": case "Microsoft": From dd62011cfb8c0307bc3f94778f1528c0265986ea Mon Sep 17 00:00:00 2001 From: Thor Brigsted Date: Tue, 22 Nov 2022 10:41:13 +0100 Subject: [PATCH 09/14] Avoid nullrefs on clearing graph --- Scripts/NodeGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index d928f94..347bc8e 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -47,7 +47,7 @@ namespace XNode { public virtual void Clear() { if (Application.isPlaying) { for (int i = 0; i < nodes.Count; i++) { - Destroy(nodes[i]); + if (nodes[i] != null) Destroy(nodes[i]); } } nodes.Clear(); From c7debc2346156ca559341c0860e6aa60af50c938 Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Sun, 27 Nov 2022 15:07:37 +0100 Subject: [PATCH 10/14] Added ifdef 2021_3_or_newer where Unity upgraded to net standard 2.1 where Dictionary.EnsureCapacity is added. --- Scripts/Node.cs | 2 ++ Scripts/NodeDataCache.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 93396bf..62a2f4a 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -406,7 +406,9 @@ namespace XNode { public void OnAfterDeserialize() { this.Clear(); +#if UNITY_2021_3_OR_NEWER this.EnsureCapacity(keys.Count); +#endif if (keys.Count != values.Count) throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 08ac91b..32906a6 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -37,7 +37,9 @@ namespace XNode { List typePortCache; if (portDataCache.TryGetValue(nodeType, out typePortCache)) { +#if UNITY_2021_3_OR_NEWER staticPorts.EnsureCapacity(typePortCache.Count); +#endif for (int i = 0; i < typePortCache.Count; i++) { staticPorts.Add(typePortCache[i].fieldName, typePortCache[i]); } From da0f291a4467b7ca310045c4784f8918da877e7f Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Sun, 27 Nov 2022 15:59:59 +0100 Subject: [PATCH 11/14] converted PortDataCache to a dictionary that holds a dictionary of fieldname and port indexed by type. Removed serialized from PortDataCache as it was not being saved anywhere. --- Scripts/NodeDataCache.cs | 48 +++++++--------------------------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 32906a6..4f4937d 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -9,7 +9,6 @@ namespace XNode { private static PortDataCache portDataCache; private static Dictionary> formerlySerializedAsCache; private static Dictionary typeQualifiedNameCache; - private static Dictionary staticPorts; private static bool Initialized { get { return portDataCache != null; } } public static string GetTypeQualifiedName(System.Type type) { @@ -35,15 +34,10 @@ namespace XNode { List dynamicListPorts = new List(); - List typePortCache; - if (portDataCache.TryGetValue(nodeType, out typePortCache)) { -#if UNITY_2021_3_OR_NEWER - staticPorts.EnsureCapacity(typePortCache.Count); -#endif - for (int i = 0; i < typePortCache.Count; i++) { - staticPorts.Add(typePortCache[i].fieldName, typePortCache[i]); - } - } + Dictionary staticPorts; + if (!portDataCache.TryGetValue(nodeType, out staticPorts)) { + staticPorts = new Dictionary(); + } // Cleanup port dict - Remove nonexisting static ports - update static port types // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. @@ -108,8 +102,6 @@ namespace XNode { listPort.connectionType = backingPort.connectionType; listPort.typeConstraint = backingPort.typeConstraint; } - - staticPorts.Clear(); } /// @@ -150,7 +142,6 @@ namespace XNode { /// Cache node types private static void BuildCache() { portDataCache = new PortDataCache(); - staticPorts = new Dictionary(); System.Type baseType = typeof(Node); List nodeTypes = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); @@ -214,8 +205,9 @@ namespace XNode { if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); else { - if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List()); - portDataCache[nodeType].Add(new NodePort(fieldInfo[i])); + if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new Dictionary()); + NodePort port = new NodePort(fieldInfo[i]); + portDataCache[nodeType].Add(port.fieldName, port); } if (formerlySerializedAsAttribute != null) { @@ -229,30 +221,6 @@ namespace XNode { } [System.Serializable] - private class PortDataCache : Dictionary>, ISerializationCallbackReceiver { - [SerializeField] private List keys = new List(); - [SerializeField] private List> values = new List>(); - - // save the dictionary to lists - public void OnBeforeSerialize() { - keys.Clear(); - values.Clear(); - foreach (var pair in this) { - keys.Add(pair.Key); - values.Add(pair.Value); - } - } - - // load dictionary from lists - public void OnAfterDeserialize() { - this.Clear(); - - if (keys.Count != values.Count) - throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable.")); - - for (int i = 0; i < keys.Count; i++) - this.Add(keys[i], values[i]); - } - } + private class PortDataCache : Dictionary> { } } } From c9685e9155a742a437140f41ae353fb5d4a14d94 Mon Sep 17 00:00:00 2001 From: Trice Helix <81759026+TriceHelix@users.noreply.github.com> Date: Sun, 12 Mar 2023 20:31:00 +0100 Subject: [PATCH 12/14] Fix NullReferenceException - Initialized "culledNodes" field to avoid rare NullReferenceException on line 440 - Slight performance boost using cached HashSets instead of allocating new lists DrawNodes() call, also O(1) lookup time for graphs with many selected/culled nodes --- Scripts/Editor/NodeEditorGUI.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 35b2e2a..8f3a574 100755 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using UnityEditor; @@ -12,8 +12,8 @@ namespace XNodeEditor { /// Contains GUI methods public partial class NodeEditorWindow { public NodeGraphEditor graphEditor; - private List selectionCache; - private List culledNodes; + private readonly HashSet selectionCache = new(); + private readonly HashSet culledNodes = new(); /// 19 if docked, 22 if not private int topPadding { get { return isDocked() ? 19 : 22; } } /// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run. @@ -399,8 +399,13 @@ namespace XNodeEditor { private void DrawNodes() { Event e = Event.current; + if (e.type == EventType.Layout) { - selectionCache = new List(Selection.objects); + selectionCache.Clear(); + var objs = Selection.objects; + selectionCache.EnsureCapacity(objs.Length); + foreach (var obj in objs) + selectionCache.Add(obj); } System.Reflection.MethodInfo onValidate = null; @@ -432,7 +437,7 @@ namespace XNodeEditor { List removeEntries = new List(); - if (e.type == EventType.Layout) culledNodes = new List(); + if (e.type == EventType.Layout) culledNodes.Clear(); for (int n = 0; n < graph.nodes.Count; n++) { // Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable. if (graph.nodes[n] == null) continue; From 111378de945d9c59ef38f0e8a73b5faabea74353 Mon Sep 17 00:00:00 2001 From: guizix <46198159+guizix@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:09:43 +0800 Subject: [PATCH 13/14] Add Attribute PortTypeOverride --- Scripts/Attributes/PortTypeOverrideAttribute.cs | 12 ++++++++++++ Scripts/Attributes/PortTypeOverrideAttribute.cs.meta | 11 +++++++++++ 2 files changed, 23 insertions(+) create mode 100644 Scripts/Attributes/PortTypeOverrideAttribute.cs create mode 100644 Scripts/Attributes/PortTypeOverrideAttribute.cs.meta diff --git a/Scripts/Attributes/PortTypeOverrideAttribute.cs b/Scripts/Attributes/PortTypeOverrideAttribute.cs new file mode 100644 index 0000000..316ca2d --- /dev/null +++ b/Scripts/Attributes/PortTypeOverrideAttribute.cs @@ -0,0 +1,12 @@ +using System; +/// Overrides the ValueType of the Port, to have a ValueType different from the type of its serializable field +/// Especially useful in Dynamic Port Lists to create Value-Port Pairs with different type. +[AttributeUsage(AttributeTargets.Field)] +public class PortTypeOverrideAttribute : Attribute { + public Type type; + /// Overrides the ValueType of the Port + /// ValueType of the Port + public PortTypeOverrideAttribute(Type type) { + this.type = type; + } +} diff --git a/Scripts/Attributes/PortTypeOverrideAttribute.cs.meta b/Scripts/Attributes/PortTypeOverrideAttribute.cs.meta new file mode 100644 index 0000000..a587759 --- /dev/null +++ b/Scripts/Attributes/PortTypeOverrideAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1410c1437e863ab4fac7a7428aaca35b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 556547b54137736e9f802cbb6353585afd9e05b6 Mon Sep 17 00:00:00 2001 From: guizix <46198159+guizix@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:18:29 +0800 Subject: [PATCH 14/14] Enables Port Type Override When Constructing a NodePort, check the PortTypeOverrideAttribute to change the ValueType of the Port. --- Scripts/NodePort.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index b8656ef..1fbc62f 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -79,6 +79,10 @@ namespace XNode { _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; } + // Override ValueType of the Port + if(attribs[i] is PortTypeOverrideAttribute) { + ValueType = (attribs[i] as PortTypeOverrideAttribute).type; + } } }