< Summary

Information
Class: GridForge.Diagnostics.GridDiagnosticSession
Assembly: GridForge
File(s): /home/runner/work/GridForge/GridForge/src/GridForge/Diagnostics/GridDiagnosticSession.cs
Line coverage
95%
Covered lines: 199
Uncovered lines: 9
Coverable lines: 208
Total lines: 390
Line coverage: 95.6%
Branch coverage
70%
Covered branches: 45
Total branches: 64
Branch coverage: 70.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%4483.33%
GetDirtyChangesInto(...)100%44100%
ClearDirtyChanges()100%11100%
Dispose()50%2285.71%
Subscribe()100%11100%
Unsubscribe()100%11100%
HandleActiveGridAdded(...)50%2275%
HandleActiveGridRemoved(...)50%2275%
HandleActiveGridChanged(...)87.5%88100%
HandleWorldReset()50%2295%
HandleObstacleChanged(...)100%22100%
HandleObstaclesCleared(...)50%3250%
HandleOccupantChanged(...)100%22100%
CanRecord(...)50%22100%
CanRecordCell(...)100%44100%
HasPendingWorldReset()100%11100%
RecordGridChange(...)100%11100%
RecordSparseVoxelChange(...)100%11100%
RecordSparseAddressRangeChange(...)50%22100%
RecordCellChange(...)100%11100%
RecordChange(...)100%22100%
.ctor(...)100%11100%
FromChange(...)100%11100%
Equals(...)50%1414100%
Equals(...)0%620%
GetHashCode()100%11100%
GetScope(...)100%88100%

File(s)

/home/runner/work/GridForge/GridForge/src/GridForge/Diagnostics/GridDiagnosticSession.cs

#LineLine coverage
 1//=======================================================================
 2// GridDiagnosticSession.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 FixedMathSharp;
 9using GridForge.Grids;
 10using GridForge.Grids.Topology;
 11using GridForge.Spatial;
 12using SwiftCollections;
 13using SwiftCollections.Utility;
 14using System;
 15
 16namespace GridForge.Diagnostics;
 17
 18/// <summary>
 19/// Captures dirty diagnostic grid, cell, and sparse address changes for one
 20/// active <see cref="GridWorld"/>.
 21/// </summary>
 22public sealed class GridDiagnosticSession : IDisposable
 23{
 24    private readonly GridWorld _world;
 25    private readonly int _worldSpawnToken;
 726    private readonly SwiftList<GridDiagnosticChange> _changes = new();
 727    private readonly SwiftDictionary<GridDiagnosticChangeKey, int> _changeIndexes = new();
 728    private readonly object _syncRoot = new();
 29    private bool _worldResetPending;
 30    private bool _disposed;
 31
 32    /// <summary>
 33    /// Creates a diagnostic dirty-tracking session for the supplied active
 34    /// world.
 35    /// </summary>
 736    public GridDiagnosticSession(GridWorld world)
 37    {
 738        if (world == null)
 039            throw new ArgumentNullException(nameof(world));
 40
 741        if (!world.IsActive)
 042            throw new InvalidOperationException("Diagnostic sessions require an active GridWorld.");
 43
 744        _world = world;
 745        _worldSpawnToken = world.SpawnToken;
 746        Subscribe();
 747    }
 48
 49    /// <summary>
 50    /// Clears and fills caller-owned storage with coalesced dirty changes.
 51    /// </summary>
 52    public int GetDirtyChangesInto(SwiftList<GridDiagnosticChange> results)
 53    {
 1054        SwiftThrowHelper.ThrowIfNull(results, nameof(results));
 55
 1056        results.Clear();
 1057        lock (_syncRoot)
 58        {
 4459            for (int i = 0; i < _changes.Count; i++)
 1260                results.Add(_changes[i]);
 1061        }
 62
 1063        if (results.Count > 1)
 464            results.SortInPlace();
 65
 1066        return results.Count;
 67    }
 68
 69    /// <summary>
 70    /// Clears all dirty changes captured so far.
 71    /// </summary>
 72    public void ClearDirtyChanges()
 73    {
 1074        lock (_syncRoot)
 75        {
 1076            _changes.Clear();
 1077            _changeIndexes.Clear();
 1078            _worldResetPending = false;
 1079        }
 1080    }
 81
 82    /// <inheritdoc/>
 83    public void Dispose()
 84    {
 785        if (_disposed)
 086            return;
 87
 788        Unsubscribe();
 789        ClearDirtyChanges();
 790        _disposed = true;
 791        GC.SuppressFinalize(this);
 792    }
 93
 94    private void Subscribe()
 95    {
 796        _world.OnActiveGridAdded += HandleActiveGridAdded;
 797        _world.OnActiveGridRemoved += HandleActiveGridRemoved;
 798        _world.OnActiveGridChange += HandleActiveGridChanged;
 799        _world.OnReset += HandleWorldReset;
 7100        GridObstacleManager.OnObstacleAdded += HandleObstacleChanged;
 7101        GridObstacleManager.OnObstacleRemoved += HandleObstacleChanged;
 7102        GridObstacleManager.OnObstaclesCleared += HandleObstaclesCleared;
 7103        GridOccupantManager.OnOccupantAdded += HandleOccupantChanged;
 7104        GridOccupantManager.OnOccupantRemoved += HandleOccupantChanged;
 7105    }
 106
 107    private void Unsubscribe()
 108    {
 7109        _world.OnActiveGridAdded -= HandleActiveGridAdded;
 7110        _world.OnActiveGridRemoved -= HandleActiveGridRemoved;
 7111        _world.OnActiveGridChange -= HandleActiveGridChanged;
 7112        _world.OnReset -= HandleWorldReset;
 7113        GridObstacleManager.OnObstacleAdded -= HandleObstacleChanged;
 7114        GridObstacleManager.OnObstacleRemoved -= HandleObstacleChanged;
 7115        GridObstacleManager.OnObstaclesCleared -= HandleObstaclesCleared;
 7116        GridOccupantManager.OnOccupantAdded -= HandleOccupantChanged;
 7117        GridOccupantManager.OnOccupantRemoved -= HandleOccupantChanged;
 7118    }
 119
 120    private void HandleActiveGridAdded(GridEventInfo eventInfo)
 121    {
 3122        if (!CanRecord(eventInfo.WorldSpawnToken))
 0123            return;
 124
 3125        RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridAdded);
 3126    }
 127
 128    private void HandleActiveGridRemoved(GridEventInfo eventInfo)
 129    {
 1130        if (!CanRecord(eventInfo.WorldSpawnToken))
 0131            return;
 132
 1133        RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridRemoved);
 1134    }
 135
 136    private void HandleActiveGridChanged(GridEventInfo eventInfo)
 137    {
 6138        if (!CanRecord(eventInfo.WorldSpawnToken)
 6139            || HasPendingWorldReset())
 140        {
 1141            return;
 142        }
 143
 5144        switch (eventInfo.ChangeKind)
 145        {
 146            case GridEventKind.SparseVoxelAdded:
 1147                RecordSparseVoxelChange(eventInfo, GridDiagnosticChangeKind.SparseVoxelAdded);
 1148                break;
 149            case GridEventKind.SparseVoxelRemoved:
 1150                RecordSparseVoxelChange(eventInfo, GridDiagnosticChangeKind.SparseVoxelRemoved);
 1151                break;
 152            default:
 3153                RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridChanged);
 154                break;
 155        }
 3156    }
 157
 158    private void HandleWorldReset()
 159    {
 2160        if (!CanRecord(_worldSpawnToken))
 0161            return;
 162
 2163        GridDiagnosticChange change = new(
 2164            GridDiagnosticChangeKind.WorldReset,
 2165            _worldSpawnToken,
 2166            ushort.MaxValue,
 2167            0,
 2168            default,
 2169            default,
 2170            default,
 2171            default);
 2172        GridDiagnosticChangeKey key = GridDiagnosticChangeKey.FromChange(change);
 173
 2174        lock (_syncRoot)
 175        {
 2176            _changes.Clear();
 2177            _changeIndexes.Clear();
 2178            _worldResetPending = true;
 2179            _changeIndexes.Add(key, 0);
 2180            _changes.Add(change);
 2181        }
 2182    }
 183
 184    private void HandleObstacleChanged(ObstacleEventInfo eventInfo)
 185    {
 5186        if (!CanRecordCell(eventInfo.VoxelIndex))
 2187            return;
 188
 3189        RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.ObstacleChanged);
 3190    }
 191
 192    private void HandleObstaclesCleared(ObstacleClearEventInfo eventInfo)
 193    {
 1194        if (!CanRecordCell(eventInfo.VoxelIndex))
 1195            return;
 196
 0197        RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.ObstacleChanged);
 0198    }
 199
 200    private void HandleOccupantChanged(OccupantEventInfo eventInfo)
 201    {
 4202        if (!CanRecordCell(eventInfo.VoxelIndex))
 1203            return;
 204
 3205        RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.OccupantChanged);
 3206    }
 207
 208    private bool CanRecord(int worldSpawnToken) =>
 22209        !_disposed
 22210        && worldSpawnToken == _worldSpawnToken;
 211
 212    private bool CanRecordCell(WorldVoxelIndex worldIndex) =>
 10213        CanRecord(worldIndex.WorldSpawnToken)
 10214        && !HasPendingWorldReset()
 10215        && _world.TryGetGrid(worldIndex, out _);
 216
 217    private bool HasPendingWorldReset()
 218    {
 13219        lock (_syncRoot)
 13220            return _worldResetPending;
 13221    }
 222
 223    private void RecordGridChange(
 224        GridEventInfo eventInfo,
 225        GridDiagnosticChangeKind kind)
 226    {
 7227        RecordChange(new GridDiagnosticChange(
 7228            kind,
 7229            eventInfo.WorldSpawnToken,
 7230            eventInfo.GridIndex,
 7231            eventInfo.GridSpawnToken,
 7232            default,
 7233            default,
 7234            eventInfo.BoundsMin,
 7235            eventInfo.BoundsMax));
 7236    }
 237
 238    private void RecordSparseVoxelChange(
 239        GridEventInfo eventInfo,
 240        GridDiagnosticChangeKind kind)
 241    {
 2242        WorldVoxelIndex worldIndex = new(
 2243            eventInfo.WorldSpawnToken,
 2244            eventInfo.GridIndex,
 2245            eventInfo.GridSpawnToken,
 2246            eventInfo.VoxelIndex);
 247
 2248        RecordChange(new GridDiagnosticChange(
 2249            kind,
 2250            eventInfo.WorldSpawnToken,
 2251            eventInfo.GridIndex,
 2252            eventInfo.GridSpawnToken,
 2253            worldIndex,
 2254            eventInfo.VoxelIndex,
 2255            eventInfo.AffectedBoundsMin,
 2256            eventInfo.AffectedBoundsMax));
 257
 2258        RecordSparseAddressRangeChange(eventInfo);
 2259    }
 260
 261    private void RecordSparseAddressRangeChange(GridEventInfo eventInfo)
 262    {
 2263        Vector3d boundsMin = eventInfo.AffectedBoundsMin;
 2264        Vector3d boundsMax = eventInfo.AffectedBoundsMax;
 2265        if (_world.TryGetGrid(eventInfo.GridIndex, out VoxelGrid? grid))
 266        {
 2267            TopologyVoxelAabb bounds = TopologyVoxelAabb.FromIndex(grid!, eventInfo.VoxelIndex);
 2268            boundsMin = bounds.Min;
 2269            boundsMax = bounds.Max;
 270        }
 271
 2272        RecordChange(new GridDiagnosticChange(
 2273            GridDiagnosticChangeKind.SparseAddressChanged,
 2274            eventInfo.WorldSpawnToken,
 2275            eventInfo.GridIndex,
 2276            eventInfo.GridSpawnToken,
 2277            default,
 2278            eventInfo.VoxelIndex,
 2279            boundsMin,
 2280            boundsMax));
 2281    }
 282
 283    private void RecordCellChange(
 284        WorldVoxelIndex worldIndex,
 285        GridDiagnosticChangeKind kind)
 286    {
 6287        RecordChange(new GridDiagnosticChange(
 6288            kind,
 6289            worldIndex.WorldSpawnToken,
 6290            worldIndex.GridIndex,
 6291            worldIndex.GridSpawnToken,
 6292            worldIndex,
 6293            worldIndex.VoxelIndex,
 6294            default,
 6295            default));
 6296    }
 297
 298    private void RecordChange(GridDiagnosticChange change)
 299    {
 17300        GridDiagnosticChangeKey key = GridDiagnosticChangeKey.FromChange(change);
 17301        lock (_syncRoot)
 302        {
 17303            if (_changeIndexes.TryGetValue(key, out int index))
 304            {
 4305                GridDiagnosticChange existing = _changes[index];
 4306                _changes[index] = existing.WithKind(existing.Kind | change.Kind);
 4307                return;
 308            }
 309
 13310            _changeIndexes.Add(key, _changes.Count);
 13311            _changes.Add(change);
 13312        }
 17313    }
 314
 315    private readonly struct GridDiagnosticChangeKey : IEquatable<GridDiagnosticChangeKey>
 316    {
 317        private readonly GridDiagnosticChangeScope _scope;
 318        private readonly int _worldSpawnToken;
 319        private readonly ushort _gridIndex;
 320        private readonly int _gridSpawnToken;
 321        private readonly WorldVoxelIndex _worldIndex;
 322        private readonly VoxelIndex _voxelIndex;
 323        private readonly Vector3d _boundsMin;
 324        private readonly Vector3d _boundsMax;
 325
 326        private GridDiagnosticChangeKey(
 327            GridDiagnosticChangeScope scope,
 328            GridDiagnosticChange change)
 329        {
 19330            _scope = scope;
 19331            _worldSpawnToken = change.WorldSpawnToken;
 19332            _gridIndex = change.GridIndex;
 19333            _gridSpawnToken = change.GridSpawnToken;
 19334            _worldIndex = change.WorldIndex;
 19335            _voxelIndex = change.VoxelIndex;
 19336            _boundsMin = change.BoundsMin;
 19337            _boundsMax = change.BoundsMax;
 19338        }
 339
 340        public static GridDiagnosticChangeKey FromChange(GridDiagnosticChange change)
 341        {
 19342            GridDiagnosticChangeScope scope = GetScope(change);
 19343            return new GridDiagnosticChangeKey(scope, change);
 344        }
 345
 346        public bool Equals(GridDiagnosticChangeKey other) =>
 4347            _scope == other._scope
 4348            && _worldSpawnToken == other._worldSpawnToken
 4349            && _gridIndex == other._gridIndex
 4350            && _gridSpawnToken == other._gridSpawnToken
 4351            && _worldIndex.Equals(other._worldIndex)
 4352            && _voxelIndex.Equals(other._voxelIndex)
 4353            && _boundsMin.Equals(other._boundsMin)
 4354            && _boundsMax.Equals(other._boundsMax);
 355
 0356        public override bool Equals(object? obj) => obj is GridDiagnosticChangeKey other && Equals(other);
 357
 358        public override int GetHashCode()
 359        {
 32360            int hash = SwiftHashTools.CombineHashCodes((int)_scope, _worldSpawnToken);
 32361            hash = SwiftHashTools.CombineHashCodes(hash, _gridIndex);
 32362            hash = SwiftHashTools.CombineHashCodes(hash, _gridSpawnToken);
 32363            hash = SwiftHashTools.CombineHashCodes(hash, _worldIndex.GetHashCode());
 32364            hash = SwiftHashTools.CombineHashCodes(hash, _voxelIndex.GetHashCode());
 32365            hash = SwiftHashTools.CombineHashCodes(hash, _boundsMin.GetHashCode());
 32366            return SwiftHashTools.CombineHashCodes(hash, _boundsMax.GetHashCode());
 367        }
 368
 369        private static GridDiagnosticChangeScope GetScope(GridDiagnosticChange change)
 370        {
 19371            if ((change.Kind & GridDiagnosticChangeKind.WorldReset) != 0)
 2372                return GridDiagnosticChangeScope.World;
 373
 17374            if (change.WorldIndex.WorldSpawnToken != 0 || change.WorldIndex.VoxelIndex.IsAllocated)
 8375                return GridDiagnosticChangeScope.Cell;
 376
 9377            return (change.Kind & GridDiagnosticChangeKind.SparseAddressChanged) != 0
 9378                ? GridDiagnosticChangeScope.Range
 9379                : GridDiagnosticChangeScope.Grid;
 380        }
 381    }
 382
 383    private enum GridDiagnosticChangeScope
 384    {
 385        World = 0,
 386        Grid = 1,
 387        Cell = 2,
 388        Range = 3
 389    }
 390}