Skip to content

Commit

Permalink
Merge pull request #26 from chickensoft-games/feat/boxless-queue
Browse files Browse the repository at this point in the history
feat: boxless queue
  • Loading branch information
jolexxa committed Jun 6, 2024
2 parents b0cd3dc + 8474049 commit a88ada1
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 1 deletion.
62 changes: 62 additions & 0 deletions Chickensoft.Collections.Tests/src/BoxlessQueueTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Chickensoft.Collections.Tests;

using System.Collections.Generic;
using Chickensoft.Collections;
using Shouldly;
using Xunit;

public class BoxlessQueueTests {
public readonly record struct ValueA;
public readonly record struct ValueB;

public class TestValueHandler : IBoxlessValueHandler {
public List<object> Values { get; } = [];
public void HandleValue<TValue>(in TValue value) where TValue : struct =>
Values.Add(value);
}

[Fact]
public void Initializes() {
var handler = new TestValueHandler();
var queue = new BoxlessQueue(handler);

queue.Handler.ShouldBe(handler);
}

[Fact]
public void EnqueueAndHandleValues() {
var handler = new TestValueHandler();
var queue = new BoxlessQueue(handler);

var valueA = new ValueA();
var valueA2 = new ValueA();
var valueB = new ValueB();

queue.Enqueue(valueA);
queue.Enqueue(valueA2);
queue.Enqueue(valueB);

queue.HasValues.ShouldBeTrue();

queue.Dequeue();
queue.Dequeue();
queue.Dequeue();

queue.HasValues.ShouldBeFalse();
queue.Dequeue();
handler.Values.ShouldBe(new object[] { valueA, valueA2, valueB });
}

[Fact]
public void ClearQueue() {
var handler = new TestValueHandler();
var queue = new BoxlessQueue(handler);

queue.Enqueue(new ValueA());
queue.Enqueue(new ValueB());

queue.Clear();

queue.HasValues.ShouldBeFalse();
}
}
98 changes: 98 additions & 0 deletions Chickensoft.Collections/src/boxless_queue/BoxlessQueue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
namespace Chickensoft.Collections;

using System;
using System.Collections.Generic;

/// <summary>
/// <para>
/// Queue that can store multiple types of structs without boxing them. It
/// does this by quietly creating a new queue whenever it sees a new value type,
/// drastically reducing heap allocations.
/// </para>
/// <para>
/// This is built around standard queues, so it takes advantage of the internal
/// capacity of the queue and all of its resizing functionality at the expense
/// of a little additional memory usage if many struct types are seen by the
/// queue. This trade-off allows the queue to drastically reduce
/// the amount of memory churn caused by boxing and unboxing values and/or
/// allocating lambdas to capture generic contexts.
/// </para>
/// <para>
/// Adapted from https://stackoverflow.com/a/6164880.
/// </para>
/// </summary>
/// <remarks>
/// Creates a new boxless queue that does not box or unbox values.
/// </remarks>
/// <param name="handler"><inheritdoc cref="Handler" path="/summary"/>
/// </param>
public class BoxlessQueue(IBoxlessValueHandler handler) {
private abstract class TypedValueQueue {
public abstract void HandleValue(IBoxlessValueHandler handler);
public abstract void Clear();
}

private class TypedMessageQueue<T> : TypedValueQueue where T : struct {
private readonly Queue<T> _queue = new();

public void Enqueue(T message) => _queue.Enqueue(message);

public override void HandleValue(IBoxlessValueHandler handler) =>
handler.HandleValue(_queue.Dequeue());

public override void Clear() => _queue.Clear();
}

/// <summary>
/// Object that implements <see cref="IBoxlessValueHandler"/>. Whenever a
/// value is dequeued, this object will be invoked with the value. This keeps
/// structs from being boxed and unboxed when they are used, drastically
/// reducing heap allocations.
/// </summary>
public IBoxlessValueHandler Handler { get; } = handler;
private readonly Queue<Type> _queueSelectorQueue = new();
private readonly Dictionary<Type, TypedValueQueue> _queues = [];

/// <summary>
/// Add a value to the queue without boxing it.
/// </summary>
/// <typeparam name="TValue">The type of value to enqueue.</typeparam>
public void Enqueue<TValue>(TValue message) where TValue : struct {
TypedMessageQueue<TValue> queue;

if (!_queues.ContainsKey(typeof(TValue))) {
queue = new TypedMessageQueue<TValue>();
_queues[typeof(TValue)] = queue;
}
else {
queue = (TypedMessageQueue<TValue>)_queues[typeof(TValue)];
}

queue.Enqueue(message);
_queueSelectorQueue.Enqueue(typeof(TValue));
}

/// <summary>
/// Returns whether the boxless queue has any values.
/// </summary>
public bool HasValues => _queueSelectorQueue.Count > 0;

/// <summary>
/// Handle the next value in the queue, if any. This will dequeue the next
/// value and invoke the <see cref="Handler"/> with it.
/// </summary>
public void Dequeue() {
if (!HasValues) { return; }
var type = _queueSelectorQueue.Dequeue();
_queues[type].HandleValue(Handler);
}

/// <summary>Clear all values from the queue.</summary>
public void Clear() {
_queueSelectorQueue.Clear();

foreach (var queue in _queues.Values) {
queue.Clear();
}
}
}
15 changes: 15 additions & 0 deletions Chickensoft.Collections/src/boxless_queue/IBoxlessValueHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Chickensoft.Collections;

/// <summary>
/// Interface for handling values stored in a <see cref="BoxlessQueue"/>.
/// </summary>
public interface IBoxlessValueHandler {
/// <summary>
/// Callback invoked when a value is dequeued from a
/// <see cref="BoxlessQueue"/>.
/// </summary>
/// <param name="value">Value that was dequeued.</param>
/// <typeparam name="TValue">Type of the value.</typeparam>
void HandleValue<TValue>(in TValue value)
where TValue : struct;
}
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,39 @@ if (table.Get<object>("identifier") is { } value) {
}
```

## Boxless Queue

The boxless queue allows you to queue struct values on the heap without boxing them, and dequeue them without needing to unbox them.

To do so, you must make an object which implements the `IBoxlessValueHandler` interface. The `HandleValue` method will be invoked whenever the boxless queue dequeues a value.

```csharp
public class MyValueHandler : IBoxlessValueHandler {
public void HandleValue<TValue>(in TValue value) where TValue : struct {
Console.WriteLine($"Received value {value}");
}
}
```

Once you have implemented the `IBoxlessValueHandler`, you can create a boxless queue.

```csharp
var handler = new MyValueHandler();

var queue = new BoxlessQueue(handler);

// Add something to the queue.
queue.Enqueue(valueA);

// See if anything is in the queue.
if (queue.HasValues) {
Console.WriteLine("Something in the queue.");
}

// Take something out of the queue. Calls our value handler.
queue.Dequeue();
```

---

🐣 Created with love by Chickensoft 🐤 — <https://chickensoft.games>
Expand Down
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"words": [
"assemblyfilters",
"automerge",
"Boxless",
"branchcoverage",
"brandedoutcast",
"buildtransitive",
Expand Down Expand Up @@ -50,6 +51,7 @@
"reportgenerator",
"reporttypes",
"Shouldly",
"struct",
"subfolders",
"targetargs",
"targetdir",
Expand All @@ -61,4 +63,4 @@
"Unparented",
"Xunit"
]
}
}

0 comments on commit a88ada1

Please sign in to comment.