Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object Pooling #822

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
58 changes: 58 additions & 0 deletions mzLib/MzLibUtil/ObjectPools/DictionaryPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;

namespace MzLibUtil;

// Used to pool HashSet instances to reduce memory allocations
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);
}

private class DictionaryPooledObjectPolicy<TKeyItem, TValueItem>(int initialCapacity)
: PooledObjectPolicy<Dictionary<TKeyItem, TValueItem>>
where TKeyItem : notnull
{
private int InitialCapacity { get; } = initialCapacity;

public override Dictionary<TKeyItem, TValueItem> Create()
{
return new Dictionary<TKeyItem, TValueItem>(capacity: InitialCapacity);
}

public override bool Return(Dictionary<TKeyItem, TValueItem> obj)
{
// Ensure the Dictionary can be safely reused
obj.Clear();
return true;
}
}
}
60 changes: 60 additions & 0 deletions mzLib/MzLibUtil/ObjectPools/HashSetPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;

namespace MzLibUtil;

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

// Used to pool HashSet instances to reduce memory allocations
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 HashSet instance from the pool.
/// </summary>
/// <returns>A HashSet instance.</returns>
public HashSet<T> Get() => _pool.Get();

/// <summary>
/// Returns a HashSet instance back to the pool.
/// </summary>
/// <param name="hashSet">The HashSet 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);
}

private class HashSetPooledObjectPolicy<TItem>(int initialCapacity) : PooledObjectPolicy<HashSet<TItem>>
{
public override HashSet<TItem> Create()
{
return new HashSet<TItem>(capacity: initialCapacity);
}

public override bool Return(HashSet<TItem> obj)
{
// Ensure the HashSet can be safely reused
obj.Clear();
return true;
}
}
}
56 changes: 56 additions & 0 deletions mzLib/MzLibUtil/ObjectPools/ListPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;

namespace MzLibUtil;

// Used to pool HashSet instances to reduce memory allocations
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);
}

private class ListPooledObjectPolicy<TItem>(int initialCapacity) : PooledObjectPolicy<List<TItem>>
{
private int InitialCapacity { get; } = initialCapacity;

public override List<TItem> Create()
{
return new List<TItem>(capacity: InitialCapacity);
}

public override bool Return(List<TItem> obj)
{
// Ensure the HashSet can be safely reused
obj.Clear();
return true;
}
}
}
56 changes: 33 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,47 @@ 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
{
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();
}
finally
{
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();
}
}
}
120 changes: 120 additions & 0 deletions mzLib/Test/ObjectPoolTests.cs
Original file line number Diff line number Diff line change
@@ -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<int>();
var hashSet = pool.Get();
Assert.That(hashSet, Is.Not.Null);
pool.Return(hashSet);
}

[Test]
public void Return_ClearsHashSetBeforeReturningToPool()
{
var pool = new HashSetPool<int>();
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<int>();
Assert.Throws<ArgumentNullException>(() => pool.Return(null));
}
}

[TestFixture]
[ExcludeFromCodeCoverage]
public class DictionaryPoolTests
{
[Test]
public void Get_ReturnsDictionaryInstance()
{
var dictionaryPool = new DictionaryPool<string, int>();
var dictionary = dictionaryPool.Get();
Assert.That(dictionary, Is.Not.Null);
Assert.That(dictionary, Is.InstanceOf<Dictionary<string, int>>());
}

[Test]
public void Return_ClearsAndReturnsDictionaryToPool()
{
var dictionaryPool = new DictionaryPool<string, int>();
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<string, int>();
Assert.Throws<ArgumentNullException>(() => dictionaryPool.Return(null));
}
}

[TestFixture]
[ExcludeFromCodeCoverage]
public class ListPoolTests
{
[Test]
public void ListPool_Get_ReturnsListWithInitialCapacity()
{
// Arrange
int initialCapacity = 16;
var listPool = new ListPool<int>(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<int>();
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<int>();

// Act & Assert
Assert.That(() => listPool.Return(null), Throws.ArgumentNullException);
}
}

Loading
Loading