Skip to content

Commit

Permalink
Object Pooling (#822)
Browse files Browse the repository at this point in the history
* Added bassic object pools

* Refactor DigestionAgent to use HashSetPool for indices
  • Loading branch information
nbollis authored Jan 15, 2025
1 parent 5443e36 commit 331ee1d
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 23 deletions.
1 change: 1 addition & 0 deletions mzLib/MzLibUtil/MzLibUtil.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Easy.Common" Version="6.7.0" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
</ItemGroup>

Expand Down
2 changes: 2 additions & 0 deletions mzLib/MzLibUtil/MzLibUtil.csproj.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=objectpools/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
94 changes: 94 additions & 0 deletions mzLib/MzLibUtil/ObjectPools/DictionaryPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;

namespace MzLibUtil;

// Example Usage:
// var pool = new DictionaryPool<int, int>();
// var dictionary = pool.Get();
// try {
// dictionary.Add(1,1);
// Do Work
// }
// finally {
// pool.Return(dictionary);
// }

/// <summary>
/// Provides a pool for <see cref="Dictionary{TKey, TValue}"/> instances to reduce memory allocations.
/// This class uses the <see cref="ObjectPool{T}"/> from Microsoft.Extensions.ObjectPool
/// to manage the pooling of <see cref="Dictionary{TKey, TValue}"/> objects.
/// </summary>
/// <typeparam name="TKey">The type of keys in the <see cref="Dictionary{TKey, TValue}"/>.</typeparam>
/// <typeparam name="TValue">The type of values in the <see cref="Dictionary{TKey, TValue}"/>.</typeparam>
/// <remarks>
/// 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.
/// </remarks>
public class DictionaryPool<TKey, TValue> where TKey : notnull
{
private readonly ObjectPool<Dictionary<TKey, TValue>> _pool;

/// <summary>
/// Initializes a new instance of the <see cref="DictionaryPool{TKey, TValue}"/> class.
/// </summary>
/// <param name="initialCapacity">Initial capacity for the pooled Dictionary instances.</param>
public DictionaryPool(int initialCapacity = 16)
{
var policy = new DictionaryPooledObjectPolicy<TKey, TValue>(initialCapacity);
var provider = new DefaultObjectPoolProvider { MaximumRetained = Environment.ProcessorCount * 2 };
_pool = provider.Create(policy);
}

/// <summary>
/// Retrieves a Dictionary instance from the pool.
/// </summary>
/// <returns>A Dictionary instance.</returns>
public Dictionary<TKey, TValue> Get() => _pool.Get();

/// <summary>
/// Returns a Dictionary instance back to the pool.
/// </summary>
/// <param name="dictionary">The Dictionary instance to return.</param>
public void Return(Dictionary<TKey, TValue> 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);
}

/// <summary>
/// Policy for pooling Dictionary instances with a specified initial capacity.
/// </summary>
/// <typeparam name="TKeyItem">The type of keys in the Dictionary.</typeparam>
/// <typeparam name="TValueItem">The type of values in the Dictionary.</typeparam>
/// <param name="initialCapacity">The initial capacity for the pooled Dictionary instances.</param>
private class DictionaryPooledObjectPolicy<TKeyItem, TValueItem>(int initialCapacity)
: PooledObjectPolicy<Dictionary<TKeyItem, TValueItem>>
where TKeyItem : notnull
{
private int InitialCapacity { get; } = initialCapacity;

/// <summary>
/// Creates a new Dictionary instance with the specified initial capacity.
/// </summary>
/// <returns>A new Dictionary instance.</returns>
public override Dictionary<TKeyItem, TValueItem> Create()
{
return new Dictionary<TKeyItem, TValueItem>(capacity: InitialCapacity);
}

/// <summary>
/// Returns a Dictionary instance to the pool after clearing it.
/// </summary>
/// <param name="obj">The Dictionary instance to return.</param>
/// <returns>True if the Dictionary instance can be reused; otherwise, false.</returns>
public override bool Return(Dictionary<TKeyItem, TValueItem> obj)
{
// Ensure the Dictionary can be safely reused
obj.Clear();
return true;
}
}
}
88 changes: 88 additions & 0 deletions mzLib/MzLibUtil/ObjectPools/HashSetPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;

namespace MzLibUtil;


// Example Usage:
// var pool = new HashSetPool<int>();
// var hashSet = pool.Get();
// try {
// hashSet.Add(1);
// Do Work
// }
// finally {
// pool.Return(hashSet);
// }

/// <summary>
/// Provides a pool for <see cref="HashSet{T}"/> instances to reduce memory allocations.
/// This class uses the <see cref="ObjectPool{T}"/> from Microsoft.Extensions.ObjectPool
/// to manage the pooling of <see cref="HashSet{T}"/> objects.
/// </summary>
/// <typeparam name="T">The type of elements in the <see cref="HashSet{T}"/>.</typeparam>
/// <remarks>
/// 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
/// </remarks>
public class HashSetPool<T>
{
private readonly ObjectPool<HashSet<T>> _pool;

/// <summary>
/// Initializes a new instance of the <see cref="HashSetPool{T}"/> class.
/// </summary>
/// <param name="initialCapacity">Initial capacity for the pooled HashSet instances.</param>
public HashSetPool(int initialCapacity = 16)
{
var policy = new HashSetPooledObjectPolicy<T>(initialCapacity);
_pool = new DefaultObjectPool<HashSet<T>>(policy);
}

/// <summary>
/// Retrieves a <see cref="HashSet{T}"/> instance from the pool.
/// </summary>
/// <returns>A <see cref="HashSet{T}"/> instance.</returns>
public HashSet<T> Get() => _pool.Get();

/// <summary>
/// Returns a <see cref="HashSet{T}"/> instance back to the pool.
/// </summary>
/// <param name="hashSet">The <see cref="HashSet{T}"/> instance to return.</param>
public void Return(HashSet<T> 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);
}

/// <summary>
/// Defines the policy for creating and returning <see cref="HashSet{T}"/> instances to the pool.
/// </summary>
/// <typeparam name="TItem">The type of elements in the <see cref="HashSet{T}"/>.</typeparam>
private class HashSetPooledObjectPolicy<TItem>(int initialCapacity) : PooledObjectPolicy<HashSet<TItem>>
{
/// <summary>
/// Creates a new <see cref="HashSet{T}"/> instance with the specified initial capacity.
/// </summary>
/// <returns>A new <see cref="HashSet{T}"/> instance.</returns>
public override HashSet<TItem> Create()
{
return new HashSet<TItem>(capacity: initialCapacity);
}

/// <summary>
/// Returns a <see cref="HashSet{T}"/> instance to the pool after clearing it.
/// </summary>
/// <param name="obj">The <see cref="HashSet{T}"/> instance to return.</param>
/// <returns>Always returns true.</returns>
public override bool Return(HashSet<TItem> obj)
{
// Ensure the HashSet can be safely reused
obj.Clear();
return true;
}
}
}
90 changes: 90 additions & 0 deletions mzLib/MzLibUtil/ObjectPools/ListPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;

namespace MzLibUtil;

// Example Usage:
// var pool = new ListPool<int>();
// var list = pool.Get();
// try {
// list.Add(1);
// Do Work
// }
// finally {
// pool.Return(list);
// }

/// <summary>
/// Provides a pool for <see cref="List{T}"/> instances to reduce memory allocations.
/// This class uses the <see cref="ObjectPool{T}"/> from Microsoft.Extensions.ObjectPool
/// to manage the pooling of <see cref="List{T}"/> objects.
/// </summary>
/// <typeparam name="T">The type of elements in the <see cref="List{T}"/>.</typeparam>
/// <remarks>
/// 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.
/// </remarks>
public class ListPool<T>
{
private readonly ObjectPool<List<T>> _pool;

/// <summary>
/// Initializes a new instance of the <see cref="ListPool{T}"/> class.
/// </summary>
/// <param name="initialCapacity">Initial capacity for the pooled HashSet instances.</param>
public ListPool(int initialCapacity = 16)
{
var policy = new ListPooledObjectPolicy<T>(initialCapacity);
var provider = new DefaultObjectPoolProvider { MaximumRetained = Environment.ProcessorCount * 2 };
_pool = provider.Create(policy);
}

/// <summary>
/// Retrieves a HashSet instance from the pool.
/// </summary>
/// <returns>A HashSet instance.</returns>
public List<T> Get() => _pool.Get();

/// <summary>
/// Returns a HashSet instance back to the pool.
/// </summary>
/// <param name="list">The HashSet instance to return.</param>
public void Return(List<T> 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);
}

/// <summary>
/// Policy for pooling List instances with a specified initial capacity.
/// </summary>
/// <typeparam name="TItem">The type of elements in the List.</typeparam>
/// <param name="initialCapacity">The initial capacity for the pooled List instances.</param>
private class ListPooledObjectPolicy<TItem>(int initialCapacity) : PooledObjectPolicy<List<TItem>>
{
private int InitialCapacity { get; } = initialCapacity;

/// <summary>
/// Creates a new List instance with the specified initial capacity.
/// </summary>
/// <returns>A new List instance.</returns>
public override List<TItem> Create()
{
return new List<TItem>(capacity: InitialCapacity);
}

/// <summary>
/// Resets the List instance to a clean state before returning it to the pool.
/// </summary>
/// <param name="obj">The List instance to reset and return.</param>
/// <returns>True if the List instance can be returned to the pool; otherwise, false.</returns>
public override bool Return(List<TItem> obj)
{
// Ensure the List can be safely reused
obj.Clear();
return true;
}
}
}
57 changes: 34 additions & 23 deletions mzLib/Omics/Digestion/DigestionAgent.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Omics.Modifications;
using MzLibUtil;
using Omics.Modifications;

namespace Omics.Digestion
{
public abstract class DigestionAgent
{
protected static readonly HashSetPool<int> HashSetPool = new HashSetPool<int>(8);

protected DigestionAgent(string name, CleavageSpecificity cleavageSpecificity, List<DigestionMotif> motifList, Modification cleavageMod)
{
Name = name;
Expand Down Expand Up @@ -73,40 +76,48 @@ protected static bool ValidMaxLength(int? length, int maxLength)
/// <returns></returns>
public List<int> GetDigestionSiteIndices(string sequence)
{
var indices = new List<int>();

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();
}
}
}
Loading

0 comments on commit 331ee1d

Please sign in to comment.