diff --git a/mzLib/MzLibUtil/MzLibUtil.csproj b/mzLib/MzLibUtil/MzLibUtil.csproj index c6b5cf526..ae8fef5ea 100644 --- a/mzLib/MzLibUtil/MzLibUtil.csproj +++ b/mzLib/MzLibUtil/MzLibUtil.csproj @@ -14,6 +14,7 @@ + diff --git a/mzLib/MzLibUtil/MzLibUtil.csproj.DotSettings b/mzLib/MzLibUtil/MzLibUtil.csproj.DotSettings new file mode 100644 index 000000000..52f9c2892 --- /dev/null +++ b/mzLib/MzLibUtil/MzLibUtil.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/mzLib/MzLibUtil/ObjectPools/DictionaryPool.cs b/mzLib/MzLibUtil/ObjectPools/DictionaryPool.cs new file mode 100644 index 000000000..0e3efec5c --- /dev/null +++ b/mzLib/MzLibUtil/ObjectPools/DictionaryPool.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; + +namespace MzLibUtil; + +// Example Usage: +// var pool = new DictionaryPool(); +// var dictionary = pool.Get(); +// try { +// dictionary.Add(1,1); +// Do Work +// } +// finally { +// pool.Return(dictionary); +// } + +/// +/// Provides a pool for instances to reduce memory allocations. +/// This class uses the from Microsoft.Extensions.ObjectPool +/// to manage the pooling of objects. +/// +/// The type of keys in the . +/// The type of values in the . +/// +/// This class is not thread-safe and should not be shared between threads. +/// This class should be pulled from outside a try finally loop and finally should return the Dictionary to the pool to ensure proper pooling in the case of a caught exception. +/// +public class DictionaryPool where TKey : notnull +{ + private readonly ObjectPool> _pool; + + /// + /// Initializes a new instance of the class. + /// + /// Initial capacity for the pooled Dictionary instances. + public DictionaryPool(int initialCapacity = 16) + { + var policy = new DictionaryPooledObjectPolicy(initialCapacity); + var provider = new DefaultObjectPoolProvider { MaximumRetained = Environment.ProcessorCount * 2 }; + _pool = provider.Create(policy); + } + + /// + /// Retrieves a Dictionary instance from the pool. + /// + /// A Dictionary instance. + public Dictionary Get() => _pool.Get(); + + /// + /// Returns a Dictionary instance back to the pool. + /// + /// The Dictionary instance to return. + public void Return(Dictionary dictionary) + { + if (dictionary == null) throw new ArgumentNullException(nameof(dictionary)); + dictionary.Clear(); // Ensure the Dictionary is clean before returning it to the pool + _pool.Return(dictionary); + } + + /// + /// Policy for pooling Dictionary instances with a specified initial capacity. + /// + /// The type of keys in the Dictionary. + /// The type of values in the Dictionary. + /// The initial capacity for the pooled Dictionary instances. + private class DictionaryPooledObjectPolicy(int initialCapacity) + : PooledObjectPolicy> + where TKeyItem : notnull + { + private int InitialCapacity { get; } = initialCapacity; + + /// + /// Creates a new Dictionary instance with the specified initial capacity. + /// + /// A new Dictionary instance. + public override Dictionary Create() + { + return new Dictionary(capacity: InitialCapacity); + } + + /// + /// Returns a Dictionary instance to the pool after clearing it. + /// + /// The Dictionary instance to return. + /// True if the Dictionary instance can be reused; otherwise, false. + public override bool Return(Dictionary obj) + { + // Ensure the Dictionary can be safely reused + obj.Clear(); + return true; + } + } +} diff --git a/mzLib/MzLibUtil/ObjectPools/HashSetPool.cs b/mzLib/MzLibUtil/ObjectPools/HashSetPool.cs new file mode 100644 index 000000000..ae33ffd99 --- /dev/null +++ b/mzLib/MzLibUtil/ObjectPools/HashSetPool.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; + +namespace MzLibUtil; + + +// Example Usage: +// var pool = new HashSetPool(); +// var hashSet = pool.Get(); +// try { +// hashSet.Add(1); +// Do Work +// } +// finally { +// pool.Return(hashSet); +// } + +/// +/// Provides a pool for instances to reduce memory allocations. +/// This class uses the from Microsoft.Extensions.ObjectPool +/// to manage the pooling of objects. +/// +/// The type of elements in the . +/// +/// This class is not thread-safe and should not be shared between threads. +/// This class should be pulled from outside a try finally loop and finally should return the HashSet to the pool to ensure proper pooling in the case of a caught exception +/// See example found in DigestionAgent.GetDigestionSiteIndices() for proper usage +/// +public class HashSetPool +{ + private readonly ObjectPool> _pool; + + /// + /// Initializes a new instance of the class. + /// + /// Initial capacity for the pooled HashSet instances. + public HashSetPool(int initialCapacity = 16) + { + var policy = new HashSetPooledObjectPolicy(initialCapacity); + _pool = new DefaultObjectPool>(policy); + } + + /// + /// Retrieves a instance from the pool. + /// + /// A instance. + public HashSet Get() => _pool.Get(); + + /// + /// Returns a instance back to the pool. + /// + /// The instance to return. + public void Return(HashSet hashSet) + { + if (hashSet == null) throw new ArgumentNullException(nameof(hashSet)); + hashSet.Clear(); // Ensure the HashSet is clean before returning it to the pool + _pool.Return(hashSet); + } + + /// + /// Defines the policy for creating and returning instances to the pool. + /// + /// The type of elements in the . + private class HashSetPooledObjectPolicy(int initialCapacity) : PooledObjectPolicy> + { + /// + /// Creates a new instance with the specified initial capacity. + /// + /// A new instance. + public override HashSet Create() + { + return new HashSet(capacity: initialCapacity); + } + + /// + /// Returns a instance to the pool after clearing it. + /// + /// The instance to return. + /// Always returns true. + public override bool Return(HashSet obj) + { + // Ensure the HashSet can be safely reused + obj.Clear(); + return true; + } + } +} diff --git a/mzLib/MzLibUtil/ObjectPools/ListPool.cs b/mzLib/MzLibUtil/ObjectPools/ListPool.cs new file mode 100644 index 000000000..9f25a7926 --- /dev/null +++ b/mzLib/MzLibUtil/ObjectPools/ListPool.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; + +namespace MzLibUtil; + +// Example Usage: +// var pool = new ListPool(); +// var list = pool.Get(); +// try { +// list.Add(1); +// Do Work +// } +// finally { +// pool.Return(list); +// } + +/// +/// Provides a pool for instances to reduce memory allocations. +/// This class uses the from Microsoft.Extensions.ObjectPool +/// to manage the pooling of objects. +/// +/// The type of elements in the . +/// +/// This class is not thread-safe and should not be shared between threads. +/// This class should be pulled from outside a try finally loop and finally should return the List to the pool to ensure proper pooling in the case of a caught exception. +/// +public class ListPool +{ + private readonly ObjectPool> _pool; + + /// + /// Initializes a new instance of the class. + /// + /// Initial capacity for the pooled HashSet instances. + public ListPool(int initialCapacity = 16) + { + var policy = new ListPooledObjectPolicy(initialCapacity); + var provider = new DefaultObjectPoolProvider { MaximumRetained = Environment.ProcessorCount * 2 }; + _pool = provider.Create(policy); + } + + /// + /// Retrieves a HashSet instance from the pool. + /// + /// A HashSet instance. + public List Get() => _pool.Get(); + + /// + /// Returns a HashSet instance back to the pool. + /// + /// The HashSet instance to return. + public void Return(List list) + { + if (list == null) throw new ArgumentNullException(nameof(list)); + list.Clear(); // Ensure the HashSet is clean before returning it to the pool + _pool.Return(list); + } + + /// + /// Policy for pooling List instances with a specified initial capacity. + /// + /// The type of elements in the List. + /// The initial capacity for the pooled List instances. + private class ListPooledObjectPolicy(int initialCapacity) : PooledObjectPolicy> + { + private int InitialCapacity { get; } = initialCapacity; + + /// + /// Creates a new List instance with the specified initial capacity. + /// + /// A new List instance. + public override List Create() + { + return new List(capacity: InitialCapacity); + } + + /// + /// Resets the List instance to a clean state before returning it to the pool. + /// + /// The List instance to reset and return. + /// True if the List instance can be returned to the pool; otherwise, false. + public override bool Return(List obj) + { + // Ensure the List can be safely reused + obj.Clear(); + return true; + } + } +} \ No newline at end of file diff --git a/mzLib/Omics/Digestion/DigestionAgent.cs b/mzLib/Omics/Digestion/DigestionAgent.cs index 88794de06..734d5e65f 100644 --- a/mzLib/Omics/Digestion/DigestionAgent.cs +++ b/mzLib/Omics/Digestion/DigestionAgent.cs @@ -1,9 +1,12 @@ -using Omics.Modifications; +using MzLibUtil; +using Omics.Modifications; namespace Omics.Digestion { public abstract class DigestionAgent { + protected static readonly HashSetPool HashSetPool = new HashSetPool(8); + protected DigestionAgent(string name, CleavageSpecificity cleavageSpecificity, List motifList, Modification cleavageMod) { Name = name; @@ -73,40 +76,48 @@ protected static bool ValidMaxLength(int? length, int maxLength) /// public List GetDigestionSiteIndices(string sequence) { - var indices = new List(); - - for (int r = 0; r < sequence.Length; r++) + var indices = HashSetPool.Get(); // use hash set to ensure no duplicates + try // Try block is to ensure that, even if an error gets thrown, the hashset is returned to the pool { - var cutSiteIndex = -1; - bool cleavagePrevented = false; + indices.Add(0); // The start of the protein is treated as a cleavage site to retain the n-terminal peptide - foreach (DigestionMotif motif in DigestionMotifs) + for (int r = 0; r < sequence.Length; r++) { - var motifResults = motif.Fits(sequence, r); - bool motifFits = motifResults.Item1; - bool motifPreventsCleavage = motifResults.Item2; + var cutSiteIndex = -1; + bool cleavagePrevented = false; - if (motifFits && r + motif.CutIndex < sequence.Length) + foreach (DigestionMotif motif in DigestionMotifs) { - cutSiteIndex = Math.Max(r + motif.CutIndex, cutSiteIndex); + var motifResults = motif.Fits(sequence, r); + bool motifFits = motifResults.Item1; + bool motifPreventsCleavage = motifResults.Item2; + + if (motifFits && r + motif.CutIndex < sequence.Length) + { + cutSiteIndex = Math.Max(r + motif.CutIndex, cutSiteIndex); + } + + if (motifPreventsCleavage) // if any motif prevents cleave + { + cleavagePrevented = true; + } } - if (motifPreventsCleavage) // if any motif prevents cleave + // if no motif prevents cleave + if (!cleavagePrevented && cutSiteIndex != -1) { - cleavagePrevented = true; + indices.Add(cutSiteIndex); } } - // if no motif prevents cleave - if (!cleavagePrevented && cutSiteIndex != -1) - { - indices.Add(cutSiteIndex); - } + indices.Add(sequence.Length); // The end of the protein is treated as a cleavage site to retain the c-terminal peptide + return indices.ToList(); // convert the hashset to a list for return. + } + finally + { + // return hashset to pool. This clears it and gets it ready for the next time it is needed from the pool. + HashSetPool.Return(indices); } - - indices.Add(0); // The start of the protein is treated as a cleavage site to retain the n-terminal peptide - indices.Add(sequence.Length); // The end of the protein is treated as a cleavage site to retain the c-terminal peptide - return indices.Distinct().OrderBy(i => i).ToList(); } } } diff --git a/mzLib/Test/ObjectPoolTests.cs b/mzLib/Test/ObjectPoolTests.cs new file mode 100644 index 000000000..bac0600eb --- /dev/null +++ b/mzLib/Test/ObjectPoolTests.cs @@ -0,0 +1,120 @@ +using MzLibUtil; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Test; + +[TestFixture] +[ExcludeFromCodeCoverage] +public class HashSetPoolTests +{ + [Test] + public void Get_ReturnsHashSetInstance() + { + var pool = new HashSetPool(); + var hashSet = pool.Get(); + Assert.That(hashSet, Is.Not.Null); + pool.Return(hashSet); + } + + [Test] + public void Return_ClearsHashSetBeforeReturningToPool() + { + var pool = new HashSetPool(); + var hashSet = pool.Get(); + hashSet.Add(1); + pool.Return(hashSet); + Assert.That(hashSet.Count, Is.EqualTo(0)); + } + + [Test] + public void Return_ThrowsArgumentNullException_WhenHashSetIsNull() + { + var pool = new HashSetPool(); + Assert.Throws(() => pool.Return(null)); + } +} + +[TestFixture] +[ExcludeFromCodeCoverage] +public class DictionaryPoolTests +{ + [Test] + public void Get_ReturnsDictionaryInstance() + { + var dictionaryPool = new DictionaryPool(); + var dictionary = dictionaryPool.Get(); + Assert.That(dictionary, Is.Not.Null); + Assert.That(dictionary, Is.InstanceOf>()); + } + + [Test] + public void Return_ClearsAndReturnsDictionaryToPool() + { + var dictionaryPool = new DictionaryPool(); + var dictionary = dictionaryPool.Get(); + dictionary["key"] = 42; + + dictionaryPool.Return(dictionary); + + Assert.That(dictionary.Count, Is.EqualTo(0)); + } + + [Test] + public void Return_ThrowsArgumentNullException_WhenDictionaryIsNull() + { + var dictionaryPool = new DictionaryPool(); + Assert.Throws(() => dictionaryPool.Return(null)); + } +} + +[TestFixture] +[ExcludeFromCodeCoverage] +public class ListPoolTests +{ + [Test] + public void ListPool_Get_ReturnsListWithInitialCapacity() + { + // Arrange + int initialCapacity = 16; + var listPool = new ListPool(initialCapacity); + + // Act + var list = listPool.Get(); + + // Assert + Assert.That(list, Is.Not.Null); + Assert.That(list.Capacity, Is.EqualTo(initialCapacity)); + } + + [Test] + public void ListPool_Return_ClearsListBeforeReturningToPool() + { + // Arrange + var listPool = new ListPool(); + var list = listPool.Get(); + list.Add(1); + list.Add(2); + + // Act + listPool.Return(list); + var returnedList = listPool.Get(); + + // Assert + Assert.That(returnedList, Is.Not.Null); + Assert.That(returnedList, Is.Empty); + } + + [Test] + public void ListPool_Return_ThrowsArgumentNullException_WhenListIsNull() + { + // Arrange + var listPool = new ListPool(); + + // Act & Assert + Assert.That(() => listPool.Return(null), Throws.ArgumentNullException); + } +} + diff --git a/mzLib/mzLib.nuspec b/mzLib/mzLib.nuspec index 6cc79db4b..b9c326d5d 100644 --- a/mzLib/mzLib.nuspec +++ b/mzLib/mzLib.nuspec @@ -23,6 +23,7 @@ + @@ -36,6 +37,7 @@ +