< Summary

Information
Class: Trailblazer.Pathing.NavigationAStarBatchWork
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/AStar/NavigationAStarAdmissionGate.cs
Line coverage
100%
Covered lines: 20
Uncovered lines: 0
Coverable lines: 20
Total lines: 480
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_AdmittedCount()100%11100%
get_IsAdmissionComplete()100%11100%
GetStatus(...)100%11100%
IsReadyToPublish(...)100%11100%
AdvanceAdmission(...)100%11100%
AdvanceSearch(...)100%11100%
PublishReadyPrefix(...)100%11100%
TakeResult(...)100%11100%
Dispose()100%22100%
IsOwnedBy(...)100%11100%
get_Owner()100%22100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/AStar/NavigationAStarAdmissionGate.cs

#LineLine coverage
 1//=======================================================================
 2// NavigationAStarAdmissionGate.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.Threading;
 10using GridForge.Grids;
 11
 12namespace Trailblazer.Pathing;
 13
 14/// <summary>Serializes context-owned A* resource admission.</summary>
 15internal sealed class NavigationAStarAdmissionGate : IDisposable
 16{
 17    private readonly object _sync = new();
 18    private readonly NavigationWorldGraphStore _store;
 19    private readonly NavigationQueryLimits _limits;
 20    private readonly NavigationQueryAdmissionCoordinator _coordinator;
 21    private readonly NavigationAStarPayloadCache _cache;
 22    private readonly NavigationAStarQueryWork[] _queries;
 23    private readonly NavigationWorldGraphLease?[] _leases;
 24    private readonly NavigationAStarPayloadReservation[] _reservations;
 25    private readonly BatchDescriptor[] _descriptors;
 26    private ulong _generation;
 27    private ulong _activeGeneration;
 28    private NavigationQueryCapacityReservation _capacityReservation;
 29    private int _activeCount;
 30    private int _activeAdmittedCount;
 31    private int _nextAdmission;
 32    private int _nextPublication;
 33    private bool _active;
 34    private bool _disposed;
 35
 36    internal NavigationAStarAdmissionGate(
 37        GridWorld world,
 38        NavigationWorldGraphStore store,
 39        NavigationQueryLimits limits,
 40        NavigationQueryAdmissionCoordinator coordinator)
 41    {
 42        SwiftThrowHelper.ThrowIfNull(world, nameof(world));
 43        SwiftThrowHelper.ThrowIfNull(store, nameof(store));
 44        SwiftThrowHelper.ThrowIfNull(coordinator, nameof(coordinator));
 45        SwiftThrowHelper.ThrowIfArgument(
 46            limits.MaxConcurrentNavigationQueries <= 0,
 47            nameof(limits),
 48            "Query limits must be explicitly initialized.");
 49        _store = store;
 50        _limits = limits;
 51        _coordinator = coordinator;
 52        _cache = new NavigationAStarPayloadCache(
 53            world,
 54            limits.MaxAStarCacheEntries,
 55            limits.MaxAStarReusablePayloadBytes,
 56            limits.MaxAStarSinglePayloadBytes,
 57            limits.MaxAStarActivePayloadBytes,
 58            limits.MaxAStarActivePayloadLeases);
 59        _queries = new NavigationAStarQueryWork[limits.MaxConcurrentNavigationQueries];
 60        _leases = new NavigationWorldGraphLease[limits.MaxConcurrentNavigationQueries];
 61        _reservations = new NavigationAStarPayloadReservation[limits.MaxConcurrentNavigationQueries];
 62        for (int i = 0; i < _queries.Length; i++)
 63        {
 64            var workspace = new NavigationAStarWorkspace(
 65                mapCapacity: limits.AStarWorkspaceMapCapacity,
 66                endpointPageCapacity: limits.AStarWorkspaceEndpointPageCapacity,
 67                componentCapacity: limits.AStarWorkspaceComponentCapacity,
 68                nodeCapacity: limits.AStarWorkspaceNodeCapacity,
 69                rayCoveredAddressCapacity: limits.RayWorkspaceCoveredAddressCapacity,
 70                rayTraceIntervalCapacity: limits.RayWorkspaceTraceIntervalCapacity,
 71                guidePointCapacity: limits.AStarWorkspaceGuidePointCapacity);
 72            _queries[i] = new NavigationAStarQueryWork(world, store, workspace, _cache);
 73        }
 74        _descriptors = new BatchDescriptor[limits.MaxBatchItems];
 75    }
 76
 77    internal NavigationAStarPayloadCache PayloadCache => _cache;
 78
 79    internal NavigationAStarQueryStatus Begin(
 80        PathQuery query,
 81        out NavigationAStarBatchWork work)
 82    {
 83        lock (_sync)
 84        {
 85            if (!CanBegin() || !FitsSingleEnvelope(query))
 86            {
 87                work = default;
 88                return NavigationAStarQueryStatus.CapacityExceeded;
 89            }
 90            _descriptors[0] = new BatchDescriptor(0, 0, query);
 91            return BeginCore(1, out work);
 92        }
 93    }
 94
 95    internal NavigationAStarQueryStatus Begin(
 96        PathQueryBatch batch,
 97        out NavigationAStarBatchWork work)
 98    {
 99        lock (_sync)
 100        {
 101            if (!CanBegin() || !FitsBatchEnvelope(batch))
 102            {
 103                work = default;
 104                return NavigationAStarQueryStatus.CapacityExceeded;
 105            }
 106            for (int i = 0; i < batch.Count; i++)
 107            {
 108                PathQueryBatchItem item = batch.Items[i];
 109                _descriptors[i] = new BatchDescriptor(item.StableOrdinal, i, item.Query);
 110            }
 111            Array.Sort(_descriptors, 0, batch.Count);
 112            return BeginCore(batch.Count, out work);
 113        }
 114    }
 115
 116    public void Dispose()
 117    {
 118        NavigationAStarBatchWork active = default;
 119        lock (_sync)
 120        {
 121            if (_disposed)
 122                return;
 123            _disposed = true;
 124            if (_active)
 125                active = new NavigationAStarBatchWork(this, _activeGeneration);
 126        }
 127        active.Dispose();
 128    }
 129
 130    internal void CancelActive()
 131    {
 132        NavigationAStarBatchWork active = default;
 133        lock (_sync)
 134        {
 135            if (_active)
 136                active = new NavigationAStarBatchWork(this, _activeGeneration);
 137        }
 138        active.Dispose();
 139    }
 140
 141    private bool CanBegin() =>
 142        !_disposed
 143        && !_active;
 144
 145    private bool FitsBatchEnvelope(PathQueryBatch batch) =>
 146        batch.Count > 0
 147        && batch.Count <= _limits.MaxBatchItems
 148        && batch.GetLogicalRetainedBytes() <= _limits.MaxBatchDescriptorBytes;
 149
 150    private bool FitsSingleEnvelope(PathQuery query)
 151    {
 152        return PathQueryBatchItem.GetLogicalRetainedBytes(query)
 153            <= _limits.MaxBatchDescriptorBytes;
 154    }
 155
 156    private NavigationAStarQueryStatus BeginCore(
 157        int count,
 158        out NavigationAStarBatchWork work)
 159    {
 160        int candidateCount = _coordinator.TryReservePrefix(
 161            PathAlgorithm.AStar,
 162            Math.Min(count, _queries.Length),
 163            out _capacityReservation);
 164        int leased = _store.TryAcquirePrefix(_leases.AsSpan(0, candidateCount));
 165        _capacityReservation = _coordinator.Trim(_capacityReservation, leased);
 166
 167        int admitted = 0;
 168        for (; admitted < leased; admitted++)
 169        {
 170            PathQuery query = _descriptors[admitted].Query;
 171            long maximumBytes = Math.Min(
 172                NavigationAStarPayload.GetMaximumRetainedBytes(
 173                    _limits.AStarWorkspaceGuidePointCapacity,
 174                    query.AllowTransitions
 175                        ? _limits.AStarWorkspaceNodeCapacity - 1
 176                        : 0,
 177                    _limits.AStarWorkspaceComponentCapacity,
 178                    _limits.AStarWorkspaceEndpointPageCapacity),
 179                _limits.MaxAStarSinglePayloadBytes);
 180            if (!_cache.TryReservePayload(maximumBytes, out _reservations[admitted]))
 181                break;
 182        }
 183        for (int i = admitted; i < leased; i++)
 184        {
 185            _leases[i]!.Dispose();
 186            _leases[i] = null;
 187        }
 188        _capacityReservation = _coordinator.Trim(_capacityReservation, admitted);
 189        for (int i = 0; i < admitted; i++)
 190        {
 191            _descriptors[i].SlotIndex = i;
 192            _queries[i].BeginReserved(
 193                _descriptors[i].Query,
 194                _leases[i]!,
 195                ref _reservations[i]);
 196            _leases[i] = null;
 197        }
 198        _generation = unchecked(_generation + 1UL);
 199        _activeGeneration = _generation;
 200        _activeCount = count;
 201        _activeAdmittedCount = admitted;
 202        _nextAdmission = 0;
 203        _nextPublication = 0;
 204        _active = true;
 205        work = new NavigationAStarBatchWork(this, _activeGeneration);
 206        return NavigationAStarQueryStatus.Pending;
 207    }
 208
 209    private BatchDescriptor GetDescriptor(int inputIndex)
 210    {
 211        SwiftThrowHelper.ThrowIfArgumentOutOfRange(
 212            inputIndex < 0 || inputIndex >= _activeCount,
 213            inputIndex,
 214            nameof(inputIndex));
 215        int descriptorOrdinal = 0;
 216        while (_descriptors[descriptorOrdinal].InputIndex != inputIndex)
 217            descriptorOrdinal++;
 218        return _descriptors[descriptorOrdinal];
 219    }
 220
 221    internal int GetAdmittedCount(NavigationAStarBatchWork work)
 222    {
 223        lock (_sync)
 224        {
 225            EnsureActive(work);
 226            return _activeAdmittedCount;
 227        }
 228    }
 229
 230    internal bool IsAdmissionComplete(NavigationAStarBatchWork work)
 231    {
 232        lock (_sync)
 233        {
 234            EnsureActive(work);
 235            return _nextAdmission >= _activeAdmittedCount;
 236        }
 237    }
 238
 239    internal NavigationAStarQueryStatus GetStatus(
 240        NavigationAStarBatchWork work,
 241        int inputIndex)
 242    {
 243        lock (_sync)
 244        {
 245            EnsureActive(work);
 246            BatchDescriptor descriptor = GetDescriptor(inputIndex);
 247            if (descriptor.SlotIndex < 0)
 248                return NavigationAStarQueryStatus.CapacityExceeded;
 249            NavigationAStarQueryWork query = _queries[descriptor.SlotIndex];
 250            lock (query)
 251                return query.Status;
 252        }
 253    }
 254
 255    internal bool IsReadyToPublish(
 256        NavigationAStarBatchWork work,
 257        int inputIndex)
 258    {
 259        lock (_sync)
 260        {
 261            EnsureActive(work);
 262            BatchDescriptor descriptor = GetDescriptor(inputIndex);
 263            return descriptor.SlotIndex < 0
 264                || _queries[descriptor.SlotIndex].IsReadyToPublish;
 265        }
 266    }
 267
 268    internal void AdvanceAdmission(
 269        NavigationAStarBatchWork work,
 270        int lookupStepLimit,
 271        int endpointCandidateStepLimit)
 272    {
 273        lock (_sync)
 274        {
 275            EnsureActive(work);
 276            while (_nextAdmission < _activeAdmittedCount)
 277            {
 278                NavigationAStarQueryWork query = _queries[_nextAdmission];
 279                query.PrepareSearchOrCheckout(
 280                    lookupStepLimit,
 281                    endpointCandidateStepLimit);
 282                if (!query.IsPrepared)
 283                    return;
 284                _nextAdmission++;
 285            }
 286        }
 287    }
 288
 289    internal NavigationAStarQueryStatus AdvanceSearch(
 290        NavigationAStarBatchWork work,
 291        int inputIndex,
 292        int lookupStepLimit,
 293        int nodeStepLimit,
 294        int edgeStepLimit,
 295        int connectionStepLimit)
 296    {
 297        BatchDescriptor descriptor;
 298        NavigationAStarQueryWork query;
 299        lock (_sync)
 300        {
 301            EnsureActive(work);
 302            descriptor = GetDescriptor(inputIndex);
 303            if (descriptor.SlotIndex < 0)
 304                return NavigationAStarQueryStatus.CapacityExceeded;
 305            query = _queries[descriptor.SlotIndex];
 306            SwiftThrowHelper.ThrowIfTrue(
 307                !query.IsPrepared,
 308                message: "The query has not completed sequential admission.");
 309            Monitor.Enter(query);
 310        }
 311        try
 312        {
 313            return query.AdvanceSearch(
 314                lookupStepLimit,
 315                nodeStepLimit,
 316                edgeStepLimit,
 317                connectionStepLimit);
 318        }
 319        finally
 320        {
 321            Monitor.Exit(query);
 322        }
 323    }
 324
 325    internal int PublishReadyPrefix(
 326        NavigationAStarBatchWork work,
 327        int maximumCount)
 328    {
 329        SwiftThrowHelper.ThrowIfNegative(maximumCount, nameof(maximumCount));
 330        lock (_sync)
 331        {
 332            EnsureActive(work);
 333            int published = 0;
 334            while (published < maximumCount
 335                && _nextPublication < _activeAdmittedCount)
 336            {
 337                NavigationAStarQueryWork query = _queries[_nextPublication];
 338                lock (query)
 339                {
 340                    if (!query.IsReadyToPublish)
 341                        break;
 342                    query.Publish();
 343                }
 344                _nextPublication++;
 345                published++;
 346            }
 347            return published;
 348        }
 349    }
 350
 351    internal NavigationAStarPayloadLease TakeResult(
 352        NavigationAStarBatchWork work,
 353        int inputIndex)
 354    {
 355        lock (_sync)
 356        {
 357            EnsureActive(work);
 358            BatchDescriptor descriptor = GetDescriptor(inputIndex);
 359            SwiftThrowHelper.ThrowIfTrue(
 360                descriptor.SlotIndex < 0,
 361                message: "The query was not admitted.");
 362            return _queries[descriptor.SlotIndex].TakeResult();
 363        }
 364    }
 365
 366    internal void Release(NavigationAStarBatchWork work)
 367    {
 368        lock (_sync)
 369        {
 370            if (!IsActive(work))
 371                return;
 372            for (int i = 0; i < _activeAdmittedCount; i++)
 373            {
 374                lock (_queries[i])
 375                    _queries[i].Dispose();
 376            }
 377            _coordinator.Release(_capacityReservation);
 378            _capacityReservation = default;
 379            if (_activeCount > 0)
 380                Array.Clear(_descriptors, 0, _activeCount);
 381            _active = false;
 382            _activeGeneration = 0;
 383            _activeCount = 0;
 384            _activeAdmittedCount = 0;
 385            _nextAdmission = 0;
 386            _nextPublication = 0;
 387        }
 388    }
 389
 390    private void EnsureActive(NavigationAStarBatchWork work)
 391    {
 392        if (!IsActive(work))
 393            throw new ObjectDisposedException(nameof(NavigationAStarBatchWork));
 394    }
 395
 396    private bool IsActive(NavigationAStarBatchWork work) =>
 397        _active
 398        && work.IsOwnedBy(this)
 399        && work.Generation == _activeGeneration;
 400
 401    private struct BatchDescriptor : IComparable<BatchDescriptor>
 402    {
 403        internal BatchDescriptor(long stableOrdinal, int inputIndex, PathQuery query)
 404        {
 405            StableOrdinal = stableOrdinal;
 406            InputIndex = inputIndex;
 407            Query = query;
 408            SlotIndex = -1;
 409        }
 410
 411        internal long StableOrdinal;
 412        internal int InputIndex;
 413        internal PathQuery Query;
 414        internal int SlotIndex;
 415
 416        public readonly int CompareTo(BatchDescriptor other)
 417        {
 418            int ordinalComparison = StableOrdinal.CompareTo(other.StableOrdinal);
 419            return ordinalComparison != 0
 420                ? ordinalComparison
 421                : InputIndex.CompareTo(other.InputIndex);
 422        }
 423    }
 424}
 425
 426/// <summary>Coordinates one deterministically admitted A* query batch.</summary>
 427internal readonly struct NavigationAStarBatchWork : IDisposable
 428{
 429    private readonly NavigationAStarAdmissionGate? _owner;
 430
 431    internal NavigationAStarBatchWork(
 432        NavigationAStarAdmissionGate owner,
 433        ulong generation)
 434    {
 411435        _owner = owner;
 411436        Generation = generation;
 411437    }
 438
 439    internal ulong Generation { get; }
 440
 5441    internal int AdmittedCount => Owner.GetAdmittedCount(this);
 442
 944443    internal bool IsAdmissionComplete => Owner.IsAdmissionComplete(this);
 444
 445    internal NavigationAStarQueryStatus GetStatus(int inputIndex) =>
 403446        Owner.GetStatus(this, inputIndex);
 447
 448    internal bool IsReadyToPublish(int inputIndex) =>
 666449        Owner.IsReadyToPublish(this, inputIndex);
 450
 451    internal void AdvanceAdmission(int lookupStepLimit, int endpointCandidateStepLimit) =>
 545452        Owner.AdvanceAdmission(this, lookupStepLimit, endpointCandidateStepLimit);
 453
 454    internal NavigationAStarQueryStatus AdvanceSearch(
 455        int inputIndex,
 456        int lookupStepLimit,
 457        int nodeStepLimit,
 458        int edgeStepLimit,
 267459        int connectionStepLimit) => Owner.AdvanceSearch(
 267460            this,
 267461            inputIndex,
 267462            lookupStepLimit,
 267463            nodeStepLimit,
 267464            edgeStepLimit,
 267465            connectionStepLimit);
 466
 467    internal int PublishReadyPrefix(int maximumCount) =>
 391468        Owner.PublishReadyPrefix(this, maximumCount);
 469
 470    internal NavigationAStarPayloadLease TakeResult(int inputIndex) =>
 369471        Owner.TakeResult(this, inputIndex);
 472
 1435473    public void Dispose() => _owner?.Release(this);
 474
 475    internal bool IsOwnedBy(NavigationAStarAdmissionGate owner) =>
 3997476        ReferenceEquals(_owner, owner);
 477
 478    private NavigationAStarAdmissionGate Owner =>
 3590479        _owner ?? throw new ObjectDisposedException(nameof(NavigationAStarBatchWork));
 480}