MemorySizeEstimator.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #nullable enable
  2. namespace Terminal.Gui.ConfigurationTests;
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Concurrent;
  6. using System.Reflection;
  7. using System.Runtime.CompilerServices;
  8. using System.Runtime.InteropServices;
  9. using System.Text;
  10. public static class MemorySizeEstimator
  11. {
  12. public static long EstimateSize<T> (T? source)
  13. {
  14. if (source is null)
  15. {
  16. return 0;
  17. }
  18. ConcurrentDictionary<object, long> visited = new (ReferenceEqualityComparer.Instance);
  19. return EstimateSizeInternal (source, visited);
  20. }
  21. private const int POINTER_SIZE = 8; // 64-bit system
  22. private const int OBJECT_HEADER_SIZE = 16; // 2 pointers for GC
  23. private static long EstimateSizeInternal (object? source, ConcurrentDictionary<object, long> visited)
  24. {
  25. if (source is null)
  26. {
  27. return 0;
  28. }
  29. // Handle already visited objects to avoid circular references
  30. if (visited.TryGetValue (source, out long existingSize))
  31. {
  32. // // Log revisited object (enable for debugging)
  33. // Console.WriteLine($"Revisited {source.GetType().FullName}: {existingSize} bytes");
  34. return existingSize;
  35. }
  36. Type type = source.GetType ();
  37. long size = 0;
  38. // Handle simple types
  39. if (IsSimpleType (type))
  40. {
  41. size = EstimateSimpleTypeSize (source, type);
  42. visited.TryAdd (source, size);
  43. // // Log simple type (enable for debugging)
  44. // Console.WriteLine($"{type.FullName}: {size} bytes");
  45. return size;
  46. }
  47. // Handle arrays
  48. if (type.IsArray)
  49. {
  50. size = EstimateArraySize (source, visited);
  51. }
  52. // Handle dictionaries
  53. else if (source is IDictionary)
  54. {
  55. size = EstimateDictionarySize (source, visited);
  56. }
  57. // Handle collections
  58. else if (typeof (ICollection).IsAssignableFrom (type))
  59. {
  60. size = EstimateCollectionSize (source, visited);
  61. }
  62. // Handle structs and classes
  63. else
  64. {
  65. size = EstimateObjectSize (source, type, visited);
  66. }
  67. visited.TryAdd (source, size);
  68. // // Log object size (enable for debugging)
  69. // if (size == 0)
  70. // {
  71. // Console.WriteLine($"Zero size for {type.FullName}");
  72. // }
  73. // else
  74. // {
  75. // Console.WriteLine($"{type.FullName}: {size} bytes");
  76. // }
  77. return size;
  78. }
  79. private static bool IsSimpleType (Type type)
  80. {
  81. if (type.IsPrimitive
  82. || type.IsEnum
  83. || type == typeof (decimal)
  84. || type == typeof (DateTime)
  85. || type == typeof (DateTimeOffset)
  86. || type == typeof (TimeSpan)
  87. || type == typeof (Guid)
  88. || type == typeof (Rune)
  89. || type == typeof (string))
  90. {
  91. return true;
  92. }
  93. // Treat structs with no writable public properties as simple types
  94. if (type.IsValueType)
  95. {
  96. PropertyInfo [] writableProperties = type.GetProperties (BindingFlags.Instance | BindingFlags.Public)
  97. .Where (p => p is { CanRead: true, CanWrite: true } && p.GetIndexParameters ().Length == 0)
  98. .ToArray ();
  99. return writableProperties.Length == 0;
  100. }
  101. // Treat Property翰Info as simple (metadata, not cloned)
  102. if (typeof (PropertyInfo).IsAssignableFrom (type))
  103. {
  104. return true;
  105. }
  106. return false;
  107. }
  108. private static long EstimateSimpleTypeSize (object source, Type type)
  109. {
  110. if (type == typeof (string))
  111. {
  112. string str = (string)source;
  113. // Header + length (4) + char array ref + chars (2 bytes each)
  114. return OBJECT_HEADER_SIZE + 4 + POINTER_SIZE + (str.Length * 2);
  115. }
  116. try
  117. {
  118. return Marshal.SizeOf (type);
  119. }
  120. catch (ArgumentException)
  121. {
  122. // Fallback for enums or other simple types
  123. return 4; // Conservative estimate
  124. }
  125. }
  126. private static long EstimateArraySize (object source, ConcurrentDictionary<object, long> visited)
  127. {
  128. Array array = (Array)source;
  129. long size = OBJECT_HEADER_SIZE + 4 + POINTER_SIZE; // Header + length + padding
  130. foreach (object? element in array)
  131. {
  132. size += EstimateSizeInternal (element, visited);
  133. }
  134. return size;
  135. }
  136. private static long EstimateDictionarySize (object source, ConcurrentDictionary<object, long> visited)
  137. {
  138. IDictionary dict = (IDictionary)source;
  139. long size = OBJECT_HEADER_SIZE + (POINTER_SIZE * 5); // Header + buckets, entries, comparer, fields
  140. size += dict.Count * 4; // Bucket array (~4 bytes per entry)
  141. size += dict.Count * (4 + 4 + POINTER_SIZE * 2); // Entry array: hashcode, next, key, value
  142. foreach (object? key in dict.Keys)
  143. {
  144. size += EstimateSizeInternal (key, visited);
  145. size += EstimateSizeInternal (dict [key], visited);
  146. }
  147. return size;
  148. }
  149. private static long EstimateCollectionSize (object source, ConcurrentDictionary<object, long> visited)
  150. {
  151. Type type = source.GetType ();
  152. long size = OBJECT_HEADER_SIZE + (POINTER_SIZE * 3); // Header + internal array + fields
  153. if (type.IsGenericType && type.GetGenericTypeDefinition () == typeof (Dictionary<,>))
  154. {
  155. return EstimateDictionarySize (source, visited);
  156. }
  157. if (source is IEnumerable enumerable)
  158. {
  159. foreach (object? item in enumerable)
  160. {
  161. size += EstimateSizeInternal (item, visited);
  162. }
  163. }
  164. return size;
  165. }
  166. private static long EstimateObjectSize (object source, Type type, ConcurrentDictionary<object, long> visited)
  167. {
  168. long size = OBJECT_HEADER_SIZE;
  169. // Size public writable properties
  170. foreach (PropertyInfo prop in type.GetProperties (BindingFlags.Instance | BindingFlags.Public)
  171. .Where (p => p is { CanRead: true, CanWrite: true } && p.GetIndexParameters ().Length == 0))
  172. {
  173. try
  174. {
  175. object? value = prop.GetValue (source);
  176. size += EstimateSizeInternal (value, visited);
  177. }
  178. catch (Exception)
  179. {
  180. // // Log exception (enable for debugging)
  181. // Console.WriteLine($"Error processing property {prop.Name} of {type.FullName}: {ex.Message}");
  182. // Continue to avoid crashing
  183. }
  184. }
  185. // For structs, also size fields (to handle generic structs)
  186. if (type.IsValueType)
  187. {
  188. FieldInfo [] fields = type.GetFields (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
  189. foreach (FieldInfo field in fields)
  190. {
  191. try
  192. {
  193. object? fieldValue = field.GetValue (source);
  194. size += EstimateSizeInternal (fieldValue, visited);
  195. }
  196. catch (Exception)
  197. {
  198. // // Log exception (enable for debugging)
  199. // Console.WriteLine($"Error processing field {field.Name} of {type.FullName}: {ex.Message}");
  200. // Continue to avoid crashing
  201. }
  202. }
  203. }
  204. return size;
  205. }
  206. private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
  207. {
  208. public static ReferenceEqualityComparer Instance { get; } = new ();
  209. public new bool Equals (object? x, object? y)
  210. {
  211. return ReferenceEquals (x, y);
  212. }
  213. public int GetHashCode (object obj)
  214. {
  215. return RuntimeHelpers.GetHashCode (obj);
  216. }
  217. }
  218. }