Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1bd875d
feat(benchmarks): update Program to run all benchmarks
engineering87 Nov 23, 2025
449429c
fix(temporal-dictionary): optimize Add method using monotonic timesta…
engineering87 Nov 23, 2025
bdf6d77
chore(deps): update project dependencies
engineering87 Dec 8, 2025
5dd9aa5
test: add more coverage for TemporalSortedList
engineering87 Jan 25, 2026
0ba5bd4
test(temporal-queue): add coverage for edge cases and concurrency sce…
engineering87 Jan 28, 2026
e0af016
test(TemporalStack): add high-value edge-case and concurrency tests
engineering87 Jan 30, 2026
cb2e089
test(temporal-multimap): add edge-case coverage for ranges, DateTime …
engineering87 Feb 8, 2026
9a7435b
test(temporal-priority-queue): add edge-case coverage for empty queue…
engineering87 Feb 8, 2026
56263b1
chore(tests): migrate to xunit.v3
engineering87 Feb 8, 2026
9f00649
chore(deps): update dependencies
engineering87 Feb 23, 2026
8bdfc82
chore(deps): update dependencies
engineering87 Feb 25, 2026
99d3b90
fix: correct GetNearest tie-breaking and TemporalDictionary thread-sa…
engineering87 Feb 28, 2026
9955edd
chore(deps): update project dependencies
engineering87 Mar 20, 2026
b74945b
docs(temporal-circular-buffer): improve inline comments
engineering87 Apr 6, 2026
b87f519
chore(deps): update project dependencies
engineering87 Apr 13, 2026
25469df
chore(deps): update project dependencies
engineering87 May 15, 2026
f424c08
chore(deps): update project dependencies
engineering87 May 29, 2026
af7cd24
fix(collections): correct concurrency races, overflow risk, and dead …
engineering87 Jun 18, 2026
8aaa619
fix(collections): use monotonic sequence tie-breaker and release GC r…
engineering87 Jul 2, 2026
8f0334b
chore(deps): update project dependencies
engineering87 Aug 6, 2026
eb6e9f9
feat(collections): add binary-search range ops and TryDequeue/TryPeek…
engineering87 Aug 20, 2026
c3c29b6
chore(deps): update dependencies
engineering87 Oct 1, 2026
019074a
feat(extensions): add inclusive directional lookup helpers
engineering87 Oct 4, 2026
6cf33d2
feat(extensions): add try directional lookup helpers
engineering87 Oct 4, 2026
f7200e2
feat(extensions): add temporal count before and after queries
engineering87 Oct 4, 2026
3cb9866
feat(extensions): add temporal count before and after queries
engineering87 Oct 4, 2026
b451bce
feat(collections): add TemporalCache with TTL and sliding expiration
engineering87 Oct 22, 2026
5072fef
docs(cache): add TemporalCache benchmarks and README documentation
engineering87 Oct 22, 2026
7f07e15
feat(core): add TimeProvider support across all temporal collections
engineering87 Oct 22, 2026
c2d476f
perf(multimap): use k-way merge for global range queries
engineering87 Oct 23, 2026
d53128a
fix(collections): resolve ordering races, hash collisions and post-tr…
engineering87 Oct 23, 2026
bae2373
fix(collections): handle null priorities and narrow exception handling
engineering87 Oct 24, 2026
fd160f9
feat(interval-tree): add explicit overlap/end-based APIs and document…
engineering87 Oct 24, 2026
71f7218
build(nuget): add package icon
engineering87 Oct 24, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,4 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp
/.github/copilot-instructions.md
89 changes: 83 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<p align="center">
<img src="assets/logo.svg" alt="TemporalCollections logo" width="560">
</p>

# TemporalCollections

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Expand Down Expand Up @@ -60,24 +64,26 @@ At the heart of all collections lies the `TemporalItem<T>` struct:
| TemporalSegmentedArray<T> | Thread-safe time-ordered segmented array optimized for append-in-order workloads and retention. | Yes | Sorted by timestamp (global) | Amortized O(1) append in-order, segment-based range queries and cleanup. |
| TemporalPriorityQueue<T> | Thread-safe priority queue with timestamped items. Supports priority-based dequeueing and queries. | Yes | Priority order | Priority-based ordering with time queries. |
| TemporalIntervalTree<T> | Thread-safe interval tree for timestamped intervals. Efficient overlap queries and interval removals. | Yes | Interval-based | Efficient interval overlap queries and removals. |
| TemporalDictionary<TKey, TValue> | Thread-safe dictionary where each key maps to a timestamped value. Supports add/update, remove, queries.| Yes | Unordered | Key-based access with timestamp tracking and queries. |
| TemporalDictionary<TKey, TValue> | Thread-safe dictionary where each key maps to multiple timestamped values. Supports add, remove, and efficient binary-search range queries.| Yes | Per-key chronological | Key-based access, binary-search range queries and cleanup. |
| TemporalMultimap<TKey, TValue> | Thread-safe multimap where each key maps to multiple timestamped values with global time-based queries. | Yes | Per-key chronological | Multiple values per key, per-key range queries and global time view. |
| TemporalCircularBuffer<T> | Thread-safe fixed-size circular buffer with timestamped items. Overwrites oldest items on overflow. | Yes | FIFO (circular) | Fixed size, efficient overwriting and time queries. |
| TemporalCache<TKey, TValue> | Thread-safe key/value cache with per-entry TTL. Supports absolute or sliding expiration and `TimeProvider`. | Yes | Unordered (by key) | TTL expiration, `GetOrAdd`, `Touch`, time queries over live entries. |

## Usage Guidance
| Collection Name | When to Use | When Not to Use |
|----------------------------------|-------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| TemporalQueue<T> | When you need a thread-safe FIFO queue with time-based retrieval and cleanup. | If you need priority ordering or random access. |
| TemporalQueue<T> | When you need a thread-safe FIFO queue with time-based retrieval and cleanup. Supports `TryDequeue`/`TryPeek` for safe non-throwing access. | If you need priority ordering or random access. |
| TemporalStack<T> | When you want a thread-safe LIFO stack with timestamp tracking and time-range queries. | If you require fast arbitrary removal or sorting by timestamp. |
| TemporalSet<T> | When you need unique timestamped items with efficient membership checks and time-based removal. | If you require ordering of elements or priority queues. |
| TemporalSlidingWindowSet<T> | When you want to automatically retain only recent items within a fixed time window. | If your window size is highly variable or if you need sorted access. |
| TemporalSortedList<T> | When you need a sorted collection by timestamp with efficient range queries. | If insertions are very frequent and performance is critical (due to list shifting). |
| TemporalSegmentedArray<T> | When you ingest events in (mostly) non-decreasing timestamp order and need fast range queries and retention. | If you frequently insert heavily out-of-order, need random removals in the middle, or key lookups. |
| TemporalPriorityQueue<T> | When priority-based ordering with timestamp tracking is required for dequeueing. | If you only need FIFO or LIFO semantics without priorities. |
| TemporalIntervalTree<T> | When you need efficient interval overlap queries and interval-based time operations. | If your data are single points rather than intervals. |
| TemporalDictionary<TKey, TValue> | When key-based access combined with timestamp tracking and querying is needed. | If ordering or range queries by timestamp are required. |
| TemporalDictionary<TKey, TValue> | When key-based access combined with timestamp tracking and efficient range queries are needed. Uses binary search internally for O(log n) range operations.| If you store a single value per key per time point (use TemporalSortedList) or need global sorted access. |
| TemporalMultimap<TKey, TValue> | When each key can have multiple timestamped values and you need per-key queries and/or a global time-ordered view.| If you store a single value per key (use TemporalDictionary) or need ordering by non-time fields.|
| TemporalCircularBuffer<T> | When you want a fixed-size buffer that overwrites oldest items with timestamp tracking. | If you need unbounded storage or complex queries. |
| TemporalCache<TKey, TValue> | When you need a key/value cache whose entries expire after a TTL (absolute or sliding), with time-based queries. | If you need multiple values per key (use TemporalDictionary) or size-based eviction (LRU). |

## ITimeQueryable<T> Interface

Expand Down Expand Up @@ -121,6 +127,24 @@ All temporal collections implement the `ITimeQueryable<T>` interface, which prov
- **GetNearest(DateTimeOffset time)**
Retrieves the item whose timestamp is closest to the specified `time`.

- **GetAtOrBefore(DateTimeOffset time)**
Retrieves the latest item whose timestamp is less than or equal to the specified `time`, or `null` if none exists.

- **TryGetAtOrBefore(DateTimeOffset time, out TemporalItem<T>? item)**
Attempts to retrieve the latest item whose timestamp is less than or equal to the specified `time`.

- **GetAtOrAfter(DateTimeOffset time)**
Retrieves the earliest item whose timestamp is greater than or equal to the specified `time`, or `null` if none exists.

- **TryGetAtOrAfter(DateTimeOffset time, out TemporalItem<T>? item)**
Attempts to retrieve the earliest item whose timestamp is greater than or equal to the specified `time`.

- **CountBefore(DateTimeOffset time)**
Counts the number of items whose timestamp is strictly before the specified `time`.

- **CountAfter(DateTimeOffset time)**
Counts the number of items whose timestamp is strictly after the specified `time`.

These methods collectively support efficient and thread-safe temporal queries and cleanups, allowing each collection to manage its items according to their timestamps while exposing a unified API.

## 🚀 Getting Started with TemporalCollections
Expand All @@ -145,14 +169,22 @@ var queue = new TemporalQueue<string>();
queue.Enqueue("event-1");
queue.Enqueue("event-2");

// Peek oldest (does not remove)
// Peek oldest without removing (throws if empty)
var oldest = queue.Peek();
Console.WriteLine($"Oldest: {oldest.Value} @ {oldest.Timestamp}");

// Dequeue oldest (removes)
// Try-peek: safe non-throwing variant
if (queue.TryPeek(out var peeked))
Console.WriteLine($"Peek: {peeked!.Value} @ {peeked.Timestamp}");

// Dequeue oldest (throws if empty)
var dequeued = queue.Dequeue();
Console.WriteLine($"Dequeued: {dequeued.Value} @ {dequeued.Timestamp}");

// Try-dequeue: safe non-throwing variant
while (queue.TryDequeue(out var item))
Console.WriteLine($"Dequeued: {item!.Value} @ {item.Timestamp}");

// Query by time range (inclusive)
var from = DateTime.UtcNow.AddMinutes(-5);
var to = DateTime.UtcNow;
Expand Down Expand Up @@ -392,6 +424,32 @@ Console.WriteLine($"Overlaps: {string.Join(", ", overlapValues)}");
tree.RemoveOlderThan(now.AddMinutes(9));
```

**TemporalCache<TKey, TValue>**

```csharp
using System;
using TemporalCollections.Collections;

// Entries expire 5 minutes after write; use slidingExpiration: true to extend on read
var cache = new TemporalCache<string, int>(TimeSpan.FromMinutes(5));

cache.Set("user:1", 42);
cache.Set("token", 7, TimeSpan.FromSeconds(30)); // per-entry TTL

if (cache.TryGetValue("user:1", out var value))
Console.WriteLine($"user:1 = {value}");

var computed = cache.GetOrAdd("user:2", _ => 99);
var ttl = cache.GetTimeToLive("token"); // remaining lifetime or null
cache.Touch("user:1"); // reset lifetime

// Eager cleanup of expired entries
int removed = cache.RemoveExpired();

// Time queries on write timestamps of live entries
var recent = cache.GetAfter(DateTime.UtcNow.AddMinutes(-1));
```

### Common queries via `ITimeQueryable<T>`

```csharp
Expand Down Expand Up @@ -422,6 +480,22 @@ This approach ensures:

By enforcing this monotonic timestamp ordering, the temporal collections can rely on consistent time-based queries and maintain correct chronological order of items.

## Custom Clock (`TimeProvider`)
All collections accept an optional `TimeProvider` (.NET 8+) used for timestamps and expiration. Existing constructors keep using `TimeProvider.System`, so current code is unaffected.

```csharp
using Microsoft.Extensions.Time.Testing; // FakeTimeProvider (optional, for tests)

var clock = new FakeTimeProvider(DateTimeOffset.UtcNow);
var window = new TemporalSlidingWindowSet<string>(TimeSpan.FromMinutes(5), clock);

window.Add("a");
clock.Advance(TimeSpan.FromMinutes(6)); // no real waiting
window.RemoveExpired(); // "a" is removed
```

`TemporalItem<T>.Create(value, timeProvider)` is also available. Monotonicity is guaranteed per closed generic type and per `TimeProvider` instance.

## 📈 Performance Benchmarks
We provide detailed performance measurements for all temporal data structures, including insertion, range queries, and removal operations.
The full benchmark results are available here: [docs/benchmarks/benchmarks.md](docs/benchmarks/benchmarks.md)
Expand All @@ -440,16 +514,19 @@ All collections are thread-safe. Locking granularity and common operations (amor
| TemporalSegmentedArray | single lock; segmented storage | O(1) amortized (in-order append) | **O(log n + m)** | O(n) |
| TemporalPriorityQueue | single lock; SortedSet by (priority,time) | O(log n) | O(n) | O(n) |
| TemporalIntervalTree | single lock; interval overlap pruning | O(log n) avg | **O(log n + m)** | O(n) |
| TemporalDictionary | concurrent dict + per-list lock | O(1) avg | O(n) | O(n) |
| TemporalDictionary | concurrent dict + per-list lock | O(1) avg | **O(log n + m)** | O(log k) |
| TemporalMultimap | single lock; per-key ordered lists | O(1) avg | O(n + m log m) | O(n) |
| TemporalCircularBuffer | single lock; ring overwrite | O(1) | O(n) | O(n) |
| TemporalCache | concurrent dict; atomic conditional updates| O(1) avg | O(n + m log m) | O(n) |

`n` = items, `m` = matches, `k` = removed.

## Notes
- **Deterministic ordering**: query results are returned in ascending timestamp order.
- **Snapshot semantics**: methods that return enumerables/lists provide a stable snapshot at call time.
- **Thread-safety**: all operations are designed to be thread-safe per collection.
- **Binary-search range queries**: `TemporalDictionary` and other sorted collections use `LowerBound`/`UpperBound` binary search internally, reducing range queries, `RemoveOlderThan`, `RemoveRange`, `CountInRange`, and `CountSince` from O(n) to O(log n + m).
- **Try-pattern**: `TemporalQueue` exposes `TryDequeue`/`TryPeek` for non-throwing access, consistent with `TemporalPriorityQueue` and standard .NET collection conventions.
- **Intervals**: for interval-based collections, the Timestamp used by this interface refers to the interval start.

⚠️ **Since v1.1.0, internal timestamp storage has been migrated from DateTime to DateTimeOffset (UTC). Public APIs remain DateTime for backward compatibility, but internal semantics are now strictly UTC-aware.**
Expand Down
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 70 additions & 1 deletion docs/benchmarks/benchmarks.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,73 @@
# Performance Testing Summary (Updated – 2025-08-19)
# Performance Testing Summary

## Latest Run (Fast job)

### Environment
- **CPU**: Intel Core Ultra 7 155H, 22 logical / 16 physical cores
- **OS**: Windows 11 25H2
- **Runtime**: .NET 9.0.21, X64 RyuJIT x86-64-v3
- **BenchmarkDotNet**: v0.15.8, `Fast` job (`LaunchCount=1`, `WarmupCount=3`, `IterationCount=10`)
- **Dataset sizes**: 5,000 items per structure; `TemporalSegmentedArray` 20,000 items (`SegmentCapacity` = 256, 1024); `TemporalMultimap` 500 values per key (`KeyCount` = 10, 100)

> ⚠️ These numbers are **not directly comparable** with the 2025 results below (dataset size halved, different job and hardware).
> Run `dotnet run -c Release -- --full` for the full BenchmarkDotNet default job.

All times are **mean in microseconds (µs)**.

### Core structures (5,000 items)

| Structure | AddItems (µs) | QueryRange (µs) | RemoveOlderThan (µs) |
|---|---:|---:|---:|
| TemporalSet | 271.5 | 0.30 | 0.08 |
| TemporalSlidingWindowSet | 292.9 | 0.32 | 0.09 |
| TemporalCircularBuffer | 287.5 | 0.08 | 0.04 |
| TemporalStack | 455.7 | 0.07 | 0.04 |
| TemporalSortedList | 477.6 | 0.07 | 0.04 |
| TemporalIntervalTree | 530.2 | 0.07 | 0.04 |
| TemporalDictionary | 611.7 | 0.11 | 0.08 |
| TemporalPriorityQueue | 5,085.7 | 0.14 | 0.06 |

**TemporalQueue**: `Enqueue` 0.08 µs, `Dequeue` 0.07 µs, `Peek` 0.02 µs, `RemoveOlderThan` 0.01 µs, `GetInRange_Count` 20.5 µs.

### TemporalCache (5,000 items)

| Method | Mean (µs) | Allocated |
|---|---:|---:|
| RemoveExpired scan | 47.0 | 64 B |
| TryGetValue (absolute) | 128.7 | 0 B |
| GetOrAdd | 134.7 | 0 B |
| QueryRange (last 10 minutes) | 196.4 | 131,584 B |
| TryGetValue (sliding) | 265.5 | 200,000 B |
| Set | 484.7 | 440,000 B |

### TemporalSegmentedArray (20,000 items, range across `SegmentCapacity` 256 / 1024)

| Method | Mean (µs) |
|---|---:|
| Clear / Extremes / CountInRange / GetNearest / RemoveRange | 0.8 – 4.5 |
| TrimExcess / ToArray | 11 – 16 |
| GetBefore / GetAfter (midpoint) | 51 – 54 |
| AddSorted (sorted input) | 129 – 134 |
| Add pre-built (sorted) | 441 – 480 |
| AddValue | 1,496 – 1,546 |
| AddRange / Add pre-built (**unsorted** input) | 4,825 – 5,133 |

> Unsorted input is **~35× slower** than `AddSorted`: prefer sorted batches when possible.

### TemporalMultimap (indicative only)

Results show high variance (up to ±60% StdDev on global queries) because of per-iteration setup; they are **not reported as reference values**.
Order of magnitude: per-key removals/queries 1–30 µs, global counts/removals 4–65 µs, inserts 0.2–5 ms, global range/nearest queries 0.4–11 ms.

### Takeaways
- `QueryRange` and `RemoveOlderThan` remain **sub-microsecond** across all core structures.
- `TemporalPriorityQueue` is still the most expensive on inserts (~10× the other structures).
- `TemporalCache` sliding expiration costs ~2× absolute expiration and allocates on every read.
- Extrapolating to 10,000 items (linear scaling, indicative only), `TemporalDictionary`, `TemporalIntervalTree` and `TemporalSortedList` inserts appear faster than in the 2025 run.

---

# Historical Results (2025-08-19)

### Overview
The following benchmarks summarize the performance of different temporal data structures across core operations: **AddItems**, **QueryRange**, and **RemoveOlderThan**.
Expand Down
1 change: 1 addition & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "test": { "runner": "Microsoft.Testing.Platform" } }
Loading
Loading