From f0b7882f6e7ad77e7565653ffde41c0bf94744fe Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Wed, 2 Nov 2022 09:07:22 +0100 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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 c7debc2346156ca559341c0860e6aa60af50c938 Mon Sep 17 00:00:00 2001 From: Simon Rodriguez Date: Sun, 27 Nov 2022 15:07:37 +0100 Subject: [PATCH 09/10] 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 10/10] 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> { } } }