feat: Adds tidy option for serialization. Codegens ballotbox. Fixes pooled timer leaking (#681)

* Fixes pooled timer leaking
* Fixes `[dumptimers` command so it outputs properly, adds spacing, and stacktraces
* Adds `[Tidy]` for serializing Lists. This will remove deleted entities during world save before serializing the list.
* Adds helpers for managing Lists/Sets/Dictionaries


### New API

```cs
// Creates the list if it is null, then adds
Utility.Add(ref list, value);
Utility.Add(ref set, value);
Utility.Add(ref dict, key, value);

// Nulls the variable if the count is zero
Utility.Remove(ref list, value);
Utility.Remove(ref set, value);
Utility.Remove(ref dict, key);

// Marks entity as dirty in addition to doing the action
entity.Add(list, value);
// Marks entity as dirty, and will create list if it doesn't exist
entity.Add(ref list, value);

// Marks entity as dirty in addition to doing the action
entity.Remove(list, value);
// Marks entity as dirty, and will null the list count is zero
entity.Remove(ref list, value);
```


### Updates to [dumptimers
<img width="825" alt="Screen Shot 2021-08-14 at 2 55 10 AM" src="https://user-images.githubusercontent.com/3953314/129442449-ccf7fe14-29d6-4f3f-9366-c8eb7b9828a7.png">
This commit is contained in:
Kamron Batman 2021-08-14 03:02:53 -07:00 • committed by GitHub
parent 84cbd52a2a
commit 360143478a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 627 additions and 464 deletions

View file

@ -22,7 +22,7 @@ namespace Server.Collections
private int _version;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PooledRefQueue<T> Create() => new(32);
public static PooledRefQueue<T> Create(int capacity = 32) => new(capacity);
// Creates a queue with room for capacity objects. The default grow factor
// is used.

View file

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using Server.Collections;
using Server.Logging;
using Server.Network;
using Server.Utilities;
using QueuePool = Server.Utilities.RefPool<Server.Utilities.QueueRef<Server.Items.Container>>;
namespace Server.Items
{
@ -16,7 +15,6 @@ namespace Server.Items
public class Container : Item
{
private static readonly QueuePool m_QueuePool = new(QueueRef<Container>.Generate, 2, 5);
private static readonly List<Item> m_FindItemsList = new();
private ContainerData m_ContainerData;
@ -1299,8 +1297,7 @@ namespace Server.Items
{
var consumed = 0;
var toDelete = new Queue<Item>();
using var toDelete = PooledRefQueue<Item>.Create();
RecurseConsumeUpTo(this, type, amount, recurse, ref consumed, toDelete);
while (toDelete.Count > 0)
@ -1313,7 +1310,7 @@ namespace Server.Items
private static void RecurseConsumeUpTo(
Item current, Type type, int amount, bool recurse, ref int consumed,
Queue<Item> toDelete
PooledRefQueue<Item> toDelete
)
{
if (current == null || current.Items.Count == 0)
@ -1721,28 +1718,26 @@ namespace Server.Items
/// </returns>
public List<T> FindItemsByType<T>(bool recurse = true, Predicate<T> predicate = null) where T : Item
{
using (var queue = m_QueuePool.Get())
using var queue = PooledRefQueue<Container>.Create(128);
queue.Enqueue(this);
var items = new List<T>();
while (queue.Count > 0)
{
queue.Enqueue(this);
var items = new List<T>();
while (queue.Count > 0)
var container = queue.Dequeue();
foreach (var item in container.Items)
{
var container = queue.Dequeue();
foreach (var item in container.Items)
if (item is T typedItem && predicate?.Invoke(typedItem) != false)
{
if (item is T typedItem && predicate?.Invoke(typedItem) != false)
{
items.Add(typedItem);
}
else if (recurse && item is Container itemContainer)
{
queue.Enqueue(itemContainer);
}
items.Add(typedItem);
}
else if (recurse && item is Container itemContainer)
{
queue.Enqueue(itemContainer);
}
}
return items;
}
return items;
}
/// <summary>
@ -1765,28 +1760,26 @@ namespace Server.Items
/// </returns>
public T FindItemByType<T>(bool recurse = true, Predicate<T> predicate = null) where T : Item
{
using (var queue = m_QueuePool.Get())
using var queue = PooledRefQueue<Container>.Create(128);
queue.Enqueue(this);
while (queue.Count > 0)
{
queue.Enqueue(this);
while (queue.Count > 0)
var container = queue.Dequeue();
foreach (var item in container.Items)
{
var container = queue.Dequeue();
foreach (var item in container.Items)
if (item is T typedItem && predicate?.Invoke(typedItem) != false)
{
if (item is T typedItem && predicate?.Invoke(typedItem) != false)
{
return typedItem;
}
return typedItem;
}
if (recurse && item is Container itemContainer)
{
queue.Enqueue(itemContainer);
}
if (recurse && item is Container itemContainer)
{
queue.Enqueue(itemContainer);
}
}
return null;
}
return null;
}
private class GroupComparer : IComparer<Item>

View file

@ -124,7 +124,7 @@ namespace Server.Network
public static void Initialize()
{
Timer.StartTimer(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1.5), CheckAllAlive);
Timer.DelayCall(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1.5), CheckAllAlive);
}
public NetState(ISocket connection)

View file

@ -66,76 +66,28 @@ namespace Server
public int Y { get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Add<T>(ref List<T> list, T value)
{
list ??= new List<T>();
list.Add(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Remove<T>(ref List<T> list, T value)
{
if (list != null)
{
list.Remove(value);
if (list.Count == 0)
{
list = null;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Replace<T>(ref List<T> list, T oldValue, T newValue)
{
if (oldValue != null && newValue != null)
{
var index = list?.IndexOf(oldValue) ?? -1;
if (index >= 0)
{
list![index] = newValue;
}
else
{
Add(ref list, newValue);
}
}
else if (oldValue != null)
{
Remove(ref list, oldValue);
}
else if (newValue != null)
{
Add(ref list, newValue);
}
}
public void OnClientChange(NetState oldState, NetState newState)
{
Replace(ref m_Clients, oldState, newState);
Utility.Replace(ref m_Clients, oldState, newState);
}
public void OnEnter(Item item)
{
Add(ref m_Items, item);
Utility.Add(ref m_Items, item);
}
public void OnLeave(Item item)
{
Remove(ref m_Items, item);
Utility.Remove(ref m_Items, item);
}
public void OnEnter(Mobile mob)
{
Add(ref m_Mobiles, mob);
Utility.Add(ref m_Mobiles, mob);
if (mob.NetState != null)
{
Add(ref m_Clients, mob.NetState);
Utility.Add(ref m_Clients, mob.NetState);
Owner.ActivateSectors(X, Y);
}
@ -143,11 +95,11 @@ namespace Server
public void OnLeave(Mobile mob)
{
Remove(ref m_Mobiles, mob);
Utility.Remove(ref m_Mobiles, mob);
if (mob.NetState != null)
{
Remove(ref m_Clients, mob.NetState);
Utility.Remove(ref m_Clients, mob.NetState);
Owner.DeactivateSectors(X, Y);
}
@ -155,7 +107,7 @@ namespace Server
public void OnEnter(Region region, Rectangle3D rect)
{
Add(ref m_RegionRects, new RegionRect(region, rect));
Utility.Add(ref m_RegionRects, new RegionRect(region, rect));
m_RegionRects.Sort();
@ -200,12 +152,12 @@ namespace Server
public void OnMultiEnter(BaseMulti multi)
{
Add(ref m_Multis, multi);
Utility.Add(ref m_Multis, multi);
}
public void OnMultiLeave(BaseMulti multi)
{
Remove(ref m_Multis, multi);
Utility.Remove(ref m_Multis, multi);
}
public void Activate()

View file

@ -20,7 +20,7 @@ namespace Server
/// <summary>
/// Hints to the source generator that this field or property should be serialized.
/// When used on a field, the source generator will generate the property entirely.
/// When used on a property, the user must call ((ISerializable)this).MarkDirty().
/// When used on a property, the user must call this.MarkDirty() after reassigning the value or modifying the value internally.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class SerializableFieldAttribute : Attribute

View file

@ -0,0 +1,27 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: TidyAttribute.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
namespace Server
{
/// <summary>
/// Hints to the source generator that a serializable list should be tidied up
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class TidyAttribute : Attribute
{
}
}

View file

@ -20,7 +20,7 @@ namespace Server
{
public interface ISerializable
{
long SavePosition { get; protected set; }
long SavePosition { get; protected internal set; }
BufferWriter SaveBuffer { get; protected internal set; }
int TypeRef { get; }
Serial Serial { get; }
@ -29,11 +29,6 @@ namespace Server
void Delete();
bool Deleted { get; }
void MarkDirty()
{
SavePosition = -1;
}
void SetTypeRef(Type type);
public void InitializeSaveBuffer(byte[] buffer)

View file

@ -0,0 +1,135 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: ISerializableExtensions.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Server
{
public static class ISerializableExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MarkDirty(this ISerializable entity)
{
entity.SavePosition = -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(this ISerializable entity, ICollection<T> list, T value)
{
list.Add(value);
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<K, V>(this ISerializable entity, IDictionary<K, V> dict, K key, V value)
{
dict[key] = value;
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Insert<T>(this ISerializable entity, IList<T> list, T value, int index)
{
list.Insert(index, value);
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Remove<T>(this ISerializable entity, ICollection<T> list, T value)
{
if (list.Remove(value))
{
entity.MarkDirty();
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Remove<K, V>(this ISerializable entity, IDictionary<K, V> dict, K key, out V value)
{
if (dict.Remove(key, out value))
{
entity.MarkDirty();
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RemoveAt<T>(this ISerializable entity, IList<T> list, int index)
{
list.RemoveAt(index);
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear<T>(this ISerializable entity, ICollection<T> list)
{
list.Clear();
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(this ISerializable entity, ref List<T> list, T value)
{
Utility.Add(ref list, value);
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<K, V>(this ISerializable entity, ref Dictionary<K, V> dict, K key, V value)
{
Utility.Add(ref dict, key, value);
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Remove<T>(this ISerializable entity, ref List<T> list, T value)
{
if (Utility.Remove(ref list, value))
{
entity.MarkDirty();
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear<T>(this ISerializable entity, ref List<T> list)
{
Utility.Clear(ref list);
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear<T>(this ISerializable entity, ref HashSet<T> set)
{
Utility.Clear(ref set);
entity.MarkDirty();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear<K, V>(this ISerializable entity, ref Dictionary<K, V> dict)
{
Utility.Clear(ref dict);
entity.MarkDirty();
}
}
}

View file

@ -133,7 +133,7 @@ namespace Server
internal DelayCallTimer(TimeSpan delay) : base(delay)
{
#if DEBUG_TIMERS
t._allowFinalization = true;
_allowFinalization = true;
#endif
Start();
}
@ -164,12 +164,15 @@ namespace Server
Version++; // Increment the version so if this is called from OnTick() and another timer is started, we don't have a problem
#if DEBUG_TIMERS
_stackTraces.Remove(GetHashCode());
#endif
if (_poolCount >= _poolCapacity)
{
#if DEBUG_TIMERS
logger.Warning($"DelayCallTimer pool reached maximum of {_poolSize} timers");
logger.Warning($"DelayCallTimer pool reached maximum of {_poolCapacity} timers");
_allowFinalization = true;
_stackTraces.Remove(GetHashCode());
#endif
return;
}
@ -184,7 +187,7 @@ namespace Server
{
_poolCount--;
#if DEBUG_TIMERS
logger.Information($"Pool count changed: {_poolCount} ({_poolCapacity})");
logger.Information($"Pool count: {_poolCount} / {_poolCapacity}");
#endif
var timer = GetFromPool();
@ -202,7 +205,7 @@ namespace Server
_timerPoolDepletionAmount++;
#if DEBUG_TIMERS
logger.Warning($"Timer pool depleted and timer was allocated.\n{new StackTrace()});
logger.Warning($"Timer pool depleted and timer was allocated.\n{new StackTrace()}");
#endif
return new DelayCallTimer(delay, interval, count, callback);
}

View file

@ -62,7 +62,7 @@ namespace Server
_poolHead = head;
_poolCount += amount;
#if DEBUG_TIMERS
logger.Information($"Pool count changed: {_poolCount} ({_poolCapacity})");
logger.Information($"Pool count: {_poolCount} / {_poolCapacity}");
#endif
}

View file

@ -203,9 +203,7 @@ namespace Server
public static void DumpInfo(TextWriter tw)
{
var now = DateTime.UtcNow;
tw.WriteLine("Date: {0}\n", now);
tw.WriteLine("Date: {0}\n", Core.Now.ToLocalTime());
tw.WriteLine("Pool - Count: {0}; Size {1}\n", _poolCount - _timerPoolDepletionAmount, _poolCapacity);
var total = 0.0;
@ -216,6 +214,10 @@ namespace Server
for (var j = 0; j < _ringSize; j++)
{
var t = _rings[i][j];
if (t == null)
{
continue;
}
var name = t.ToString();
@ -230,9 +232,22 @@ namespace Server
foreach (var (name, count) in hash.OrderByDescending(o => o.Value))
{
tw.WriteLine($"- Type: {name}; Count: {count}; Percent: {count / total}%");
var percent = count / total;
var line = $"{count:#,0} ({percent:P1})";
// 6 - 15 / 8 = 1
var tabs = new string('\t', line.Length < 12 ? 2 : 1);
tw.WriteLine($"{line}{tabs}{name}");
}
#if DEBUG_TIMERS
tw.WriteLine("\nStack Traces:");
foreach (var kvp in DelayCallTimer._stackTraces)
{
tw.WriteLine(kvp.Value);
tw.WriteLine();
}
#endif
tw.WriteLine();
tw.WriteLine();
}

View file

@ -1,165 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: RefPool.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
namespace Server.Utilities
{
/// <summary> A resource reference object that can be disposed. </summary>
/// <remarks>
/// Disposing the reference is expected to return itself back into the
/// original pool that created it.
/// </remarks>
public interface IRef : IDisposable
{
}
/// <summary>
/// Base implementation of the <see cref="IRef" /> interface.
/// </summary>
/// <remarks>
/// New implementations of <see cref="IRef" /> should either derive from, or mirror
/// the functionality of this base implementation.
/// </remarks>
/// <typeparam name="TDerived"></typeparam>
public abstract class BaseRef<TDerived> : IRef where TDerived : IRef
{
private readonly RefPool<TDerived> m_Pool;
public BaseRef(RefPool<TDerived> pool) => m_Pool = pool;
public void Dispose()
{
OnDispose();
m_Pool.Return((TDerived)(object)this);
}
protected abstract void OnDispose();
}
/// <summary>
/// A resource reference pool that manages a collection of reusable resources.
/// </summary>
/// <typeparam name="TRef">The <see cref="IRef" /> resource type the pool will contain.</typeparam>
public class RefPool<TRef> where TRef : IRef
{
public delegate TRef Generator(RefPool<TRef> targetPool);
public const int DEFAULT_RESOURCE_RETENTION = 10;
private readonly Generator m_Generator;
private readonly Stack<TRef> m_Resources = new();
private int m_MaxRefrenceRetention;
/// <param name="generator">The generator function for creating new resources.</param>
/// <param name="preGenerateCount">
/// An amount of resources that should be pre-generated during initialization of the resource
/// pool.
/// </param>
public RefPool(Generator generator, int preGenerateCount = 0, int maxRefrenceRetention = DEFAULT_RESOURCE_RETENTION)
{
if (generator == null)
{
throw new ArgumentNullException(nameof(generator));
}
if (preGenerateCount > maxRefrenceRetention)
{
throw new IndexOutOfRangeException(
$"{nameof(preGenerateCount)} greater than {nameof(maxRefrenceRetention)}"
);
}
m_Generator = generator;
m_MaxRefrenceRetention = maxRefrenceRetention;
while (--preGenerateCount >= 0)
{
m_Resources.Push(generator(this));
}
}
/// <summary>
/// The maximum number of unused resources to hold in the pool.
/// </summary>
public int MaxRefrenceRetention
{
get => m_MaxRefrenceRetention;
set
{
m_MaxRefrenceRetention = value;
while (m_Resources.Count > value)
{
m_Resources.Pop();
}
}
}
/// <summary>
/// Retrieves a resource reference that is managed by this <see cref="RefPool{TRef}" />. If the pool is has unused
/// resources,
/// it will remove one from the pool and return it; otherwise, a new resource will be generated.
/// </summary>
/// <returns>Unused resource, or a new resource if no unused resources available.</returns>
public TRef Get() => m_Resources.TryPop(out var item) ? item : m_Generator(this);
/// <summary>
/// Returns a resource reference to the pool of unused resources.
/// </summary>
/// <param name="queueRef">Resource to be returned.</param>
public void Return(TRef queueRef)
{
if (m_Resources.Count < MaxRefrenceRetention)
{
m_Resources.Push(queueRef);
}
}
}
public class QueueRef<T> : Queue<T>, IRef
{
/// <summary>
/// Generator function for creating instances of the <see cref="QueueRef{T}" /> resource.
/// </summary>
public static RefPool<QueueRef<T>>.Generator Generate = targetPool => new QueueRef<T>(targetPool);
private readonly RefPool<QueueRef<T>> m_Pool;
private QueueRef(RefPool<QueueRef<T>> pool) => m_Pool = pool;
/// <summary>Clears the queue and returns this resource to its parent resource pool.</summary>
public void Dispose()
{
Clear();
m_Pool.Return(this);
}
}
public class StackRef<T> : Stack<T>, IRef
{
/// <summary>
/// Generator function for creating instances of the <see cref="StackRef{T}" /> resource.
/// </summary>
public static RefPool<StackRef<T>>.Generator Generate = targetPool => new StackRef<T>(targetPool);
private readonly RefPool<StackRef<T>> m_Pool;
private StackRef(RefPool<StackRef<T>> pool) => m_Pool = pool;
/// <summary>Clears the stack and returns this resource to its parent resource pool.</summary>
public void Dispose()
{
Clear();
m_Pool.Return(this);
}
}
}

View file

@ -1492,5 +1492,159 @@ namespace Server
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetTimeStamp() => Core.Now.ToString("yyyy-MM-dd-HH-mm-ss");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(ref List<T> list, T value)
{
list ??= new List<T>();
list.Add(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(ref HashSet<T> set, T value)
{
set ??= new HashSet<T>();
set.Add(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<K, V>(ref Dictionary<K, V> dict, K key, V value)
{
dict ??= new Dictionary<K, V>();
dict.Add(key, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Remove<T>(ref List<T> list, T value)
{
if (list != null)
{
var removed = list.Remove(value);
if (list.Count == 0)
{
list = null;
}
return removed;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Remove<T>(ref HashSet<T> set, T value)
{
if (set != null)
{
var removed = set.Remove(value);
if (set.Count == 0)
{
set = null;
}
return removed;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Remove<K, V>(ref Dictionary<K, V> dict, K key)
{
if (dict != null)
{
var removed = dict.Remove(key);
if (dict.Count == 0)
{
dict = null;
}
return removed;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Remove<K, V>(ref Dictionary<K, V> dict, K key, out V value)
{
if (dict != null)
{
var removed = dict.Remove(key, out value);
if (dict.Count == 0)
{
dict = null;
}
return removed;
}
value = default;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Replace<T>(ref List<T> list, T oldValue, T newValue)
{
if (oldValue != null && newValue != null)
{
var index = list?.IndexOf(oldValue) ?? -1;
if (index >= 0)
{
list![index] = newValue;
}
else
{
Add(ref list, newValue);
}
}
else if (oldValue != null)
{
Remove(ref list, oldValue);
}
else if (newValue != null)
{
Add(ref list, newValue);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Replace<K, V>(ref Dictionary<K, V> dict, K key, V oldValue, V newValue)
{
if (newValue != null)
{
Add(ref dict, key, newValue);
}
else if (oldValue != null)
{
Remove(ref dict, key);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear<T>(ref List<T> list)
{
list.Clear();
list = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear<T>(ref HashSet<T> set)
{
set.Clear();
set = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear<K, V>(ref Dictionary<K, V> dict)
{
dict.Clear();
dict = null;
}
}
}