Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
63 changes: 53 additions & 10 deletions src/Meziantou.Analyzer/Internals/ObjectPool.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable RS1035 // Do not use APIs banned for analyzers
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;

namespace Meziantou.Analyzer.Internals;

Expand Down Expand Up @@ -42,7 +43,7 @@ public static ObjectPool<StringBuilder> CreateStringBuilderPool()
return provider.Create(new StringBuilderPooledObjectPolicy());
}

public static ObjectPool<Queue<T>> CreateQueuePool<T>()
public static ObjectPool<PooledQueue<T>> CreateQueuePool<T>()
{
var provider = new DefaultObjectPoolProvider();
return provider.Create(new QueuePooledObjectPolicy<T>());
Expand Down Expand Up @@ -354,30 +355,72 @@ public override bool Return(StringBuilder obj)
}

/// <summary>
/// A policy for pooling <see cref="Queue{T}"/> instances.
/// A queue that keeps track of the maximum number of items it contained, so a pool can detect the instances whose
/// backing array grew too much. <see cref="Queue{T}.Count"/> cannot be used for that purpose because the consumers
/// usually dequeue all the items before returning the queue to the pool, and <see cref="Queue{T}.Clear"/> does not
/// release the backing array.
/// </summary>
/// <typeparam name="T">The type of the items of the queue.</typeparam>
internal sealed class PooledQueue<T>
{
private readonly Queue<T> _queue = new();

/// <summary>
/// Gets the number of items contained in the queue.
/// </summary>
public int Count => _queue.Count;

/// <summary>
/// Gets the maximum number of items the queue contained since the last call to <see cref="Clear"/>.
/// </summary>
public int MaximumCount { get; private set; }

public void Enqueue(T item)
{
_queue.Enqueue(item);
if (_queue.Count > MaximumCount)
{
MaximumCount = _queue.Count;
}
}

public bool TryDequeue([MaybeNullWhen(false)] out T result)
{
return _queue.TryDequeue(out result);
}

public void Clear()
{
_queue.Clear();
MaximumCount = 0;
}
}

/// <summary>
/// A policy for pooling <see cref="PooledQueue{T}"/> instances.
/// </summary>
/// <typeparam name="T">The type of the items of the pooled queues.</typeparam>
internal sealed class QueuePooledObjectPolicy<T> : PooledObjectPolicy<Queue<T>>
internal sealed class QueuePooledObjectPolicy<T> : PooledObjectPolicy<PooledQueue<T>>
{
/// <summary>
/// Gets or sets the maximum number of items a <see cref="Queue{T}"/> can contain to be retained,
/// when <see cref="Return(Queue{T})"/> is invoked.
/// Gets or sets the maximum number of items a <see cref="PooledQueue{T}"/> can have contained to be retained,
/// when <see cref="Return(PooledQueue{T})"/> is invoked.
/// </summary>
/// <value>Defaults to <c>1024</c>.</value>
public int MaximumRetainedCount { get; set; } = 1024;

/// <inheritdoc />
public override Queue<T> Create()
public override PooledQueue<T> Create()
{
return new Queue<T>();
return new PooledQueue<T>();
}

/// <inheritdoc />
public override bool Return(Queue<T> obj)
public override bool Return(PooledQueue<T> obj)
{
if (obj.Count > MaximumRetainedCount)
if (obj.MaximumCount > MaximumRetainedCount)
{
// Too big. Discard this one.
// The backing array grew too much. Discard this one.
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Meziantou.Analyzer.Rules;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseLangwordInXmlCommentAnalyzer : DiagnosticAnalyzer
{
private static readonly ObjectPool<Queue<SyntaxNode>> NodeQueuePool = ObjectPool.CreateQueuePool<SyntaxNode>();
private static readonly ObjectPool<PooledQueue<SyntaxNode>> NodeQueuePool = ObjectPool.CreateQueuePool<SyntaxNode>();

private static readonly HashSet<string> CSharpKeywords = new(StringComparer.Ordinal)
{
Expand Down
47 changes: 45 additions & 2 deletions tests/Meziantou.Analyzer.Test/Internals/ObjectPoolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public void QueuePooledObjectPolicy_ClearsTheQueueOnReturn()
queue.Enqueue(new object());

Assert.True(policy.Return(queue));
Assert.Empty(queue);
Assert.Equal(0, queue.Count);
}

[Fact]
Expand All @@ -26,6 +26,33 @@ public void QueuePooledObjectPolicy_DoesNotRetainOversizedQueues()
Assert.False(policy.Return(queue));
}

[Fact]
public void QueuePooledObjectPolicy_DoesNotRetainOversizedQueuesThatWereDrained()
{
var policy = new QueuePooledObjectPolicy<object> { MaximumRetainedCount = 1 };
var queue = policy.Create();
queue.Enqueue(new object());
queue.Enqueue(new object());
while (queue.TryDequeue(out _))
{
}

Assert.Equal(0, queue.Count);
Assert.False(policy.Return(queue));
}

[Fact]
public void QueuePooledObjectPolicy_ResetsTheMaximumCountOnReturn()
{
var policy = new QueuePooledObjectPolicy<object> { MaximumRetainedCount = 1 };
var queue = policy.Create();
queue.Enqueue(new object());

Assert.True(policy.Return(queue));
Assert.Equal(0, queue.MaximumCount);
Assert.True(policy.Return(queue));
}

[Fact]
public void QueuePool_DoesNotKeepTheItemsAlive()
{
Expand All @@ -34,6 +61,22 @@ public void QueuePool_DoesNotKeepTheItemsAlive()
queue.Enqueue(new object());
pool.Return(queue);

Assert.Empty(pool.Get());
Assert.Equal(0, pool.Get().Count);
}

[Fact]
public void QueuePool_DoesNotReuseADrainedOversizedQueue()
{
var pool = ObjectPool.Create(new QueuePooledObjectPolicy<object> { MaximumRetainedCount = 1 });
var queue = pool.Get();
queue.Enqueue(new object());
queue.Enqueue(new object());
while (queue.TryDequeue(out _))
{
}

pool.Return(queue);

Assert.NotSame(queue, pool.Get());
}
}