< Summary

Information
Class: GridForge.Diagnostics.GridDiagnosticSession
Assembly: GridForge
File(s): /home/runner/work/GridForge/GridForge/src/GridForge/Diagnostics/GridDiagnosticSession.cs
Line coverage
100%
Covered lines: 180
Uncovered lines: 0
Coverable lines: 180
Total lines: 335
Line coverage: 100%
Branch coverage
100%
Covered branches: 50
Total branches: 50
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%44100%
GetDirtyChangesInto(...)100%44100%
ClearDirtyChanges()100%11100%
Dispose()100%22100%
Subscribe()100%11100%
Unsubscribe()100%11100%
HandleActiveGridAdded(...)100%22100%
HandleActiveGridRemoved(...)100%22100%
HandleActiveGridChanged(...)100%88100%
HandleWorldReset()100%22100%
HandleObstacleChanged(...)100%22100%
HandleObstaclesCleared(...)100%22100%
HandleOccupantChanged(...)100%22100%
CanRecord(...)100%22100%
CanRecordGrid(...)100%44100%
CanRecordCell(...)100%44100%
HasPendingWorldReset()100%11100%
RecordGridChange(...)100%11100%
RecordSparseVoxelChange(...)100%11100%
RecordSparseAddressRangeChange(...)100%11100%
RecordCellChange(...)100%11100%
RecordChange(...)100%22100%
CreateChangeKey(...)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 System;
 9using GridForge.Grids;
 10using GridForge.Grids.Topology;
 11using GridForge.Spatial;
 12using SwiftCollections;
 13
 14namespace GridForge.Diagnostics;
 15
 16/// <summary>
 17/// Captures dirty diagnostic grid, cell, and sparse address changes for one
 18/// active <see cref="GridWorld"/>.
 19/// </summary>
 20public sealed class GridDiagnosticSession : IDisposable
 21{
 22    private readonly GridWorld _world;
 23    private readonly long _worldSpawnToken;
 1624    private readonly SwiftList<GridDiagnosticChange> _changes = new();
 1625    private readonly SwiftDictionary<GridDiagnosticChange, int> _changeIndexes = new();
 1626    private readonly object _syncRoot = new();
 27    private bool _worldResetPending;
 28    private bool _disposed;
 29
 30    /// <summary>
 31    /// Creates a diagnostic dirty-tracking session for the supplied active
 32    /// world.
 33    /// </summary>
 1634    public GridDiagnosticSession(GridWorld world)
 35    {
 1636        if (world == null)
 137            throw new ArgumentNullException(nameof(world));
 38
 1539        if (!world.IsActive)
 140            throw new InvalidOperationException("Diagnostic sessions require an active GridWorld.");
 41
 1442        _world = world;
 1443        _worldSpawnToken = world.SpawnToken;
 1444        Subscribe();
 1445    }
 46
 47    /// <summary>
 48    /// Clears and fills caller-owned storage with coalesced dirty changes.
 49    /// </summary>
 50    public int GetDirtyChangesInto(SwiftList<GridDiagnosticChange> results)
 51    {
 1752        SwiftThrowHelper.ThrowIfNull(results, nameof(results));
 53
 1754        results.Clear();
 1755        lock (_syncRoot)
 56        {
 6457            for (int i = 0; i < _changes.Count; i++)
 1558                results.Add(_changes[i]);
 1759        }
 60
 1761        if (results.Count > 1)
 562            results.SortInPlace();
 63
 1764        return results.Count;
 65    }
 66
 67    /// <summary>
 68    /// Clears all dirty changes captured so far.
 69    /// </summary>
 70    public void ClearDirtyChanges()
 71    {
 1772        lock (_syncRoot)
 73        {
 1774            _changes.Clear();
 1775            _changeIndexes.Clear();
 1776            _worldResetPending = false;
 1777        }
 1778    }
 79
 80    /// <inheritdoc/>
 81    public void Dispose()
 82    {
 1783        if (_disposed)
 384            return;
 85
 1486        Unsubscribe();
 1487        ClearDirtyChanges();
 1488        _disposed = true;
 1489        GC.SuppressFinalize(this);
 1490    }
 91
 92    private void Subscribe()
 93    {
 1494        _world.OnActiveGridAdded += HandleActiveGridAdded;
 1495        _world.OnActiveGridRemoved += HandleActiveGridRemoved;
 1496        _world.OnActiveGridChange += HandleActiveGridChanged;
 1497        _world.OnReset += HandleWorldReset;
 1498        GridObstacleManager.OnObstacleAdded += HandleObstacleChanged;
 1499        GridObstacleManager.OnObstacleRemoved += HandleObstacleChanged;
 14100        GridObstacleManager.OnObstaclesCleared += HandleObstaclesCleared;
 14101        GridOccupantManager.OnOccupantAdded += HandleOccupantChanged;
 14102        GridOccupantManager.OnOccupantRemoved += HandleOccupantChanged;
 14103    }
 104
 105    private void Unsubscribe()
 106    {
 14107        _world.OnActiveGridAdded -= HandleActiveGridAdded;
 14108        _world.OnActiveGridRemoved -= HandleActiveGridRemoved;
 14109        _world.OnActiveGridChange -= HandleActiveGridChanged;
 14110        _world.OnReset -= HandleWorldReset;
 14111        GridObstacleManager.OnObstacleAdded -= HandleObstacleChanged;
 14112        GridObstacleManager.OnObstacleRemoved -= HandleObstacleChanged;
 14113        GridObstacleManager.OnObstaclesCleared -= HandleObstaclesCleared;
 14114        GridOccupantManager.OnOccupantAdded -= HandleOccupantChanged;
 14115        GridOccupantManager.OnOccupantRemoved -= HandleOccupantChanged;
 14116    }
 117
 118    private void HandleActiveGridAdded(GridEventInfo eventInfo)
 119    {
 4120        if (!CanRecordGrid(eventInfo, out _))
 1121            return;
 122
 3123        RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridAdded);
 3124    }
 125
 126    private void HandleActiveGridRemoved(GridEventInfo eventInfo)
 127    {
 3128        if (!CanRecord(eventInfo.WorldSpawnToken))
 1129            return;
 130
 2131        RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridRemoved);
 2132    }
 133
 134    private void HandleActiveGridChanged(GridEventInfo eventInfo)
 135    {
 10136        if (!CanRecordGrid(eventInfo, out VoxelGrid? grid) || HasPendingWorldReset())
 3137            return;
 138
 7139        switch (eventInfo.ChangeKind)
 140        {
 141            case GridEventKind.SparseVoxelAdded:
 1142                RecordSparseVoxelChange(eventInfo, grid!, GridDiagnosticChangeKind.SparseVoxelAdded);
 1143                break;
 144            case GridEventKind.SparseVoxelRemoved:
 1145                RecordSparseVoxelChange(eventInfo, grid!, GridDiagnosticChangeKind.SparseVoxelRemoved);
 1146                break;
 147            default:
 5148                RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridChanged);
 149                break;
 150        }
 5151    }
 152
 153    private void HandleWorldReset()
 154    {
 3155        if (!CanRecord(_worldSpawnToken))
 1156            return;
 157
 2158        GridDiagnosticChange change = new(
 2159            GridDiagnosticChangeKind.WorldReset,
 2160            _worldSpawnToken,
 2161            ushort.MaxValue,
 2162            0,
 2163            default,
 2164            default,
 2165            default,
 2166            default);
 2167        GridDiagnosticChange key = CreateChangeKey(change);
 168
 2169        lock (_syncRoot)
 170        {
 2171            _changes.Clear();
 2172            _changeIndexes.Clear();
 2173            _worldResetPending = true;
 2174            _changeIndexes.Add(key, 0);
 2175            _changes.Add(change);
 2176        }
 2177    }
 178
 179    private void HandleObstacleChanged(ObstacleEventInfo eventInfo)
 180    {
 7181        if (!CanRecordCell(eventInfo.VoxelIndex))
 3182            return;
 183
 4184        RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.ObstacleChanged);
 4185    }
 186
 187    private void HandleObstaclesCleared(ObstacleClearEventInfo eventInfo)
 188    {
 2189        if (!CanRecordCell(eventInfo.VoxelIndex))
 1190            return;
 191
 1192        RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.ObstacleChanged);
 1193    }
 194
 195    private void HandleOccupantChanged(OccupantEventInfo eventInfo)
 196    {
 4197        if (!CanRecordCell(eventInfo.VoxelIndex))
 1198            return;
 199
 3200        RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.OccupantChanged);
 3201    }
 202
 203    private bool CanRecord(long worldSpawnToken) =>
 33204        !_disposed
 33205        && worldSpawnToken == _worldSpawnToken;
 206
 207    private bool CanRecordGrid(GridEventInfo eventInfo, out VoxelGrid? grid)
 208    {
 14209        grid = null;
 14210        return CanRecord(eventInfo.WorldSpawnToken)
 14211            && _world.TryGetGrid(eventInfo.GridIndex, out grid)
 14212            && grid!.SpawnToken == eventInfo.GridSpawnToken;
 213    }
 214
 215    private bool CanRecordCell(WorldVoxelIndex worldIndex) =>
 13216        CanRecord(worldIndex.WorldSpawnToken)
 13217        && !HasPendingWorldReset()
 13218        && _world.TryGetGrid(worldIndex, out _);
 219
 220    private bool HasPendingWorldReset()
 221    {
 17222        lock (_syncRoot)
 17223            return _worldResetPending;
 17224    }
 225
 226    private void RecordGridChange(
 227        GridEventInfo eventInfo,
 228        GridDiagnosticChangeKind kind)
 229    {
 10230        RecordChange(new GridDiagnosticChange(
 10231            kind,
 10232            eventInfo.WorldSpawnToken,
 10233            eventInfo.GridIndex,
 10234            eventInfo.GridSpawnToken,
 10235            default,
 10236            default,
 10237            eventInfo.BoundsMin,
 10238            eventInfo.BoundsMax));
 10239    }
 240
 241    private void RecordSparseVoxelChange(
 242        GridEventInfo eventInfo,
 243        VoxelGrid grid,
 244        GridDiagnosticChangeKind kind)
 245    {
 2246        WorldVoxelIndex worldIndex = new(
 2247            eventInfo.WorldSpawnToken,
 2248            eventInfo.GridIndex,
 2249            eventInfo.GridSpawnToken,
 2250            eventInfo.VoxelIndex);
 251
 2252        RecordChange(new GridDiagnosticChange(
 2253            kind,
 2254            eventInfo.WorldSpawnToken,
 2255            eventInfo.GridIndex,
 2256            eventInfo.GridSpawnToken,
 2257            worldIndex,
 2258            eventInfo.VoxelIndex,
 2259            eventInfo.AffectedBoundsMin,
 2260            eventInfo.AffectedBoundsMax));
 261
 2262        RecordSparseAddressRangeChange(eventInfo, grid);
 2263    }
 264
 265    private void RecordSparseAddressRangeChange(GridEventInfo eventInfo, VoxelGrid grid)
 266    {
 2267        TopologyVoxelAabb bounds = TopologyVoxelAabb.FromIndex(grid, eventInfo.VoxelIndex);
 268
 2269        RecordChange(new GridDiagnosticChange(
 2270            GridDiagnosticChangeKind.SparseAddressChanged,
 2271            eventInfo.WorldSpawnToken,
 2272            eventInfo.GridIndex,
 2273            eventInfo.GridSpawnToken,
 2274            default,
 2275            eventInfo.VoxelIndex,
 2276            bounds.Min,
 2277            bounds.Max));
 2278    }
 279
 280    private void RecordCellChange(
 281        WorldVoxelIndex worldIndex,
 282        GridDiagnosticChangeKind kind)
 283    {
 8284        RecordChange(new GridDiagnosticChange(
 8285            kind,
 8286            worldIndex.WorldSpawnToken,
 8287            worldIndex.GridIndex,
 8288            worldIndex.GridSpawnToken,
 8289            worldIndex,
 8290            worldIndex.VoxelIndex,
 8291            default,
 8292            default));
 8293    }
 294
 295    private void RecordChange(GridDiagnosticChange change)
 296    {
 22297        GridDiagnosticChange key = CreateChangeKey(change);
 22298        lock (_syncRoot)
 299        {
 22300            if (_changeIndexes.TryGetValue(key, out int index))
 301            {
 6302                GridDiagnosticChange existing = _changes[index];
 6303                _changes[index] = existing.WithKind(existing.Kind | change.Kind);
 6304                return;
 305            }
 306
 16307            _changeIndexes.Add(key, _changes.Count);
 16308            _changes.Add(change);
 16309        }
 22310    }
 311
 312    private static GridDiagnosticChange CreateChangeKey(GridDiagnosticChange change) =>
 24313        change.WithKind((GridDiagnosticChangeKind)GetScope(change));
 314
 315    private static GridDiagnosticChangeScope GetScope(GridDiagnosticChange change)
 316    {
 24317        if ((change.Kind & GridDiagnosticChangeKind.WorldReset) != 0)
 2318            return GridDiagnosticChangeScope.World;
 319
 22320        if (change.WorldIndex.WorldSpawnToken != 0 || change.WorldIndex.VoxelIndex.IsAllocated)
 10321            return GridDiagnosticChangeScope.Cell;
 322
 12323        return (change.Kind & GridDiagnosticChangeKind.SparseAddressChanged) != 0
 12324            ? GridDiagnosticChangeScope.Range
 12325            : GridDiagnosticChangeScope.Grid;
 326    }
 327
 328    private enum GridDiagnosticChangeScope
 329    {
 330        World = 0,
 331        Grid = 1,
 332        Cell = 2,
 333        Range = 3
 334    }
 335}