< Summary

Line coverage
100%
Covered lines: 193
Uncovered lines: 0
Coverable lines: 193
Total lines: 674
Line coverage: 100%
Branch coverage
100%
Covered branches: 92
Total branches: 92
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Collection/SwiftTicketMap.cs

#LineLine coverage
 1//=======================================================================
 2// SwiftTicketMap.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using System;
 9using System.Collections;
 10using System.Collections.Generic;
 11using System.Runtime.CompilerServices;
 12using System.Text.Json.Serialization;
 13using Chronicler;
 14using Chronicler.Diagnostics;
 15using Chronicler.Serialization;
 16using MemoryPack;
 17using SwiftCollections.Utility;
 18
 19namespace SwiftCollections;
 20
 21/// <summary>
 22/// Represents a high-performance ticket map that assigns stable tickets to stored items.
 23/// </summary>
 24/// <remarks>
 25/// <para>
 26/// <see cref="SwiftTicketMap{T}"/> is similar to <see cref="SwiftSlotMap{T}"/> but adds
 27/// <b>generation tracking</b> to prevent stale references from accessing reused slots.
 28/// </para>
 29///
 30/// <para>
 31/// When an item is added, a <see cref="SwiftTicket"/> containing both an index and generation
 32/// is returned. If the item is removed and the slot reused later, the generation value
 33/// changes, causing older tickets to automatically become invalid.
 34/// </para>
 35///
 36/// <para>
 37/// This pattern is widely used to safely reference objects without risking accidental access
 38/// to recycled memory slots.
 39/// </para>
 40///
 41/// <para>
 42/// Key characteristics:
 43/// <list type="bullet">
 44/// <item><description>O(1) insertion and removal.</description></item>
 45/// <item><description>Stable tickets for the lifetime of stored items.</description></item>
 46/// <item><description>Automatic invalidation of stale tickets via generation counters.</description></item>
 47/// <item><description>Cache-friendly contiguous storage.</description></item>
 48/// </list>
 49/// </para>
 50///
 51/// <para>
 52/// Use <see cref="SwiftSlotMap{T}"/> when raw indices are acceptable.
 53/// Use <see cref="SwiftTicketMap{T}"/> when ticket safety is required.
 54/// </para>
 55/// </remarks>
 56/// <typeparam name="T">Specifies the type of elements stored in the map.</typeparam>
 57[Serializable]
 58[JsonConverter(typeof(StateJsonConverterFactory))]
 59[MemoryPackable]
 60public sealed partial class SwiftTicketMap<T> : IStateBacked<SwiftTicketMapState<T>>, ISwiftCloneable<T>, IEnumerable<T>
 61{
 62    #region Nested Types
 63
 64    private struct Entry
 65    {
 66        public uint Generation;
 67        public bool IsUsed;
 68        public T Value;
 69    }
 70
 71    #endregion
 72
 73    #region Constants
 74
 75    /// <summary>
 76    /// Represents the default initial capacity for the collection.
 77    /// </summary>
 78    /// <remarks>
 79    /// Use this constant when initializing the collection to its default size.
 80    /// The value is typically used to optimize memory allocation for small collections.
 81    /// </remarks>
 82    public const int DefaultCapacity = 8;
 83
 84    #endregion
 85
 86    #region Fields
 87
 88    private Entry[] _entries;
 89    private SwiftIntStack _freeIndices;
 90
 91    private int _count;
 92    private int _peak;
 93
 94    private uint _version;
 95
 96    #endregion
 97
 98    #region Constructors
 99
 100    /// <summary>
 101    /// Initializes a new instance of the SwiftTicketMap class with the default capacity.
 102    /// </summary>
 84103    public SwiftTicketMap() : this(DefaultCapacity) { }
 104
 105    /// <summary>
 106    /// Initializes a new instance of the SwiftTicketMap class with the specified initial capacity.
 107    /// </summary>
 108    /// <remarks>
 109    /// The actual capacity will be set to the next power of two greater than or equal to the specified capacity,
 110    /// or to the default capacity if the specified value is too small.
 111    /// This ensures efficient internal storage and lookup performance.
 112    /// </remarks>
 113    /// <param name="capacity">
 114    /// The initial number of elements that the map can contain.
 115    /// If less than or equal to the default capacity, the default capacity is used.
 116    /// Must be a non-negative integer.
 117    /// </param>
 52118    public SwiftTicketMap(int capacity)
 119    {
 52120        capacity = capacity <= DefaultCapacity
 52121            ? DefaultCapacity
 52122            : SwiftHashTools.NextPowerOfTwo(capacity);
 123
 52124        _entries = new Entry[capacity];
 52125        _freeIndices = new SwiftIntStack(capacity);
 52126    }
 127
 128    ///  <summary>
 129    ///  Initializes a new instance of the <see cref="SwiftTicketMap{T}"/> class with the specified <see cref="SwiftTick
 130    ///  </summary>
 131    ///  <param name="state">The state containing the internal array, count, offset, and version for initialization.</pa
 132    [MemoryPackConstructor]
 15133    public SwiftTicketMap(SwiftTicketMapState<T> state)
 134    {
 15135        _entries = Array.Empty<Entry>();
 15136        _freeIndices = new SwiftIntStack(0);
 137
 15138        State = state;
 14139    }
 140
 141    #endregion
 142
 143    #region Properties
 144
 145    /// <summary>
 146    /// Gets the number of elements contained in the collection.
 147    /// </summary>
 148    [JsonIgnore]
 149    [MemoryPackIgnore]
 13150    public int Count => _count;
 151
 152    /// <summary>
 153    /// Gets the total number of elements that the internal data structure can hold without resizing.
 154    /// </summary>
 155    /// <remarks>
 156    /// This value represents the allocated size of the underlying storage, which may be greater than the actual number 
 157    /// Capacity is always greater than or equal to the current count of elements.
 158    /// </remarks>
 159    [JsonIgnore]
 160    [MemoryPackIgnore]
 8161    public int Capacity => _entries.Length;
 162
 163    /// <summary>
 164    /// Gets or sets the current state of the ticket map.
 165    /// </summary>
 166    /// <remarks>
 167    /// This property provides a snapshot of the map's internal state, which can be used for serialization, diagnostics,
 168    /// or restoring the map to a previous state.
 169    /// Setting this property replaces the entire state of the map, including its contents and allocation metadata.
 170    /// </remarks>
 171    [JsonInclude]
 172    [MemoryPackInclude]
 173    public SwiftTicketMapState<T> State
 174    {
 175        get
 176        {
 9177            int length = _entries.Length;
 178
 9179            var items = new T[length];
 9180            var allocated = new bool[length];
 9181            var generations = new uint[length];
 182
 274183            for (int i = 0; i < length; i++)
 184            {
 128185                generations[i] = _entries[i].Generation;
 186
 128187                if (_entries[i].IsUsed)
 188                {
 72189                    items[i] = _entries[i].Value;
 72190                    allocated[i] = true;
 191                }
 192            }
 193
 9194            int[] free = new int[_freeIndices.Count];
 9195            Array.Copy(_freeIndices.Array, free, _freeIndices.Count);
 196
 9197            return new SwiftTicketMapState<T>(
 9198                items,
 9199                allocated,
 9200                generations,
 9201                free,
 9202                _peak
 9203            );
 204        }
 205        internal set
 206        {
 15207            var items = value.Items ?? Array.Empty<T>();
 15208            var allocated = value.Allocated ?? Array.Empty<bool>();
 15209            var generations = value.Generations ?? Array.Empty<uint>();
 15210            var freeIndices = value.FreeIndices ?? Array.Empty<int>();
 211
 15212            int sourceLength = GetStateSourceLength(items, allocated, generations);
 15213            int capacity = GetStateCapacity(sourceLength);
 15214            InitializeStateStorage(capacity, freeIndices.Length);
 215
 15216            int maxReferencedIndex = RestoreEntries(items, allocated, generations, sourceLength);
 15217            maxReferencedIndex = RestoreFreeIndices(freeIndices, capacity, maxReferencedIndex);
 218
 14219            _peak = NormalizePeak(value.Peak, maxReferencedIndex, capacity);
 14220            _version = 0;
 14221        }
 222    }
 223
 224    private static int GetStateSourceLength(T[] items, bool[] allocated, uint[] generations)
 225    {
 15226        return Math.Max(items.Length, Math.Max(allocated.Length, generations.Length));
 227    }
 228
 229    private static int GetStateCapacity(int sourceLength)
 230    {
 15231        if (sourceLength < DefaultCapacity)
 7232            return DefaultCapacity;
 233
 8234        return SwiftHashTools.NextPowerOfTwo(sourceLength);
 235    }
 236
 237    private void InitializeStateStorage(int capacity, int freeIndexCount)
 238    {
 15239        _entries = new Entry[capacity];
 15240        _freeIndices = new SwiftIntStack(Math.Max(SwiftIntStack.DefaultCapacity, freeIndexCount));
 15241        _count = 0;
 15242    }
 243
 244    private int RestoreEntries(T[] items, bool[] allocated, uint[] generations, int sourceLength)
 245    {
 15246        int maxReferencedIndex = -1;
 290247        for (int i = 0; i < sourceLength; i++)
 248        {
 130249            ref Entry entry = ref _entries[i];
 130250            bool isAllocated = IsStateIndexAllocated(allocated, i);
 251
 130252            entry.Generation = GetStateGeneration(generations, i);
 253            // Generation history survives Clear; only allocation reserves the high-water range.
 130254            if (isAllocated)
 78255                maxReferencedIndex = i;
 256
 130257            if (isAllocated)
 78258                RestoreAllocatedEntry(ref entry, items, i);
 259        }
 260
 15261        return maxReferencedIndex;
 262    }
 263
 264    private static uint GetStateGeneration(uint[] generations, int index)
 265    {
 130266        if (generations.Length <= index)
 2267            return 0;
 268
 128269        return generations[index];
 270    }
 271
 272    private static bool IsStateIndexAllocated(bool[] allocated, int index)
 273    {
 130274        return allocated.Length > index && allocated[index];
 275    }
 276
 277    private void RestoreAllocatedEntry(ref Entry entry, T[] items, int index)
 278    {
 78279        if (items.Length > index)
 77280            entry.Value = items[index];
 281
 78282        entry.IsUsed = true;
 78283        _count++;
 78284    }
 285
 286    private int RestoreFreeIndices(int[] freeIndices, int capacity, int maxReferencedIndex)
 287    {
 41288        foreach (var index in freeIndices)
 289        {
 6290            ChronicleGuard.ThrowIfArgument((uint)index >= (uint)capacity, message: "Free index is out of range.");
 291
 5292            _freeIndices.Push(index);
 5293            if (index > maxReferencedIndex)
 2294                maxReferencedIndex = index;
 295        }
 296
 14297        return maxReferencedIndex;
 298    }
 299
 300    private static int NormalizePeak(int peak, int maxReferencedIndex, int capacity)
 301    {
 14302        if (peak < 0)
 1303            peak = 0;
 304
 14305        return Math.Min(Math.Max(peak, maxReferencedIndex + 1), capacity);
 306    }
 307
 308    #endregion
 309
 310    #region Core Operations
 311
 312    /// <summary>
 313    /// Adds the specified value to the collection and returns a ticket that can be used to reference it.
 314    /// </summary>
 315    /// <remarks>
 316    /// The returned ticket can be used to access or remove the value later.
 317    /// Tickets are only valid as long as the value remains in the collection.
 318    /// </remarks>
 319    /// <param name="value">The value to add to the collection.</param>
 320    /// <returns>A <see cref="SwiftTicket"/> that uniquely identifies the added value within the collection.</returns>
 321    public SwiftTicket Add(T value)
 322    {
 323        int index;
 324
 596325        if (_freeIndices.Count == 0)
 326        {
 591327            index = _peak++;
 328
 591329            if ((uint)index >= (uint)_entries.Length)
 20330                Resize(_entries.Length * 2);
 331        }
 332        else
 333        {
 5334            index = _freeIndices.Pop();
 335        }
 336
 596337        ref Entry entry = ref _entries[index];
 338
 596339        entry.Value = value;
 596340        entry.IsUsed = true;
 341
 596342        _count++;
 596343        _version++;
 344
 596345        return new SwiftTicket(index, entry.Generation);
 346    }
 347
 348    /// <summary>
 349    /// Attempts to retrieve the value associated with the specified ticket.
 350    /// </summary>
 351    /// <remarks>
 352    /// Use this method to safely attempt retrieval without throwing an exception if the ticket is invalid or the entry 
 353    /// </remarks>
 354    /// <param name="ticket">The ticket used to identify the entry to retrieve.</param>
 355    /// <param name="value">
 356    /// When this method returns, contains the value associated with the specified ticket if the ticket is valid
 357    /// and the entry is in use; otherwise, the default value for the type of the value parameter.
 358    /// This parameter is passed uninitialized.
 359    /// </param>
 360    /// <returns>true if the value was found and retrieved successfully; otherwise, false.</returns>
 361    public bool TryGet(SwiftTicket ticket, out T value)
 362    {
 215363        if ((uint)ticket.Index >= (uint)_entries.Length)
 364        {
 1365            value = default!;
 1366            return false;
 367        }
 368
 214369        ref Entry entry = ref _entries[ticket.Index];
 370
 214371        if (!entry.IsUsed || entry.Generation != ticket.Generation)
 372        {
 3373            value = default!;
 3374            return false;
 375        }
 376
 211377        value = entry.Value;
 211378        return true;
 379    }
 380
 381    /// <summary>
 382    /// Returns a reference to the value associated with the specified ticket.
 383    /// </summary>
 384    /// <param name="ticket">
 385    /// A ticket that identifies the entry whose value is to be accessed.
 386    /// The ticket must be valid and refer to an existing entry.
 387    /// </param>
 388    /// <returns>A reference to the value of type T associated with the specified ticket.</returns>
 389    /// <exception cref="InvalidOperationException">Thrown if the ticket does not refer to a valid or currently used ent
 390    public ref T GetRef(SwiftTicket ticket)
 391    {
 3392        ref Entry entry = ref _entries[ticket.Index];
 393
 3394        ChronicleGuard.ThrowIfTrue(!entry.IsUsed || entry.Generation != ticket.Generation, message: "Invalid ticket");
 395
 1396        return ref entry.Value;
 397    }
 398
 399    /// <summary>
 400    /// Removes the entry associated with the specified ticket from the collection.
 401    /// </summary>
 402    /// <remarks>
 403    /// If the ticket does not refer to a valid or currently used entry, the method returns false and no action is taken
 404    /// Removing an entry invalidates the ticket for future operations.
 405    /// </remarks>
 406    /// <param name="ticket">The ticket identifying the entry to remove. The ticket must refer to a valid, currently use
 407    /// <returns>true if the entry was successfully removed; otherwise, false.</returns>
 408    public bool Remove(SwiftTicket ticket)
 409    {
 14410        if ((uint)ticket.Index >= (uint)_entries.Length)
 1411            return false;
 412
 13413        ref Entry entry = ref _entries[ticket.Index];
 414
 13415        if (!entry.IsUsed || entry.Generation != ticket.Generation)
 2416            return false;
 417
 11418        entry.Value = default!;
 11419        entry.IsUsed = false;
 420
 11421        entry.Generation++;
 422
 11423        _freeIndices.Push(ticket.Index);
 424
 11425        _count--;
 11426        _version++;
 427
 11428        return true;
 429    }
 430
 431    /// <summary>
 432    /// Determines whether the specified ticket refers to a valid and currently used entry.
 433    /// </summary>
 434    /// <remarks>
 435    /// A ticket may become invalid if the referenced entry has been removed or replaced.
 436    /// Use this method to check ticket validity before accessing the associated entry.
 437    /// </remarks>
 438    /// <param name="ticket">The ticket to validate. The ticket must have been obtained from this collection; otherwise,
 439    /// <returns>true if the ticket is valid and refers to an active entry; otherwise, false.</returns>
 440    public bool IsValid(SwiftTicket ticket)
 441    {
 23442        if ((uint)ticket.Index >= (uint)_entries.Length)
 1443            return false;
 444
 22445        ref Entry entry = ref _entries[ticket.Index];
 446
 22447        return entry.IsUsed && entry.Generation == ticket.Generation;
 448    }
 449
 450    /// <summary>Removes all elements and invalidates their tickets without reducing capacity.</summary>
 2451    public void Clear() => Clear(clearRetainedReferences: false);
 452
 453    /// <summary>Removes all elements, optionally releasing references throughout retained backing storage.</summary>
 454    /// <param name="clearRetainedReferences">If true, also releases references outside the previously used range.</para
 455    /// <remarks>Stored generations survive clearing so old tickets remain invalid on reuse. Capacity is preserved.</rem
 456    public void Clear(bool clearRetainedReferences)
 457    {
 144458        int end = clearRetainedReferences && RuntimeHelpers.IsReferenceOrContainsReferences<T>() ? _entries.Length : _pe
 2478459        for (int i = 0; i < end; i++)
 460        {
 1095461            ref Entry entry = ref _entries[i];
 1095462            if (entry.IsUsed)
 463            {
 168464                entry.IsUsed = false;
 168465                entry.Generation++;
 466            }
 1095467            entry.Value = default!;
 468        }
 144469        _freeIndices.Reset();
 144470        _count = 0;
 144471        _peak = 0;
 144472        _version++;
 144473    }
 474    #endregion
 475
 476    #region Capacity
 477
 478    /// <summary>
 479    /// Ensures that the underlying storage has at least the specified capacity, expanding it if necessary.
 480    /// </summary>
 481    /// <remarks>
 482    /// If the current capacity is less than the specified value, the storage is resized to accommodate at least that ma
 483    /// The actual capacity may be rounded up to the next power of two for performance reasons.
 484    /// </remarks>
 485    /// <param name="capacity">The minimum number of elements that the storage should be able to hold. Must be a non-neg
 486    public void EnsureCapacity(int capacity)
 487    {
 2488        capacity = SwiftHashTools.NextPowerOfTwo(capacity);
 2489        if (capacity > _entries.Length)
 1490            Resize(capacity);
 2491    }
 492
 493    private void Resize(int newSize)
 494    {
 21495        Entry[] newArray = new Entry[newSize];
 21496        Array.Copy(_entries, newArray, _entries.Length);
 21497        _entries = newArray;
 21498    }
 499
 500    #endregion
 501
 502    #region Utility
 503
 504    /// <inheritdoc/>
 505    public void CloneTo(ICollection<T> output)
 506    {
 2507        ChronicleGuard.ThrowIfNull(output, nameof(output));
 508
 2509        output.Clear();
 510
 2511        uint count = 0;
 2512        uint peak = (uint)_peak;
 513
 48514        for (uint i = 0; i < peak && count < (uint)_count; i++)
 515        {
 22516            if (_entries[i].IsUsed)
 517            {
 21518                output.Add(_entries[i].Value);
 21519                count++;
 520            }
 521        }
 2522    }
 523
 524    /// <summary>
 525    /// Determines whether the <see cref="SwiftTicketMap{T}"/> contains an element that matches the conditions defined b
 526    /// </summary>
 527    /// <param name="match">The predicate that defines the conditions of the element to search for.</param>
 528    /// <returns><c>true</c> if the <see cref="SwiftTicketMap{T}"/> contains one or more elements that match the specifi
 529    public bool Exists(Predicate<T> match)
 530    {
 3531        ChronicleGuard.ThrowIfNull(match, nameof(match));
 532
 3533        uint count = 0;
 3534        uint peak = (uint)_peak;
 535
 10536        for (uint i = 0; i < peak; i++)
 537        {
 5538            if (count >= (uint)_count)
 539                break;
 540
 4541            if (_entries[i].IsUsed)
 542            {
 3543                if (match(_entries[i].Value))
 2544                    return true;
 545
 1546                count++;
 547            }
 548        }
 549
 1550        return false;
 551    }
 552
 553    /// <summary>
 554    /// Searches for an element that matches the conditions defined by the specified predicate, and returns the first ma
 555    /// </summary>
 556    /// <param name="match">The predicate that defines the conditions of the element to search for.</param>
 557    /// <returns>The first element that matches the conditions defined by the specified predicate, if found; otherwise, 
 558    public T Find(Predicate<T> match)
 559    {
 3560        ChronicleGuard.ThrowIfNull(match, nameof(match));
 561
 3562        uint count = 0;
 3563        uint peak = (uint)_peak;
 564
 14565        for (uint i = 0; i < peak && count < (uint)_count; i++)
 566        {
 6567            if (_entries[i].IsUsed)
 568            {
 5569                T item = _entries[i].Value;
 5570                if (match(item))
 2571                    return item;
 572
 3573                count++;
 574            }
 575        }
 576
 1577        return default!;
 578    }
 579
 580    #endregion
 581
 582    #region Enumeration
 583
 584    /// <inheritdoc cref="IEnumerable.GetEnumerator()"/>
 49585    public SwiftTicketMapEnumerator GetEnumerator() => new(this);
 23586    IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
 21587    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 588
 589
 590
 591    #endregion
 592}

/home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Collection/SwiftTicketMap.SwiftTicketMapEnumerator.cs

#LineLine coverage
 1//=======================================================================
 2// SwiftTicketMap.SwiftTicketMapEnumerator.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8
 9using System.Collections;
 10using System.Collections.Generic;
 11using Chronicler.Diagnostics;
 12
 13namespace SwiftCollections;
 14
 15public sealed partial class SwiftTicketMap<T>
 16{
 17    /// <summary>
 18    /// Enumerates the elements of a <see cref="SwiftTicketMap{T}"/> collection in a forward-only, read-only manner.
 19    /// </summary>
 20    /// <remarks>
 21    /// The enumerator is invalidated if the underlying collection is modified during enumeration.
 22    /// In such cases, subsequent calls to MoveNext will throw an InvalidOperationException.
 23    /// This enumerator is typically obtained by calling GetEnumerator on a <see cref="SwiftTicketMap{T}"/> instance.
 24    /// </remarks>
 25    public struct SwiftTicketMapEnumerator : IEnumerator<T>
 26    {
 27        private readonly SwiftTicketMap<T> _bucket;
 28        private readonly uint _version;
 29        private int _index;
 30        private T _current;
 31
 32        internal SwiftTicketMapEnumerator(SwiftTicketMap<T> map)
 33        {
 4934            _bucket = map;
 4935            _version = map._version;
 4936            _index = -1;
 4937            _current = default!;
 4938        }
 39
 40        /// <inheritdoc/>
 12841        public readonly T Current => _current;
 42
 43        readonly object IEnumerator.Current
 44        {
 45            get
 46            {
 3447                ChronicleGuard.ThrowIfTrue((uint)_index >= (uint)_bucket._peak, message: "Enumerator is before the first
 1448                return _current!;
 49            }
 50        }
 51
 52        /// <inheritdoc/>
 53        public bool MoveNext()
 54        {
 19955            ChronicleGuard.ThrowIfTrue(_version != _bucket._version, message: "Collection modified");
 56
 19557            uint peak = (uint)_bucket._peak;
 19758            while (++_index < peak)
 59            {
 15460                if (_bucket._entries[_index].IsUsed)
 61                {
 15262                    _current = _bucket._entries[_index].Value;
 15263                    return true;
 64                }
 65            }
 66
 4367            return false;
 68        }
 69
 70        /// <inheritdoc/>
 71        public void Reset()
 72        {
 1073            ChronicleGuard.ThrowIfTrue(_version != _bucket._version, message: "Collection modified");
 74
 775            _index = -1;
 776            _current = default!;
 777        }
 78
 79        /// <inheritdoc/>
 2980        public void Dispose() => _index = -1;
 81    }
 82}