| | | 1 | | //======================================================================= |
| | | 2 | | // NavigationWorldGraphStore.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 | | using System; |
| | | 9 | | using System.Threading; |
| | | 10 | | using SwiftCollections; |
| | | 11 | | |
| | | 12 | | namespace Trailblazer.Pathing; |
| | | 13 | | |
| | | 14 | | /// <summary>Publishes immutable roots and retains only generations with active leases.</summary> |
| | | 15 | | internal sealed class NavigationWorldGraphStore : System.IDisposable |
| | | 16 | | { |
| | 1819 | 17 | | private readonly object _sync = new(); |
| | 1819 | 18 | | private readonly SwiftList<NavigationWorldGraph> _retired = new(); |
| | 1819 | 19 | | private readonly SwiftList<NavigationWorldGraphLease> _leasePool = new(); |
| | 1819 | 20 | | private NavigationWorldGraph _current = NavigationWorldGraph.Empty; |
| | | 21 | | |
| | | 22 | | private readonly int _maxRetiredSnapshots; |
| | | 23 | | private readonly int _maxActiveSnapshots; |
| | | 24 | | private readonly long _maxRetiredBytes; |
| | | 25 | | private readonly long _maxActiveBytes; |
| | | 26 | | private readonly int _maxPersistentPages; |
| | | 27 | | private readonly int _maxConcurrentLeases; |
| | | 28 | | private int _activeLeaseCount; |
| | | 29 | | private bool _safetyPending; |
| | | 30 | | private bool _disposed; |
| | | 31 | | |
| | 1819 | 32 | | internal NavigationWorldGraphStore( |
| | 1819 | 33 | | int maxActiveSnapshots, |
| | 1819 | 34 | | int maxRetiredSnapshots, |
| | 1819 | 35 | | long maxRetiredBytes, |
| | 1819 | 36 | | long maxActiveBytes, |
| | 1819 | 37 | | int maxPersistentPages, |
| | 1819 | 38 | | int maxConcurrentLeases) |
| | | 39 | | { |
| | 1819 | 40 | | _maxActiveSnapshots = maxActiveSnapshots; |
| | 1819 | 41 | | _maxRetiredSnapshots = maxRetiredSnapshots; |
| | 1819 | 42 | | _maxRetiredBytes = maxRetiredBytes; |
| | 1819 | 43 | | _maxActiveBytes = maxActiveBytes; |
| | 1819 | 44 | | _maxPersistentPages = maxPersistentPages; |
| | 1819 | 45 | | _maxConcurrentLeases = maxConcurrentLeases; |
| | 1819 | 46 | | } |
| | | 47 | | |
| | 205184 | 48 | | internal NavigationWorldGraph Current => Volatile.Read(ref _current); |
| | | 49 | | |
| | | 50 | | internal int ActiveLeaseCount |
| | | 51 | | { |
| | 6963 | 52 | | get { lock (_sync) return _activeLeaseCount; } |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | internal int ActiveGenerationCount |
| | | 56 | | { |
| | | 57 | | get |
| | | 58 | | { |
| | 2200 | 59 | | lock (_sync) |
| | | 60 | | { |
| | 2200 | 61 | | CollectReleased(); |
| | 2200 | 62 | | return 1 + _retired.Count; |
| | | 63 | | } |
| | 2200 | 64 | | } |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | internal long RetiredBytes |
| | | 68 | | { |
| | | 69 | | get |
| | | 70 | | { |
| | 2203 | 71 | | lock (_sync) |
| | | 72 | | { |
| | 2203 | 73 | | CollectReleased(); |
| | 2203 | 74 | | long bytes = 0; |
| | 4442 | 75 | | for (int i = 0; i < _retired.Count; i++) |
| | 18 | 76 | | bytes += _retired[i].RetainedBytes; |
| | 2203 | 77 | | return bytes; |
| | | 78 | | } |
| | 2203 | 79 | | } |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | internal int RetiredGenerationCount |
| | | 83 | | { |
| | | 84 | | get |
| | | 85 | | { |
| | 2205 | 86 | | lock (_sync) |
| | | 87 | | { |
| | 2205 | 88 | | CollectReleased(); |
| | 2205 | 89 | | return _retired.Count; |
| | | 90 | | } |
| | 2205 | 91 | | } |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | internal NavigationWorldGraphLease? TryAcquire() |
| | | 95 | | { |
| | 8974 | 96 | | lock (_sync) |
| | | 97 | | { |
| | 8974 | 98 | | CollectReleased(); |
| | 8974 | 99 | | if (_disposed |
| | 8974 | 100 | | || _safetyPending |
| | 8974 | 101 | | || _activeLeaseCount >= _maxConcurrentLeases |
| | 8974 | 102 | | || WouldExceedRetiredCapacityAfterLease(Current)) |
| | 25 | 103 | | return null; |
| | 8949 | 104 | | NavigationWorldGraph current = Current; |
| | 8949 | 105 | | return RentLeaseUnderLock(current); |
| | | 106 | | } |
| | 8974 | 107 | | } |
| | | 108 | | |
| | | 109 | | internal int TryAcquirePrefix(Span<NavigationWorldGraphLease?> output) |
| | | 110 | | { |
| | 466 | 111 | | lock (_sync) |
| | | 112 | | { |
| | 466 | 113 | | output.Clear(); |
| | 466 | 114 | | CollectReleased(); |
| | 466 | 115 | | if (_disposed || _safetyPending || output.Length == 0) |
| | 3 | 116 | | return 0; |
| | 463 | 117 | | NavigationWorldGraph current = Current; |
| | 463 | 118 | | if (WouldExceedRetiredCapacityAfterLease(current)) |
| | 1 | 119 | | return 0; |
| | 462 | 120 | | int count = System.Math.Min( |
| | 462 | 121 | | output.Length, |
| | 462 | 122 | | _maxConcurrentLeases - _activeLeaseCount); |
| | 1868 | 123 | | for (int i = 0; i < count; i++) |
| | 472 | 124 | | output[i] = RentLeaseUnderLock(current); |
| | 462 | 125 | | return count; |
| | | 126 | | } |
| | 466 | 127 | | } |
| | | 128 | | |
| | | 129 | | internal void Return(NavigationWorldGraphLease lease) |
| | | 130 | | { |
| | 9420 | 131 | | NavigationWorldGraph graph = lease.DetachGraph(); |
| | 9420 | 132 | | graph.Return(); |
| | 9420 | 133 | | lock (_sync) |
| | | 134 | | { |
| | 9420 | 135 | | _activeLeaseCount--; |
| | 9420 | 136 | | CollectReleased(); |
| | 9420 | 137 | | if (!_disposed && _leasePool.Count < _maxConcurrentLeases) |
| | 9418 | 138 | | _leasePool.Add(lease); |
| | 9420 | 139 | | } |
| | 9420 | 140 | | } |
| | | 141 | | |
| | | 142 | | internal NavigationCandidatePublication TryPublish(NavigationWorldGraph next) |
| | | 143 | | { |
| | 5029 | 144 | | lock (_sync) |
| | | 145 | | { |
| | 5029 | 146 | | CollectReleased(); |
| | 5029 | 147 | | if (_disposed) |
| | 1 | 148 | | return NavigationCandidatePublication.Deferred; |
| | 5028 | 149 | | SwiftThrowHelper.ThrowIfTrue( |
| | 5028 | 150 | | next.GraphVersion <= _current.GraphVersion, |
| | 5028 | 151 | | message: "Published navigation graph versions must increase monotonically."); |
| | 5026 | 152 | | if (next.RetainedBytes > _maxActiveBytes |
| | 5026 | 153 | | || next.PersistentPageCount > _maxPersistentPages) |
| | 3 | 154 | | return NavigationCandidatePublication.PermanentCapacity; |
| | 5023 | 155 | | if (WouldExceedRetiredCapacity(_current)) |
| | 5 | 156 | | return NavigationCandidatePublication.Deferred; |
| | 5018 | 157 | | NavigationWorldGraph prior = _current; |
| | 5018 | 158 | | Volatile.Write(ref _current, next); |
| | 5018 | 159 | | if (prior.LeaseCount > 0) |
| | 26 | 160 | | _retired.Add(prior); |
| | 5018 | 161 | | CollectReleased(); |
| | 5018 | 162 | | return NavigationCandidatePublication.Published; |
| | | 163 | | } |
| | 5027 | 164 | | } |
| | | 165 | | |
| | | 166 | | internal bool CanPublish |
| | | 167 | | { |
| | | 168 | | get |
| | | 169 | | { |
| | 20564 | 170 | | lock (_sync) |
| | | 171 | | { |
| | 20564 | 172 | | CollectReleased(); |
| | 20564 | 173 | | return !_disposed && !WouldExceedRetiredCapacity(_current); |
| | | 174 | | } |
| | 20564 | 175 | | } |
| | | 176 | | } |
| | | 177 | | |
| | | 178 | | internal bool IsSafetyPending |
| | | 179 | | { |
| | 60723 | 180 | | get { lock (_sync) return _safetyPending; } |
| | | 181 | | } |
| | | 182 | | |
| | | 183 | | internal void MarkSafetyPending() |
| | | 184 | | { |
| | 8 | 185 | | SetSafetyPending(true); |
| | 8 | 186 | | } |
| | | 187 | | |
| | | 188 | | internal void ClearSafetyPending() |
| | | 189 | | { |
| | 108 | 190 | | SetSafetyPending(false); |
| | 108 | 191 | | } |
| | | 192 | | |
| | | 193 | | internal void SetSafetyPending(bool pending) |
| | | 194 | | { |
| | 152 | 195 | | lock (_sync) |
| | 152 | 196 | | _safetyPending = pending; |
| | 152 | 197 | | } |
| | | 198 | | |
| | | 199 | | public void Dispose() |
| | | 200 | | { |
| | 1819 | 201 | | lock (_sync) |
| | | 202 | | { |
| | 1819 | 203 | | if (_disposed) |
| | 1 | 204 | | return; |
| | 1818 | 205 | | _disposed = true; |
| | 1818 | 206 | | _leasePool.Clear(); |
| | 1818 | 207 | | CollectReleased(); |
| | 1818 | 208 | | } |
| | 1819 | 209 | | } |
| | | 210 | | |
| | | 211 | | private void CollectReleased() |
| | | 212 | | { |
| | 57897 | 213 | | int destination = 0; |
| | 116270 | 214 | | for (int i = 0; i < _retired.Count; i++) |
| | | 215 | | { |
| | 238 | 216 | | NavigationWorldGraph graph = _retired[i]; |
| | 238 | 217 | | if (graph.LeaseCount > 0) |
| | 212 | 218 | | _retired[destination++] = graph; |
| | | 219 | | } |
| | 57923 | 220 | | while (_retired.Count > destination) |
| | 26 | 221 | | _retired.RemoveAt(_retired.Count - 1); |
| | 57897 | 222 | | } |
| | | 223 | | |
| | | 224 | | private NavigationWorldGraphLease RentLeaseUnderLock( |
| | | 225 | | NavigationWorldGraph current) |
| | | 226 | | { |
| | 9421 | 227 | | _activeLeaseCount++; |
| | 9421 | 228 | | if (_leasePool.Count == 0) |
| | 1198 | 229 | | return new NavigationWorldGraphLease(this, current); |
| | 8223 | 230 | | NavigationWorldGraphLease lease = _leasePool[_leasePool.Count - 1]; |
| | 8223 | 231 | | _leasePool.RemoveAt(_leasePool.Count - 1); |
| | 8223 | 232 | | lease.Reinitialize(this, current); |
| | 8223 | 233 | | return lease; |
| | | 234 | | } |
| | | 235 | | |
| | | 236 | | private bool WouldExceedRetiredCapacity(NavigationWorldGraph current) |
| | | 237 | | { |
| | 25586 | 238 | | if (current.LeaseCount == 0) |
| | 25515 | 239 | | return false; |
| | 71 | 240 | | if (_retired.Count >= _maxRetiredSnapshots |
| | 71 | 241 | | || _retired.Count + 2 > _maxActiveSnapshots) |
| | 25 | 242 | | return true; |
| | | 243 | | |
| | 46 | 244 | | long bytes = current.RetainedBytes; |
| | 94 | 245 | | for (int i = 0; i < _retired.Count; i++) |
| | 1 | 246 | | bytes += _retired[i].RetainedBytes; |
| | 46 | 247 | | return bytes > _maxRetiredBytes; |
| | | 248 | | } |
| | | 249 | | |
| | | 250 | | private bool WouldExceedRetiredCapacityAfterLease(NavigationWorldGraph current) |
| | | 251 | | { |
| | | 252 | | // A disabled retirement budget is a writer-blocking mode, not a reader ban. |
| | | 253 | | // Publication already refuses to retire a leased current root until all readers return. |
| | 9413 | 254 | | if (_maxRetiredSnapshots == 0 || _maxRetiredBytes == 0) |
| | 25 | 255 | | return false; |
| | 9388 | 256 | | if (_retired.Count >= _maxRetiredSnapshots |
| | 9388 | 257 | | || _retired.Count + 2 > _maxActiveSnapshots) |
| | 2 | 258 | | return true; |
| | 9386 | 259 | | long bytes = current.RetainedBytes; |
| | 18804 | 260 | | for (int i = 0; i < _retired.Count; i++) |
| | 16 | 261 | | bytes += _retired[i].RetainedBytes; |
| | 9386 | 262 | | return bytes > _maxRetiredBytes; |
| | | 263 | | } |
| | | 264 | | } |