From 73e4946e27805b473e70d4949c03b1070e78b4b1 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 31 Jul 2021 02:50:11 +0200 Subject: [PATCH] Pow3r goes brrr with generational IDs. --- .../Power/EntitySystems/PowerNetSystem.cs | 30 +- Content.Server/Power/Pow3r/PowerState.cs | 309 ++++++++++++++++-- Pow3r/Program.SaveLoad.cs | 11 +- Pow3r/Program.Simulation.cs | 6 - Pow3r/Program.UI.cs | 32 +- 5 files changed, 310 insertions(+), 78 deletions(-) diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index 84b65289b5..d8eee5d111 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -19,7 +19,6 @@ namespace Content.Server.Power.EntitySystems private readonly HashSet _powerNetReconnectQueue = new(); private readonly HashSet _apcNetReconnectQueue = new(); - private int _nextId = 1; private readonly BatteryRampPegSolver _solver = new(); public override void Initialize() @@ -50,7 +49,7 @@ namespace Content.Server.Power.EntitySystems private void ApcPowerReceiverShutdown(EntityUid uid, ApcPowerReceiverComponent component, ComponentShutdown args) { - _powerState.Loads.Remove(component.NetworkLoad.Id); + _powerState.Loads.Free(component.NetworkLoad.Id); } private static void ApcPowerReceiverPaused( @@ -68,7 +67,7 @@ namespace Content.Server.Power.EntitySystems private void BatteryShutdown(EntityUid uid, PowerNetworkBatteryComponent component, ComponentShutdown args) { - _powerState.Batteries.Remove(component.NetworkBattery.Id); + _powerState.Batteries.Free(component.NetworkBattery.Id); } private static void BatteryPaused(EntityUid uid, PowerNetworkBatteryComponent component, EntityPausedEvent args) @@ -83,7 +82,7 @@ namespace Content.Server.Power.EntitySystems private void PowerConsumerShutdown(EntityUid uid, PowerConsumerComponent component, ComponentShutdown args) { - _powerState.Loads.Remove(component.NetworkLoad.Id); + _powerState.Loads.Free(component.NetworkLoad.Id); } private static void PowerConsumerPaused(EntityUid uid, PowerConsumerComponent component, EntityPausedEvent args) @@ -98,7 +97,7 @@ namespace Content.Server.Power.EntitySystems private void PowerSupplierShutdown(EntityUid uid, PowerSupplierComponent component, ComponentShutdown args) { - _powerState.Supplies.Remove(component.NetworkSupply.Id); + _powerState.Supplies.Free(component.NetworkSupply.Id); } private static void PowerSupplierPaused(EntityUid uid, PowerSupplierComponent component, EntityPausedEvent args) @@ -113,7 +112,7 @@ namespace Content.Server.Power.EntitySystems public void DestroyPowerNet(PowerNet powerNet) { - _powerState.Networks.Remove(powerNet.NetworkNode.Id); + _powerState.Networks.Free(powerNet.NetworkNode.Id); } public void QueueReconnectPowerNet(PowerNet powerNet) @@ -128,7 +127,7 @@ namespace Content.Server.Power.EntitySystems public void DestroyApcNet(ApcNet apcNet) { - _powerState.Networks.Remove(apcNet.NetworkNode.Id); + _powerState.Networks.Free(apcNet.NetworkNode.Id); } public void QueueReconnectApcNet(ApcNet apcNet) @@ -213,26 +212,22 @@ namespace Content.Server.Power.EntitySystems private void AllocLoad(PowerState.Load load) { - load.Id = AllocId(); - _powerState.Loads.Add(load.Id, load); + _powerState.Loads.Allocate(out load.Id) = load; } private void AllocSupply(PowerState.Supply supply) { - supply.Id = AllocId(); - _powerState.Supplies.Add(supply.Id, supply); + _powerState.Supplies.Allocate(out supply.Id) = supply; } private void AllocBattery(PowerState.Battery battery) { - battery.Id = AllocId(); - _powerState.Batteries.Add(battery.Id, battery); + _powerState.Batteries.Allocate(out battery.Id) = battery; } private void AllocNetwork(PowerState.Network network) { - network.Id = AllocId(); - _powerState.Networks.Add(network.Id, network); + _powerState.Networks.Allocate(out network.Id) = network; } private static void DoReconnectApcNet(ApcNet net) @@ -296,11 +291,6 @@ namespace Content.Server.Power.EntitySystems battery.NetworkBattery.LinkedNetworkDischarging = netNode.Id; } } - - private PowerState.NodeId AllocId() - { - return new(_nextId++); - } } /// diff --git a/Content.Server/Power/Pow3r/PowerState.cs b/Content.Server/Power/Pow3r/PowerState.cs index 276fc785a9..0d220b9259 100644 --- a/Content.Server/Power/Pow3r/PowerState.cs +++ b/Content.Server/Power/Pow3r/PowerState.cs @@ -1,40 +1,51 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Numerics; +using System.Linq; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Power.Pow3r { public sealed class PowerState { - public const int MaxTickData = 180; - public static readonly JsonSerializerOptions SerializerOptions = new() { IncludeFields = true, Converters = {new NodeIdJsonConverter()} }; - public Dictionary Supplies = new(); - public Dictionary Networks = new(); - public Dictionary Loads = new(); - public Dictionary Batteries = new(); + public GenIdStorage Supplies = new(); + public GenIdStorage Networks = new(); + public GenIdStorage Loads = new(); + public GenIdStorage Batteries = new(); public readonly struct NodeId : IEquatable { - public readonly int Id; + public readonly int Index; + public readonly int Generation; - public NodeId(int id) + public long Combined => (uint) Index | ((long) Generation << 32); + + public NodeId(int index, int generation) { - Id = id; + Index = index; + Generation = generation; + } + + public NodeId(long combined) + { + Index = (int) combined; + Generation = (int) (combined >> 32); } public bool Equals(NodeId other) { - return Id == other.Id; + return Index == other.Index && Generation == other.Generation; } public override bool Equals(object? obj) @@ -44,7 +55,7 @@ namespace Content.Server.Power.Pow3r public override int GetHashCode() { - return Id; + return HashCode.Combine(Index, Generation); } public static bool operator ==(NodeId left, NodeId right) @@ -59,7 +70,261 @@ namespace Content.Server.Power.Pow3r public override string ToString() { - return Id.ToString(); + return $"{Index} (G{Generation})"; + } + } + + public static class GenIdStorage + { + public static GenIdStorage FromEnumerable(IEnumerable<(NodeId, T)> enumerable) + { + return GenIdStorage.FromEnumerable(enumerable); + } + } + + public sealed class GenIdStorage + { + // This is an implementation of "generational index" storage. + // + // The advantage of this storage method is extremely fast, O(1) lookup (way faster than Dictionary). + // Resolving a value in the storage is a single array load and generation compare. Extremely fast. + // Indices can also be cached into temporary + // Disadvantages are that storage cannot be shrunk, and sparse storage is inefficient space wise. + // Also this implementation does not have optimizations necessary to make sparse iteration efficient. + // + // The idea here is that the index type (NodeId in this case) has both an index and a generation. + // The index is an integer index into the storage array, the generation is used to avoid use-after-free. + // + // Empty slots in the array form a linked list of free slots. + // When we allocate a new slot, we pop one link off this linked list and hand out its index + generation. + // + // When we free a node, we bump the generation of the slot and make it the head of the linked list. + // The generation being bumped means that any IDs to this slot will fail to resolve (generation mismatch). + // + + // Index of the next free slot to use when allocating a new one. + // If this is int.MaxValue, + // it basically means "no slot available" and the next allocation call should resize the array storage. + private int _nextFree = int.MaxValue; + private Slot[] _storage; + + public int Count { get; private set; } + + public ref T this[NodeId id] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ref var slot = ref _storage[id.Index]; + if (slot.Generation != id.Generation) + ThrowKeyNotFound(); + + return ref slot.Value; + } + } + + public GenIdStorage() + { + _storage = Array.Empty(); + } + + public static GenIdStorage FromEnumerable(IEnumerable<(NodeId, T)> enumerable) + { + var storage = new GenIdStorage(); + + // Cache enumerable to array to do double enumeration. + var cache = enumerable.ToArray(); + + if (cache.Length == 0) + return storage; + + // Figure out max size necessary and set storage size to that. + var maxSize = cache.Max(tup => tup.Item1.Index) + 1; + storage._storage = new Slot[maxSize]; + + // Fill in slots. + foreach (var (id, value) in cache) + { + DebugTools.Assert(id.Generation != 0, "Generation cannot be 0"); + + ref var slot = ref storage._storage[id.Index]; + DebugTools.Assert(slot.Generation == 0, "Duplicate key index!"); + + slot.Generation = id.Generation; + slot.Value = value; + } + + // Go through empty slots and build the free chain. + var nextFree = int.MaxValue; + for (var i = 0; i < storage._storage.Length; i++) + { + ref var slot = ref storage._storage[i]; + + if (slot.Generation != 0) + // Slot in use. + continue; + + slot.NextSlot = nextFree; + nextFree = i; + } + + storage.Count = cache.Length; + storage._nextFree = nextFree; + + return storage; + } + + public ref T Allocate(out NodeId id) + { + if (_nextFree == int.MaxValue) + ReAllocate(); + + var idx = _nextFree; + ref var slot = ref _storage[idx]; + + Count += 1; + _nextFree = slot.NextSlot; + // NextSlot = -1 indicates filled. + slot.NextSlot = -1; + + id = new NodeId(idx, slot.Generation); + return ref slot.Value; + } + + public void Free(NodeId id) + { + var idx = id.Index; + ref var slot = ref _storage[idx]; + if (slot.Generation != id.Generation) + ThrowKeyNotFound(); + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + slot.Value = default!; + + Count -= 1; + slot.Generation += 1; + slot.NextSlot = _nextFree; + _nextFree = idx; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ReAllocate() + { + var oldLength = _storage.Length; + var newLength = Math.Max(oldLength, 2) * 2; + + ReAllocateTo(newLength); + } + + private void ReAllocateTo(int newSize) + { + var oldLength = _storage.Length; + DebugTools.Assert(newSize >= oldLength, "Cannot shrink GenIdStorage"); + + Array.Resize(ref _storage, newSize); + + for (var i = oldLength; i < newSize - 1; i++) + { + // Build linked list chain for newly allocated segment. + ref var slot = ref _storage[i]; + slot.NextSlot = i + 1; + // Every slot starts at generation 1. + slot.Generation = 1; + } + + _storage[^1].NextSlot = _nextFree; + + _nextFree = oldLength; + } + + public ValuesCollection Values => new(this); + + private struct Slot + { + // Next link on the free list. if int.MaxValue then this is the tail. + // If negative, this slot is occupied. + public int NextSlot; + // Generation of this slot. + public int Generation; + public T Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowKeyNotFound() + { + throw new KeyNotFoundException(); + } + + public readonly struct ValuesCollection : IReadOnlyCollection + { + private readonly GenIdStorage _owner; + + public ValuesCollection(GenIdStorage owner) + { + _owner = owner; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_owner); + } + + public int Count => _owner.Count; + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + // Save the array in the enumerator here to avoid a few pointer dereferences. + private readonly Slot[] _owner; + private int _index; + + public Enumerator(GenIdStorage owner) + { + _owner = owner._storage; + Current = default!; + _index = -1; + } + + public bool MoveNext() + { + while (true) + { + _index += 1; + if (_index >= _owner.Length) + return false; + + ref var slot = ref _owner[_index]; + + if (slot.NextSlot < 0) + { + Current = slot.Value; + return true; + } + } + } + + public void Reset() + { + _index = -1; + } + + object IEnumerator.Current => Current!; + + public T Current { get; private set; } + + public void Dispose() + { + } + } } } @@ -67,12 +332,12 @@ namespace Content.Server.Power.Pow3r { public override NodeId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return new(reader.GetInt32()); + return new NodeId(reader.GetInt64()); } public override void Write(Utf8JsonWriter writer, NodeId value, JsonSerializerOptions options) { - writer.WriteNumberValue(value.Id); + writer.WriteNumberValue(value.Combined); } } @@ -186,22 +451,8 @@ namespace Content.Server.Power.Pow3r // "Supplying" means the network is connected to the OUTPUT port of the battery. [ViewVariables] public List BatteriesDischarging = new(); - // Calculation parameters used by GraphWalkSolver. - // Unused by BatteryRampPegSolver. - [JsonIgnore] public float LocalDemandTotal; - [JsonIgnore] public float LocalDemandMet; - [JsonIgnore] public float GroupDemandTotal; - [JsonIgnore] public float GroupDemandMet; - [ViewVariables] [JsonIgnore] public int Height; [JsonIgnore] public bool HeightTouched; - - // Supply available this tick. - [JsonIgnore] public float AvailableSupplyTotal; - - // Max theoretical supply assuming max ramp. - [JsonIgnore] public float TheoreticalSupplyTotal; - public float RemainingDemand => LocalDemandTotal - LocalDemandMet; } } } diff --git a/Pow3r/Program.SaveLoad.cs b/Pow3r/Program.SaveLoad.cs index 2ffdc8eb4e..5fff64f662 100644 --- a/Pow3r/Program.SaveLoad.cs +++ b/Pow3r/Program.SaveLoad.cs @@ -20,15 +20,14 @@ namespace Pow3r return; _paused = dat.Paused; - _nextId = dat.NextId; _currentSolver = dat.Solver; _state = new PowerState { - Networks = dat.Networks.ToDictionary(n => n.Id, n => n), - Supplies = dat.Supplies.ToDictionary(s => s.Id, s => s), - Loads = dat.Loads.ToDictionary(l => l.Id, l => l), - Batteries = dat.Batteries.ToDictionary(b => b.Id, b => b) + Networks = GenIdStorage.FromEnumerable(dat.Networks.Select(n => (n.Id, n))), + Supplies = GenIdStorage.FromEnumerable(dat.Supplies.Select(s => (s.Id, s))), + Loads = GenIdStorage.FromEnumerable(dat.Loads.Select(l => (l.Id, l))), + Batteries = GenIdStorage.FromEnumerable(dat.Batteries.Select(b => (b.Id, b))) }; _displayLoads = dat.Loads.ToDictionary(n => n.Id, _ => new DisplayLoad()); @@ -44,7 +43,6 @@ namespace Pow3r var data = new DiskDat { Paused = _paused, - NextId = _nextId, Solver = _currentSolver, Loads = _state.Loads.Values.ToList(), @@ -59,7 +57,6 @@ namespace Pow3r private sealed class DiskDat { public bool Paused; - public int NextId; public int Solver; public List Loads; diff --git a/Pow3r/Program.Simulation.cs b/Pow3r/Program.Simulation.cs index 2c8072e3e8..fac2dbc415 100644 --- a/Pow3r/Program.Simulation.cs +++ b/Pow3r/Program.Simulation.cs @@ -10,7 +10,6 @@ namespace Pow3r { private const int MaxTickData = 180; - private int _nextId = 1; private PowerState _state = new(); private Network _linking; private int _tickDataIdx; @@ -33,11 +32,6 @@ namespace Pow3r private readonly Queue _remQueue = new(); private readonly Stopwatch _simStopwatch = new Stopwatch(); - private NodeId AllocId() - { - return new(_nextId++); - } - private void Tick(float frameTime) { if (_paused) diff --git a/Pow3r/Program.UI.cs b/Pow3r/Program.UI.cs index 96f7b90cf8..9a99a52699 100644 --- a/Pow3r/Program.UI.cs +++ b/Pow3r/Program.UI.cs @@ -36,30 +36,30 @@ namespace Pow3r if (Button("Generator")) { - var id = AllocId(); - _state.Supplies.Add(id, new Supply { Id = id }); - _displaySupplies.Add(id, new DisplaySupply()); + var supply = new Supply(); + _state.Supplies.Allocate(out supply.Id) = supply; + _displaySupplies.Add(supply.Id, new DisplaySupply()); } if (Button("Load")) { - var id = AllocId(); - _state.Loads.Add(id, new Load { Id = id }); - _displayLoads.Add(id, new DisplayLoad()); + var load = new Load(); + _state.Loads.Allocate(out load.Id) = load; + _displayLoads.Add(load.Id, new DisplayLoad()); } if (Button("Network")) { - var id = AllocId(); - _state.Networks.Add(id, new Network { Id = id }); - _displayNetworks.Add(id, new DisplayNetwork()); + var network = new Network(); + _state.Networks.Allocate(out network.Id) = network; + _displayNetworks.Add(network.Id, new DisplayNetwork()); } if (Button("Battery")) { - var id = AllocId(); - _state.Batteries.Add(id, new Battery { Id = id }); - _displayBatteries.Add(id, new DisplayBattery()); + var battery = new Battery(); + _state.Batteries.Allocate(out battery.Id) = battery; + _displayBatteries.Add(battery.Id, new DisplayBattery()); } Checkbox("Paused", ref _paused); @@ -355,25 +355,25 @@ namespace Pow3r switch (item) { case Network n: - _state.Networks.Remove(n.Id); + _state.Networks.Free(n.Id); _displayNetworks.Remove(n.Id); reLink = true; break; case Supply s: - _state.Supplies.Remove(s.Id); + _state.Supplies.Free(s.Id); _state.Networks.Values.ForEach(n => n.Supplies.Remove(s.Id)); _displaySupplies.Remove(s.Id); break; case Load l: - _state.Loads.Remove(l.Id); + _state.Loads.Free(l.Id); _state.Networks.Values.ForEach(n => n.Loads.Remove(l.Id)); _displayLoads.Remove(l.Id); break; case Battery b: - _state.Batteries.Remove(b.Id); + _state.Batteries.Free(b.Id); _state.Networks.Values.ForEach(n => n.BatteriesCharging.Remove(b.Id)); _state.Networks.Values.ForEach(n => n.BatteriesDischarging.Remove(b.Id)); _displayBatteries.Remove(b.Id);